Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 8 additions & 13 deletions .github/scripts/appcast.sh
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
#!/usr/bin/env bash
# appcast.sh VERSION TAG DIR - write DIR/appcast.xml, the Sparkle feed for the
# macOS .dmg in DIR. The app reads it from the latest published release
# (SUFeedURL in scripts/bundle-app.sh).
# appcast.sh VERSION TAG DIR NOTES_HTML - write DIR/appcast.xml, the Sparkle
# feed for the macOS .dmg in DIR, with the release notes in NOTES_HTML. The app
# reads it from the latest published release (SUFeedURL in scripts/bundle-app.sh).
#
# Sparkle compares sparkle:version with the bundle's CFBundleVersion, which is
# the version. The release notes are the annotated tag's message.
# the version.
set -euo pipefail
version="$1"
tag="$2"
dir="$3"
notes_html="$4"
: "${GH_REPO:?}"
script_dir="$(cd "$(dirname "$0")" && pwd)"

Expand All @@ -19,14 +20,8 @@ length=$(wc -c < "$dmg" | tr -d ' ')
url="https://github.com/$GH_REPO/releases/download/$tag/$(basename "$dmg")"
published=$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')

description=""
notes=$("$script_dir/tag-notes.sh" "$tag")
if [[ -n "${notes//[[:space:]]/}" ]]; then
# A CDATA section cannot hold its own terminator.
html=$(gh api markdown -f text="$notes" -f mode=gfm -f context="$GH_REPO" \
| sed 's/]]>/]]]]><![CDATA[>/g')
description=" <description><![CDATA[$html]]></description>"
fi
# A CDATA section cannot hold its own terminator.
notes=$(sed 's/]]>/]]]]><![CDATA[>/g' "$notes_html")

cat > "$dir/appcast.xml" <<XML
<?xml version="1.0" encoding="utf-8"?>
Expand All @@ -41,7 +36,7 @@ cat > "$dir/appcast.xml" <<XML
<sparkle:shortVersionString>$version</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
<sparkle:fullReleaseNotesLink>https://github.com/$GH_REPO/releases/tag/$tag</sparkle:fullReleaseNotesLink>
$description
<description><![CDATA[$notes]]></description>
<enclosure url="$url" length="$length" type="application/octet-stream" sparkle:edSignature="$signature"/>
</item>
</channel>
Expand Down
106 changes: 106 additions & 0 deletions .github/scripts/release-draft.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
# release-draft.sh VERSION TAG DIR NOTES_MD - fill the draft release TAG from
# the packages in DIR and the release notes in NOTES_MD: write and sign
# appcast.xml, write SHA256SUMS.txt, set the release text and upload every file
# in DIR. The draft is created if it does not exist; a published release fails
# the run.
#
# The release text is the notes, a table of the downloads, and GitHub's
# generated list of pull requests in a collapsed block. When NOTES_MD is empty,
# that generated list is the notes, for the release text and the feed.
#
# With NOTES_ONLY=1, DIR holds only the .dmg and the draft's SHA256SUMS.txt,
# and only appcast.xml is uploaded again.
set -euo pipefail
version="$1"
tag="$2"
dir="$3"
notes_md="$4"
: "${GH_REPO:?}"
script_dir="$(cd "$(dirname "$0")" && pwd)"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT

draft=$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null || true)
if [[ "$draft" == false ]]; then
echo "::error::$tag is published; its text and files are no longer changed" >&2
exit 1
fi

gh api "repos/$GH_REPO/releases/generate-notes" -f tag_name="$tag" --jq .body \
| tr -d '\r' > "$work/generated.md"
if [[ -z "$(tr -d '[:space:]' < "$notes_md")" ]]; then
notes_md="$work/generated.md"
fi

if [[ -n "${NOTES_ONLY:-}" ]]; then
[[ -f "$dir/SHA256SUMS.txt" ]] || { echo "::error::$tag has no SHA256SUMS.txt" >&2; exit 1; }
else
rm -f "$dir/SHA256SUMS.txt" "$dir/appcast.xml"
(cd "$dir" && sha256sum -- * > "$work/SHA256SUMS.txt")
mv "$work/SHA256SUMS.txt" "$dir/"
fi
cat "$dir/SHA256SUMS.txt"
packages=$(awk '{print $2}' "$dir/SHA256SUMS.txt")

# Sparkle's window shows the notes as HTML.
gh api markdown -f text="$(cat "$notes_md")" -f mode=gfm -f context="$GH_REPO" > "$work/notes.html"
"$script_dir/appcast.sh" "$version" "$tag" "$dir" "$work/notes.html"

# Prints one table row per host, with a link per package in SHA256SUMS.txt.
downloads() {
local base="https://github.com/$GH_REPO/releases/download/$tag"
local row host files file links
echo "| Host | Package |"
echo "|---|---|"
for row in \
"macOS 14 or later, Apple silicon|macos-arm64.dmg:dmg" \
"Windows x64|setup.exe:installer windows-x64.zip:zip" \
"Debian, Ubuntu|linux-x86_64.deb:x86_64 linux-aarch64.deb:aarch64" \
"Fedora, RHEL|linux-x86_64.rpm:x86_64 linux-aarch64.rpm:aarch64" \
"Any Linux distribution|linux-x86_64.AppImage:x86_64 linux-aarch64.AppImage:aarch64"; do
host="${row%%|*}"
files="${row#*|}"
links=""
for file in $files; do
name="stemd-$version-${file%%:*}"
grep -qxF "$name" <<< "$packages" || continue
links+="${links:+ · }[${file#*:}]($base/$name)"
done
if [[ -n "$links" ]]; then
echo "| $host | $links |"
fi
done
echo
echo "Windows and Linux use an NVIDIA GPU with driver 580 or later, else the CPU."
echo "Linux needs glibc 2.35 or later. Checksums are in \`SHA256SUMS.txt\`."
}

{
cat "$notes_md"
echo
echo "## Downloads"
echo
downloads
if [[ "$notes_md" != "$work/generated.md" ]]; then
echo
echo "<details>"
echo "<summary>Pull requests</summary>"
echo
cat "$work/generated.md"
echo
echo "</details>"
fi
} > "$work/body.md"

if [[ -n "$draft" ]]; then
gh release edit "$tag" --notes-file "$work/body.md" >/dev/null
else
gh release create "$tag" --draft --verify-tag --title "Stemd $version" \
--notes-file "$work/body.md" >/dev/null
fi
if [[ -n "${NOTES_ONLY:-}" ]]; then
gh release upload "$tag" --clobber -- "$dir/appcast.xml"
else
(cd "$dir" && gh release upload "$tag" --clobber -- *)
fi
30 changes: 12 additions & 18 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
# NOTARY_ISSUER for a team key. Without them the app is signed ad hoc and no .dmg is made; a
# release tag fails.
#
# A release also gets appcast.xml, the Sparkle feed of the macOS app. The
# secret UPDATE_SIGNING_KEY signs the .dmg in it; its public half is
# A release also gets appcast.xml, the Sparkle feed of the macOS app, and
# SHA256SUMS.txt (.github/scripts/release-draft.sh). The secret
# UPDATE_SIGNING_KEY signs the .dmg in the feed; its public half is
# packaging/update-ed25519.pub, which the bundle carries as SUPublicEDKey.
name: CD

Expand Down Expand Up @@ -314,22 +315,15 @@ jobs:
with:
path: dist
merge-multiple: true
- name: Update feed
# A draft, so nothing is public until published by hand. The annotated
# tag's message holds the release notes, which go into the release text
# and appcast.xml; GitHub's generated notes stand in for an empty one.
# A rerun replaces the draft's text and files.
- name: Draft release
env:
UPDATE_SIGNING_KEY: ${{ secrets.UPDATE_SIGNING_KEY }}
run: .github/scripts/appcast.sh "$VERSION" "$GITHUB_REF_NAME" dist
# A draft, so nothing is public until published by hand. A rerun
# replaces the files.
- name: Draft release
run: |
tag="$GITHUB_REF_NAME"
draft=$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null || true)
if [[ "$draft" == false ]]; then
echo "::error::$tag is published; its files are no longer changed"
exit 1
fi
if [[ -z "$draft" ]]; then
gh release create "$tag" --draft --verify-tag --title "stemd ${tag#v}" --notes ""
fi
gh release upload "$tag" --clobber dist/*
(cd dist && sha256sum -- *) | tee -a "$GITHUB_STEP_SUMMARY"
.github/scripts/tag-notes.sh "$GITHUB_REF_NAME" > "$RUNNER_TEMP/notes.md"
.github/scripts/release-draft.sh "$VERSION" "$GITHUB_REF_NAME" dist "$RUNNER_TEMP/notes.md"
cat dist/SHA256SUMS.txt >> "$GITHUB_STEP_SUMMARY"
echo "Draft: $(gh release view "$GITHUB_REF_NAME" --json url -q .url)" >> "$GITHUB_STEP_SUMMARY"
53 changes: 53 additions & 0 deletions .github/workflows/release-notes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Replace the release notes of a draft release: its text and appcast.xml,
# signed again. CD takes the notes from the tag's message; this workflow
# corrects them without a new build:
#
# gh workflow run release-notes.yml -f tag=v0.2.0 -F notes=@notes.md
#
# Empty notes give GitHub's generated notes, as in CD.
name: Release notes

on:
workflow_dispatch:
inputs:
tag:
description: The draft release's tag, such as v0.2.0
required: true
type: string
notes:
description: The release notes, in Markdown
required: false
type: string

permissions:
contents: read

jobs:
notes:
name: Release notes
runs-on: ubuntu-24.04
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ inputs.tag }}
NOTES: ${{ inputs.notes }}
steps:
- uses: actions/checkout@v5
with:
sparse-checkout: |
.github/scripts
packaging/update-ed25519.pub
sparse-checkout-cone-mode: false
- name: Update the draft
env:
UPDATE_SIGNING_KEY: ${{ secrets.UPDATE_SIGNING_KEY }}
NOTES_ONLY: 1
run: |
version="${TAG#v}"
gh release download "$TAG" --dir dist \
--pattern "stemd-$version-macos-arm64.dmg" --pattern SHA256SUMS.txt
printf '%s\n' "$NOTES" | tr -d '\r' > "$RUNNER_TEMP/notes.md"
.github/scripts/release-draft.sh "$version" "$TAG" dist "$RUNNER_TEMP/notes.md"
echo "Draft: $(gh release view "$TAG" --json url -q .url)" >> "$GITHUB_STEP_SUMMARY"
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ members = [
]

[workspace.package]
version = "0.1.4"
version = "0.2.0"
edition = "2024"
# 1.89 for `File::try_lock`, which keeps a second server off a cache in use.
rust-version = "1.89"
Expand Down
Loading
Loading