在线升级:后台检查、下载并在重启或退出时安装新版本,发布流水线与应用图标
- 新增 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 测试
This commit is contained in:
1 parent
e60275dfde
commit
f0690fe6b2
54 files changed
+6425
-43
No files matched your search
@@ -0,0 +1,320 @@
|
||||
# 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"
|
||||
@@ -1,6 +1,6 @@
|
||||
# The Windows build, which the Macs this is developed on cannot compile:
|
||||
# the whole crate through clippy, and the external CLI's tests, including
|
||||
# the named pipe between two processes.
|
||||
# the whole crate through clippy, the external CLI's tests, including the
|
||||
# named pipe between two processes, and the updater's.
|
||||
name: windows
|
||||
|
||||
on:
|
||||
@@ -24,4 +24,5 @@ jobs:
|
||||
- uses: ilammy/setup-nasm@v1
|
||||
- run: cargo clippy --all-targets -- -D warnings
|
||||
- run: "cargo test --lib cli::"
|
||||
- run: "cargo test --lib update::"
|
||||
- run: cargo test --test cli_bin
|
||||
Reference in new issue
Block a user