release #7
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: ['v*'] | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| jobs: | |
| release-desktop: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: macos-latest | |
| args: '--target aarch64-apple-darwin' | |
| rust-target: aarch64-apple-darwin | |
| - platform: macos-latest | |
| args: '--target x86_64-apple-darwin' | |
| rust-target: x86_64-apple-darwin | |
| - platform: ubuntu-24.04 | |
| args: '' | |
| rust-target: '' | |
| - platform: windows-latest | |
| args: '' | |
| rust-target: '' | |
| - platform: windows-latest | |
| args: '--target aarch64-pc-windows-msvc' | |
| rust-target: aarch64-pc-windows-msvc | |
| runs-on: ${{ matrix.platform }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| version: 10.33.0 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.rust-target }} | |
| - name: Cache cargo registry | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| packages/app/src-tauri/target | |
| key: ${{ matrix.platform }}-${{ matrix.rust-target || 'default' }}-cargo-${{ hashFiles('packages/app/src-tauri/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ matrix.platform }}-${{ matrix.rust-target || 'default' }}-cargo- | |
| ${{ matrix.platform }}-cargo- | |
| - name: Install Linux deps | |
| if: startsWith(matrix.platform, 'ubuntu-') | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libpipewire-0.3-dev patchelf | |
| - run: pnpm install --frozen-lockfile | |
| - name: Extract release notes | |
| id: notes | |
| shell: bash | |
| run: | | |
| if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| else | |
| VERSION="$(node -p "require('./package.json').version")" | |
| fi | |
| BODY=$(awk -v v="$VERSION" ' | |
| $1 == "##" && $2 == v { capture=1; next } | |
| capture && $1 == "##" { exit } | |
| capture { print } | |
| ' CHANGELOG.md) | |
| [ -z "$BODY" ] && BODY="Release $VERSION" | |
| { | |
| echo "body<<RELEASE_NOTES_EOF" | |
| echo "$BODY" | |
| echo "RELEASE_NOTES_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - uses: tauri-apps/tauri-action@v0 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| with: | |
| tagName: v__VERSION__ | |
| releaseName: 'sbox v__VERSION__' | |
| releaseBody: ${{ steps.notes.outputs.body }} | |
| releaseDraft: true | |
| prerelease: false | |
| includeUpdaterJson: false | |
| args: ${{ matrix.args }} | |
| publish-manifest: | |
| needs: release-desktop | |
| if: ${{ needs.release-desktop.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 24 | |
| - name: Resolve release tag | |
| id: release | |
| shell: bash | |
| run: | | |
| if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then | |
| TAG="${GITHUB_REF_NAME}" | |
| VERSION="${TAG#v}" | |
| else | |
| VERSION="$(node -p "require('./package.json').version")" | |
| TAG="v${VERSION}" | |
| fi | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Force release body from CHANGELOG | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| TAG: ${{ steps.release.outputs.tag }} | |
| RELEASE_VERSION: ${{ steps.release.outputs.version }} | |
| shell: bash | |
| run: | | |
| VERSION="$RELEASE_VERSION" | |
| BODY=$(awk -v v="$VERSION" ' | |
| $1 == "##" && $2 == v { capture=1; next } | |
| capture && $1 == "##" { exit } | |
| capture { print } | |
| ' CHANGELOG.md) | |
| [ -z "$BODY" ] && BODY="Release $VERSION" | |
| # tauri-action only sets the body when first creating the release; if an earlier | |
| # run already created the draft, its body is stuck on whatever was there. Force it. | |
| gh release edit "$TAG" -R "$REPO" --notes "$BODY" | |
| # 给桌面产物文件名加系统和架构标识(tauri-action 上传时不含)。 | |
| # 此 job needs: release-desktop,所有桌面 matrix 已完成,不会再有旧名文件上传。 | |
| # sed 不加 $ 行尾锚,.tar.gz / .zip / .sig 后缀会自动保留。 | |
| - name: Rename desktop assets with OS tag | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| TAG: ${{ steps.release.outputs.tag }} | |
| ASSET_VERSION: ${{ steps.release.outputs.version }} | |
| run: | | |
| gh api "/repos/$REPO/releases/tags/$TAG" --jq '.assets[] | "\(.id)\t\(.name)"' | | |
| while IFS=$'\t' read -r id name; do | |
| new=$(printf '%s' "$name" | sed -E \ | |
| -e "s#^sbox_[0-9][0-9.]*_aarch64\.dmg#sbox_${ASSET_VERSION}_macos_arm64.dmg#" \ | |
| -e "s#^sbox_[0-9][0-9.]*_x64\.dmg#sbox_${ASSET_VERSION}_macos_x64.dmg#" \ | |
| -e "s#^sbox_aarch64\.app\.tar\.gz#sbox_${ASSET_VERSION}_macos_arm64.app.tar.gz#" \ | |
| -e "s#^sbox_x64\.app\.tar\.gz#sbox_${ASSET_VERSION}_macos_x64.app.tar.gz#" \ | |
| -e "s#^sbox_[0-9][0-9.]*_amd64\.(AppImage|deb)#sbox_${ASSET_VERSION}_linux_x64.\1#" \ | |
| -e "s#^sbox_[0-9][0-9.]*_arm64\.(AppImage|deb)#sbox_${ASSET_VERSION}_linux_arm64.\1#" \ | |
| -e "s#^sbox_[0-9][0-9.]*_x64-setup\.exe#sbox_${ASSET_VERSION}_windows_x64.exe#" \ | |
| -e "s#^sbox_[0-9][0-9.]*_arm64-setup\.exe#sbox_${ASSET_VERSION}_windows_arm64.exe#") | |
| if [ "$new" != "$name" ]; then | |
| echo "rename: $name -> $new" | |
| gh api -X PATCH "/repos/$REPO/releases/assets/$id" -f name="$new" >/dev/null | |
| fi | |
| done | |
| - name: Generate updater manifest | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| TAG: ${{ steps.release.outputs.tag }} | |
| ASSET_VERSION: ${{ steps.release.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| gh api "/repos/$REPO/releases/tags/$TAG" > release.json | |
| rm -rf updater-signatures | |
| mkdir -p updater-signatures | |
| gh release download "$TAG" -R "$REPO" -p "*.sig" --dir updater-signatures --clobber | |
| node <<'NODE' | |
| const fs = require('node:fs'); | |
| const path = require('node:path'); | |
| const version = process.env.ASSET_VERSION; | |
| const release = JSON.parse(fs.readFileSync('release.json', 'utf8')); | |
| const assets = new Map(release.assets.map((asset) => [asset.name, asset])); | |
| const platforms = {}; | |
| const missing = []; | |
| function changelogNotes() { | |
| const changelog = fs.readFileSync('CHANGELOG.md', 'utf8').split(/\r?\n/); | |
| const lines = []; | |
| let capture = false; | |
| for (const line of changelog) { | |
| const parts = line.trim().split(/\s+/); | |
| if (parts[0] === '##' && parts[1] === version) { | |
| capture = true; | |
| continue; | |
| } | |
| if (capture && parts[0] === '##') break; | |
| if (capture) lines.push(line); | |
| } | |
| const body = lines.join('\n').trim(); | |
| return body || `Release ${version}`; | |
| } | |
| function signatureFor(assetName) { | |
| const sigName = `${assetName}.sig`; | |
| if (!assets.has(sigName)) { | |
| throw new Error(`missing signature asset: ${sigName}`); | |
| } | |
| const sigPath = path.join('updater-signatures', sigName); | |
| if (!fs.existsSync(sigPath)) { | |
| throw new Error(`signature asset was not downloaded: ${sigName}`); | |
| } | |
| return fs.readFileSync(sigPath, 'utf8'); | |
| } | |
| function add(keys, candidates) { | |
| const asset = candidates.map((name) => assets.get(name)).find(Boolean); | |
| if (!asset) { | |
| missing.push(candidates.join(' or ')); | |
| return; | |
| } | |
| const entry = { | |
| signature: signatureFor(asset.name), | |
| url: asset.browser_download_url, | |
| }; | |
| for (const key of keys) { | |
| platforms[key] = entry; | |
| } | |
| } | |
| add(['darwin-x86_64', 'darwin-x86_64-app'], [ | |
| `sbox_${version}_macos_x64.app.tar.gz`, | |
| ]); | |
| add(['darwin-aarch64', 'darwin-aarch64-app'], [ | |
| `sbox_${version}_macos_arm64.app.tar.gz`, | |
| ]); | |
| add(['windows-x86_64', 'windows-x86_64-nsis'], [ | |
| `sbox_${version}_windows_x64.exe`, | |
| `sbox_${version}_windows_x64.exe.zip`, | |
| `sbox_${version}_windows_x64.nsis.zip`, | |
| ]); | |
| add(['windows-aarch64', 'windows-aarch64-nsis'], [ | |
| `sbox_${version}_windows_arm64.exe`, | |
| `sbox_${version}_windows_arm64.exe.zip`, | |
| `sbox_${version}_windows_arm64.nsis.zip`, | |
| ]); | |
| add(['linux-x86_64', 'linux-x86_64-appimage'], [ | |
| `sbox_${version}_linux_x64.AppImage.tar.gz`, | |
| `sbox_${version}_linux_x64.AppImage`, | |
| ]); | |
| if (missing.length > 0) { | |
| throw new Error(`missing updater payload asset(s): ${missing.join(', ')}`); | |
| } | |
| fs.writeFileSync( | |
| 'latest.json', | |
| `${JSON.stringify({ | |
| version, | |
| notes: changelogNotes(), | |
| pub_date: new Date().toISOString(), | |
| platforms, | |
| }, null, 2)}\n`, | |
| ); | |
| NODE | |
| gh release view updater -R "$REPO" >/dev/null 2>&1 || \ | |
| gh release create updater -R "$REPO" \ | |
| --title "Updater Manifests" \ | |
| --notes "Auto-maintained by CI. Hosts the updater JSON for sbox." \ | |
| --latest=false | |
| gh release upload updater latest.json -R "$REPO" --clobber | |
| # 自动更新读的是固定 updater release 那份;版本 release 若残留旧 latest.json,则删除。 | |
| gh release delete-asset "$TAG" latest.json -R "$REPO" --yes || true |