Files
shell-rs/.github/workflows/release.yml
Xuz f0690fe6b2 在线升级:后台检查、下载并在重启或退出时安装新版本,发布流水线与应用图标
- 新增 update 模块:从 dl.shellrs.com 取已签名的更新清单(minisign,签名绑定通道和版本),
  按大小和 SHA-256 校验安装包;下载可续传、可换镜像,走系统代理
- 按安装方式安装:macOS 整包交换并核对开发者签名,Windows 静默运行安装程序,Linux 覆盖 AppImage;
  开发构建和无法自更新的安装只提示前往下载页
- 设置新增「关于」:版本信息、检查更新、下载进度和「自动检查并下载更新」开关;
  新版本就绪后标题栏右上角提示,更新对话框写明重启会中断什么;退出时自动安装,新版本启动后提示已更新
- 数据库拒绝打开更新版本建的库,迁移已有的库之前先备份成 shellrs.db.v<旧版本>.bak
- 新增 release workflow 和 packaging 脚本:universal macOS 包签名公证、Inno Setup 安装程序、
  AppImage、上传 R2、生成并签名更新清单;新增 CHANGELOG.md
- 应用图标放在 assets/logo;开发模式下 macOS Dock 显示 logo,Windows exe 嵌入图标
- 修正 AppImage 里 shellrs 命令链接到临时挂载点;启动时刷新已安装但过期的 Agent skill
- 新增清单、验签、下载、各平台安装器和界面的单元测试与 UI 测试
2026-10-01 12:33:01 +08:00

321 lines
13 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# A release: push a tag `v<version>` matching Cargo.toml's version.
#
# verify the tag, the CHANGELOG section, the update keys
# macos universal app: 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
#
# 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 README.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
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
runs-on: macos-14
timeout-minutes: 120
env:
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 aarch64-apple-darwin x86_64-apple-darwin
- uses: Swatinem/rust-cache@v2
- name: Build both halves and join them
run: |
export SHELLRS_BUILD_DATE=$(date -u +%Y-%m-%d)
cargo build --release --locked --bin shellrs --target aarch64-apple-darwin
cargo build --release --locked --bin shellrs --target x86_64-apple-darwin
mkdir -p dist build
lipo -create -output build/shellrs \
target/aarch64-apple-darwin/release/shellrs \
target/x86_64-apple-darwin/release/shellrs
packaging/macos/bundle.sh build/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-universal.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-universal.dmg"
hdiutil create -volname ShellRS -srcfolder "$RUNNER_TEMP/dmg" -ov -format UDZO "$dmg"
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
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
# 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/* 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 }}
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"