352 lines
15 KiB
YAML
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"
|