- 新增 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 测试
321 lines
13 KiB
YAML
321 lines
13 KiB
YAML
# 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"
|