Files
shell-rs/.github/workflows/release.yml
okooo 552d55777d release workflow 暂时只打 Windows 包
macOS 和 Linux 的 job 关掉,另行通知后再打开。
2026-10-11 09:59:39 +08:00

352 lines
15 KiB
YAML

# A release: push a tag `v<version>` matching Cargo.toml's version.
#
# verify the tag, the CHANGELOG section, the update keys
# macos per-architecture apps: signed, notarized, stapled; update zip and DMG
# windows per-user Inno Setup installer (unsigned for now)
# linux AppImage
# publish (environment `release`, approved by hand) uploads to R2 behind
# dl.shellrs.com, creates the GitHub release, and only then
# replaces the channel's manifest, which is what clients follow;
# the manifest lists the GitHub release as each package's
# second address; last, it has Cloudflare rebuild shellrs.com,
# whose download page and changelog read that manifest
#
# A tag with a pre-release (v0.3.0-beta.1) goes to the beta channel only. A
# stable release also becomes the beta channel's, unless beta is ahead.
#
# Running it by hand (workflow_dispatch) builds and packages from the
# current commit without publishing, to try the pipeline.
#
# Secrets, see docs/release.md:
# repository: APPLE_CERTIFICATE_P12 (base64), APPLE_CERTIFICATE_PASSWORD,
# APPLE_SIGNING_IDENTITY, APPLE_TEAM_ID, APPLE_API_KEY_P8
# (base64), APPLE_API_KEY_ID, APPLE_API_ISSUER
# environment `release`: MINISIGN_SECRET_KEY, MINISIGN_PASSWORD,
# R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY,
# R2_BUCKET, CF_ZONE_ID, CF_API_TOKEN, SITE_DEPLOY_HOOK
name: release
on:
push:
tags: ["v*"]
workflow_dispatch:
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
jobs:
verify:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
channel: ${{ steps.version.outputs.channel }}
steps:
- uses: actions/checkout@v4
- id: version
name: Version and channel
run: |
version=$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2)
if [ "$GITHUB_REF_TYPE" = tag ] && [ "${GITHUB_REF_NAME#v}" != "$version" ]; then
echo "::error::tag $GITHUB_REF_NAME does not match Cargo.toml version $version"
exit 1
fi
case "$version" in *-*) channel=beta ;; *) channel=stable ;; esac
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "channel=$channel" >> "$GITHUB_OUTPUT"
- name: Release notes from CHANGELOG.md
run: |
version=${{ steps.version.outputs.version }}
awk -v heading="## [$version]" '
index($0, heading) == 1 { found = 1; next }
found && /^## \[/ { exit }
found { print }
' CHANGELOG.md | sed -e '/./,$!d' > notes.md
if [ ! -s notes.md ]; then
echo "::error::CHANGELOG.md has no section ## [$version]"
exit 1
fi
cat notes.md
- name: The build knows the update keys
run: |
if grep -q 'pub const TRUSTED_KEYS: &\[&str\] = &\[\];' src/update/build_info.rs; then
echo "::error::src/update/build_info.rs has no TRUSTED_KEYS: a release would reject every update"
exit 1
fi
- uses: actions/upload-artifact@v4
with:
name: notes
path: notes.md
macos:
needs: verify
# 暂时只打 Windows 包,另行通知后再打开。
if: false
runs-on: macos-14
strategy:
fail-fast: false
matrix:
arch: [aarch64, x86_64]
timeout-minutes: 120
env:
ARCH: ${{ matrix.arch }}
VERSION: ${{ needs.verify.outputs.version }}
SHELLRS_UPDATE_CHANNEL: ${{ needs.verify.outputs.channel }}
SHELLRS_COMMIT: ${{ github.sha }}
SHELLRS_APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
steps:
- uses: actions/checkout@v4
- run: rustup toolchain install
- run: rustup target add "$ARCH-apple-darwin"
- uses: Swatinem/rust-cache@v2
with:
key: macos-${{ matrix.arch }}
- name: Build the architecture-specific app
run: |
export SHELLRS_BUILD_DATE=$(date -u +%Y-%m-%d)
cargo build --release --locked --bin shellrs --target "$ARCH-apple-darwin"
mkdir -p dist build
lipo "target/$ARCH-apple-darwin/release/shellrs" -verify_arch "${ARCH/aarch64/arm64}"
packaging/macos/bundle.sh "target/$ARCH-apple-darwin/release/shellrs" "$VERSION" "$GITHUB_RUN_NUMBER" build
- name: Sign
env:
CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_P12 }}
CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
run: |
keychain="$RUNNER_TEMP/signing.keychain-db"
password=$(uuidgen)
echo "$CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/certificate.p12"
security create-keychain -p "$password" "$keychain"
security set-keychain-settings -lut 21600 "$keychain"
security unlock-keychain -p "$password" "$keychain"
security import "$RUNNER_TEMP/certificate.p12" -P "$CERTIFICATE_PASSWORD" \
-A -t cert -f pkcs12 -k "$keychain"
security set-key-partition-list -S apple-tool:,apple: -k "$password" "$keychain"
security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"')
codesign --force --timestamp --options runtime --sign "$IDENTITY" build/ShellRS.app
codesign --verify --deep --strict --verbose=2 build/ShellRS.app
- name: Notarize and staple
env:
API_KEY: ${{ secrets.APPLE_API_KEY_P8 }}
API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
run: |
echo "$API_KEY" | base64 --decode > "$RUNNER_TEMP/AuthKey.p8"
notarize() {
xcrun notarytool submit "$1" --key "$RUNNER_TEMP/AuthKey.p8" \
--key-id "$API_KEY_ID" --issuer "$API_ISSUER" \
--wait --output-format json > "$RUNNER_TEMP/notary.json"
cat "$RUNNER_TEMP/notary.json"
# --wait returning is not success: the status has to say so.
if [ "$(jq -r .status "$RUNNER_TEMP/notary.json")" != Accepted ]; then
xcrun notarytool log "$(jq -r .id "$RUNNER_TEMP/notary.json")" \
--key "$RUNNER_TEMP/AuthKey.p8" --key-id "$API_KEY_ID" --issuer "$API_ISSUER"
exit 1
fi
}
ditto -c -k --keepParent build/ShellRS.app "$RUNNER_TEMP/notarize.zip"
notarize "$RUNNER_TEMP/notarize.zip"
xcrun stapler staple build/ShellRS.app
# The update package: the stapled bundle, zipped the way the
# updater unzips it (ditto keeps the signature intact).
ditto -c -k --keepParent build/ShellRS.app "dist/ShellRS-$VERSION-macos-$ARCH.app.zip"
# The first-install disk image, with the Applications shortcut.
mkdir -p "$RUNNER_TEMP/dmg"
cp -R build/ShellRS.app "$RUNNER_TEMP/dmg/"
ln -s /Applications "$RUNNER_TEMP/dmg/Applications"
dmg="dist/ShellRS-$VERSION-macos-$ARCH.dmg"
# hdiutil's own estimate of the image size once came out too small
# for the app ("No space left on device"), so the size is given,
# with room to spare; the compressed image is about half the app.
# It also fails now and then with "Resource busy" on the runners.
df -h "$RUNNER_TEMP" .
size=$(( $(du -sm "$RUNNER_TEMP/dmg" | cut -f1) * 2 + 20 ))
for attempt in 1 2 3; do
hdiutil create -volname ShellRS -srcfolder "$RUNNER_TEMP/dmg" -ov \
-fs HFS+ -size "${size}m" -format UDZO "$dmg" && break
[ "$attempt" = 3 ] && exit 1
sleep 10
done
codesign --force --timestamp --sign "$IDENTITY" "$dmg"
notarize "$dmg"
xcrun stapler staple "$dmg"
spctl -a -vv -t install "$dmg"
- uses: actions/upload-artifact@v4
with:
name: macos-${{ matrix.arch }}
path: dist/*
windows:
needs: verify
runs-on: windows-2025
timeout-minutes: 120
env:
VERSION: ${{ needs.verify.outputs.version }}
SHELLRS_UPDATE_CHANNEL: ${{ needs.verify.outputs.channel }}
SHELLRS_COMMIT: ${{ github.sha }}
steps:
- uses: actions/checkout@v4
- run: rustup toolchain install
- uses: Swatinem/rust-cache@v2
- uses: ilammy/setup-nasm@v1
- name: Build
shell: bash
run: |
export SHELLRS_BUILD_DATE=$(date -u +%Y-%m-%d)
cargo build --release --locked --bins
- name: Installer
shell: pwsh
run: |
# windows-2025 ships Inno Setup 6.
$iscc = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
if (-not (Test-Path $iscc)) { choco install innosetup -y; }
& $iscc "/DAppVersion=$env:VERSION" "/DBuildDir=$env:GITHUB_WORKSPACE\target\release" `
"/O$env:GITHUB_WORKSPACE\dist" packaging\windows\shellrs.iss
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- uses: actions/upload-artifact@v4
with:
name: windows
path: dist/*
linux:
needs: verify
# 暂时只打 Windows 包,另行通知后再打开。
if: false
# Ubuntu 22.04's glibc (2.35) is the oldest the AppImage runs on.
runs-on: ubuntu-22.04
timeout-minutes: 120
env:
VERSION: ${{ needs.verify.outputs.version }}
SHELLRS_UPDATE_CHANNEL: ${{ needs.verify.outputs.channel }}
SHELLRS_COMMIT: ${{ github.sha }}
steps:
- uses: actions/checkout@v4
- name: Build dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake clang pkg-config libfontconfig-dev \
libfreetype-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev \
libx11-xcb-dev libxcb1-dev libvulkan-dev libdbus-1-dev libssl-dev
- run: rustup toolchain install
- uses: Swatinem/rust-cache@v2
- name: Build
run: |
export SHELLRS_BUILD_DATE=$(date -u +%Y-%m-%d)
cargo build --release --locked --bin shellrs
- name: AppImage
run: |
curl -fsSL -o "$RUNNER_TEMP/appimagetool" \
https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x "$RUNNER_TEMP/appimagetool"
# No FUSE on the runner: let the tool unpack itself.
export APPIMAGE_EXTRACT_AND_RUN=1
APPIMAGETOOL="$RUNNER_TEMP/appimagetool" \
packaging/linux/appimage.sh target/release/shellrs "$VERSION" dist
- uses: actions/upload-artifact@v4
with:
name: linux
path: dist/*
publish:
needs: [verify, macos, windows, linux]
if: github.ref_type == 'tag'
runs-on: ubuntu-latest
environment: release
env:
VERSION: ${{ needs.verify.outputs.version }}
CHANNEL: ${{ needs.verify.outputs.channel }}
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
BUCKET: ${{ secrets.R2_BUCKET }}
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Collect
run: |
mkdir -p dist
cp artifacts/macos-aarch64/* artifacts/macos-x86_64/* artifacts/windows/* artifacts/linux/* dist/
cp artifacts/notes/notes.md notes.md
ls -l dist
- name: Signing key
env:
KEY: ${{ secrets.MINISIGN_SECRET_KEY }}
run: |
sudo apt-get update && sudo apt-get install -y minisign
printf '%s\n' "$KEY" > "$RUNNER_TEMP/minisign.key"
echo "MINISIGN_KEY_FILE=$RUNNER_TEMP/minisign.key" >> "$GITHUB_ENV"
- name: Sign the packages
env:
MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }}
run: |
cd dist
for file in ShellRS-*; do
printf '%s\n' "$MINISIGN_PASSWORD" | minisign -S -s "$MINISIGN_KEY_FILE" \
-m "$file" -t "shellrs $VERSION $file"
done
sha256sum ShellRS-* > SHA256SUMS
- name: Upload the packages
run: |
aws s3 cp dist "s3://$BUCKET/releases/$VERSION/" --recursive \
--endpoint-url "$R2" --cache-control "public, max-age=31536000, immutable"
- name: GitHub release
run: |
prerelease=""
[ "$CHANNEL" = beta ] && prerelease="--prerelease"
gh release create "v$VERSION" dist/* --title "ShellRS $VERSION" \
--notes-file notes.md $prerelease
- name: Manifests, last
env:
MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }}
# The GitHub release just created: clients try it when
# dl.shellrs.com fails. The repository is public, so it needs no
# sign-in.
SHELLRS_MIRROR_BASE: ${{ github.server_url }}/${{ github.repository }}/releases/download/v${{ needs.verify.outputs.version }}
run: |
publish() {
packaging/make-manifest.sh "$VERSION" "$1" notes.md dist "$1.json"
aws s3 cp "$1.json" "s3://$BUCKET/update/v1/$1.json" --endpoint-url "$R2" \
--content-type application/json --cache-control "public, max-age=300"
echo "https://dl.shellrs.com/update/v1/$1.json" >> purge.txt
}
publish "$CHANNEL"
if [ "$CHANNEL" = stable ]; then
# Beta follows stable unless it is already ahead.
beta=$(curl -fsS https://dl.shellrs.com/update/v1/beta.json | jq -r .manifest | jq -r .version || true)
[ -n "$beta" ] || beta=0.0.0
if python3 - "$VERSION" "$beta" <<'EOF'
import sys
def key(version):
core, _, pre = version.partition("-")
parts = [int(part) for part in core.split(".")]
# A release sorts after its pre-releases.
return parts, (1,) if not pre else (0, pre)
sys.exit(0 if key(sys.argv[1]) > key(sys.argv[2]) else 1)
EOF
then
publish beta
fi
fi
- name: Purge the manifests from the CDN cache
env:
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
run: |
files=$(jq -R . purge.txt | jq -s '{files: .}')
curl -fsS -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/purge_cache" \
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
--data "$files"
# After the purge: the site's build reads the manifests from dl.shellrs.com.
- name: Rebuild shellrs.com
env:
SITE_DEPLOY_HOOK: ${{ secrets.SITE_DEPLOY_HOOK }}
run: curl -fsS -X POST "$SITE_DEPLOY_HOOK"