chore(release): v1.1.0 (#211) #25
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*'] | |
| permissions: | |
| contents: write | |
| concurrency: | |
| # One release run per tag, and never cancel one mid-flight: a canceled job could | |
| # leave a half-populated draft that a later run would then publish. | |
| group: release-${{ github.ref_name }} | |
| cancel-in-progress: false | |
| env: | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| # Open a DRAFT release for the tag. The build jobs attach every asset to it, and the | |
| # `publish` job flips it live last — so a release is only published once all assets are | |
| # present. This is what makes the release immutable-safe: once a release is published its | |
| # tag and assets freeze, so nothing can be added afterward. A draft keeps the tag unlocked | |
| # until publish; a failed build simply leaves an unpublished draft the tag can be reused on. | |
| create-release: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Create draft release (idempotent) | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| # A published release is frozen under immutability — refuse to clobber assets | |
| # onto it (e.g. a manual re-run of an already-succeeded workflow). Only an | |
| # unpublished draft from a failed attempt is safe to reuse. | |
| if [ "$(gh release view "$TAG" --json isDraft --jq .isDraft)" != "true" ]; then | |
| echo "::error::release $TAG is already published — refusing to modify it. Cut a new tag for a new release." | |
| exit 1 | |
| fi | |
| echo "release $TAG already exists as a draft (re-run) — reusing it." | |
| else | |
| # A semver pre-release tag carries a hyphen (v1.0.0-rc1, v0.9.0-beta). Mark it | |
| # --prerelease so GitHub never surfaces it as the repo's "Latest" release; a final | |
| # tag (v1.0.0) has no hyphen and publishes as a normal release. | |
| PRERELEASE="" | |
| case "$TAG" in *-*) PRERELEASE="--prerelease" ;; esac | |
| gh release create "$TAG" --draft $PRERELEASE --generate-notes --title "$TAG" | |
| fi | |
| linux: | |
| needs: create-release | |
| name: linux artifacts (gnu + musl) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| id-token: write # OIDC token for build-provenance signing | |
| attestations: write # write the provenance to the attestation API | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Install pinned toolchain | |
| run: rustup show | |
| - name: Add musl target | |
| run: rustup target add x86_64-unknown-linux-musl | |
| - name: Install musl tools | |
| run: sudo apt-get update && sudo apt-get install -y musl-tools | |
| - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 | |
| with: | |
| workspaces: . | |
| - name: Build (gnu) | |
| run: cargo build --release -p glass-mcp --locked | |
| - name: Build (musl, static) | |
| run: cargo build --release -p glass-mcp --target x86_64-unknown-linux-musl --locked | |
| - name: Package | |
| run: | | |
| set -euo pipefail | |
| tag="${GITHUB_REF_NAME}" | |
| mkdir -p dist | |
| pkg() { # <built-binary> <readme> <suffix> | |
| local name="glass-mcp-${tag}-$3" dir | |
| dir="dist/$name" | |
| mkdir -p "$dir" | |
| install -m0755 "$1" "$dir/glass-mcp" | |
| strip "$dir/glass-mcp" | |
| cp "packaging/$2" "$dir/README.md" | |
| tar -C dist -czf "dist/$name.tar.gz" "$name" | |
| } | |
| pkg target/release/glass-mcp README-gnu.md x86_64-linux-gnu | |
| pkg target/x86_64-unknown-linux-musl/release/glass-mcp README-musl.md x86_64-linux-musl | |
| ( cd dist && for f in *.tar.gz; do sha256sum "$f" > "$f.sha256"; done ) | |
| - name: Attest build provenance | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 | |
| with: | |
| subject-path: 'dist/*.tar.gz' | |
| - name: Upload to draft release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG: ${{ github.ref_name }} | |
| run: gh release upload "$TAG" dist/*.tar.gz dist/*.tar.gz.sha256 --clobber | |
| windows: | |
| needs: create-release | |
| name: windows artifact | |
| # The shipped glass-mcp.exe statically links the MSVC CRT (.cargo/config.toml, | |
| # +crt-static). Cut Windows releases ONLY here (not on a dev box) for consistent build | |
| # provenance. Toolchain/CRT redistribution licensing has open questions to confirm | |
| # before commercial GA — see /LICENSING.md. | |
| runs-on: windows-latest | |
| permissions: | |
| contents: write | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Install pinned toolchain | |
| run: rustup show | |
| - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 | |
| with: | |
| workspaces: . | |
| - name: Build | |
| run: cargo build --release -p glass-mcp --locked | |
| # Windows Authenticode signing (Azure Trusted Signing). Until the Trusted Signing | |
| # account is provisioned and its secrets are set, this SKIPS signing (exit 0) and | |
| # still uploads the (unsigned) artifact — same as before. Secrets can't be read in a | |
| # job-level `if:`, so we probe one here and gate the sign step on its output. | |
| # Required secrets (all org-level, WINDOWS_TRUSTED_SIGNING_* prefix): | |
| # WINDOWS_TRUSTED_SIGNING_ENDPOINT, WINDOWS_TRUSTED_SIGNING_ACCOUNT, | |
| # WINDOWS_TRUSTED_SIGNING_CERT_PROFILE, WINDOWS_TRUSTED_SIGNING_AZURE_TENANT_ID, | |
| # WINDOWS_TRUSTED_SIGNING_AZURE_CLIENT_ID, WINDOWS_TRUSTED_SIGNING_AZURE_CLIENT_SECRET. | |
| # Provisioning runbook: internal. | |
| - name: Check signing credentials | |
| id: creds | |
| shell: pwsh | |
| env: | |
| SECRET: ${{ secrets.WINDOWS_TRUSTED_SIGNING_AZURE_CLIENT_SECRET }} | |
| run: | | |
| if ($env:SECRET) { | |
| "present=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append | |
| } else { | |
| "present=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append | |
| Write-Output "::notice::Windows signing secrets not configured — shipping unsigned artifact." | |
| } | |
| - name: Sign glass-mcp.exe (Authenticode) | |
| if: steps.creds.outputs.present == 'true' | |
| uses: Azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0 | |
| with: | |
| azure-tenant-id: ${{ secrets.WINDOWS_TRUSTED_SIGNING_AZURE_TENANT_ID }} | |
| azure-client-id: ${{ secrets.WINDOWS_TRUSTED_SIGNING_AZURE_CLIENT_ID }} | |
| azure-client-secret: ${{ secrets.WINDOWS_TRUSTED_SIGNING_AZURE_CLIENT_SECRET }} | |
| endpoint: ${{ secrets.WINDOWS_TRUSTED_SIGNING_ENDPOINT }} | |
| signing-account-name: ${{ secrets.WINDOWS_TRUSTED_SIGNING_ACCOUNT }} | |
| certificate-profile-name: ${{ secrets.WINDOWS_TRUSTED_SIGNING_CERT_PROFILE }} | |
| files-folder: target/release | |
| files-folder-filter: exe | |
| file-digest: SHA256 | |
| timestamp-rfc3161: http://timestamp.acs.microsoft.com | |
| timestamp-digest: SHA256 | |
| - name: Package | |
| shell: pwsh | |
| run: | | |
| $tag = $env:GITHUB_REF_NAME | |
| $name = "glass-mcp-$tag-x86_64-windows" | |
| New-Item -ItemType Directory -Force -Path "dist/$name" | Out-Null | |
| Copy-Item target/release/glass-mcp.exe "dist/$name/glass-mcp.exe" | |
| Copy-Item packaging/README-windows.md "dist/$name/README.md" | |
| Compress-Archive -Path "dist/$name/*" -DestinationPath "dist/$name.zip" -Force | |
| $hash = (Get-FileHash "dist/$name.zip" -Algorithm SHA256).Hash.ToLower() | |
| "$hash $name.zip" | Out-File "dist/$name.zip.sha256" -Encoding ascii | |
| - name: Attest build provenance | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 | |
| with: | |
| subject-path: 'dist/*.zip' | |
| - name: Upload to draft release | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| $files = (Get-ChildItem dist/*.zip, dist/*.zip.sha256).FullName | |
| gh release upload $env:GITHUB_REF_NAME $files --clobber | |
| macos: | |
| needs: create-release | |
| name: macos artifact (universal, notarized) | |
| runs-on: macos-latest | |
| permissions: | |
| contents: write | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| # The macOS release artifact needs Developer ID signing + notarization secrets. | |
| # Until the Apple Developer enrollment is approved and these are configured, this | |
| # job SKIPS every real step (exit 0) so tagging a release still publishes the | |
| # Linux/Windows artifacts. Secrets can't be read in a job-level `if:`, so we probe | |
| # one here and gate the rest on its output. | |
| - name: Check signing credentials | |
| id: creds | |
| env: | |
| CERT: ${{ secrets.MACOS_DEVELOPER_ID_CERT_P12 }} | |
| run: | | |
| if [ -n "$CERT" ]; then | |
| echo "present=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "present=false" >> "$GITHUB_OUTPUT" | |
| echo "::notice::macOS signing secrets not configured — skipping notarized macOS artifact (Apple Developer enrollment pending)." | |
| fi | |
| - name: Install pinned toolchain | |
| if: steps.creds.outputs.present == 'true' | |
| run: | | |
| rustup show | |
| rustup target add x86_64-apple-darwin | |
| - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 | |
| if: steps.creds.outputs.present == 'true' | |
| with: | |
| workspaces: . | |
| - name: Import Developer ID certificate | |
| if: steps.creds.outputs.present == 'true' | |
| env: | |
| CERT_P12: ${{ secrets.MACOS_DEVELOPER_ID_CERT_P12 }} | |
| CERT_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_CERT_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| keychain="$RUNNER_TEMP/build.keychain-db" | |
| kc_pw="$(openssl rand -base64 24)" | |
| echo "SIGN_KEYCHAIN=$keychain" >> "$GITHUB_ENV" | |
| security create-keychain -p "$kc_pw" "$keychain" | |
| security set-keychain-settings -lut 21600 "$keychain" | |
| security unlock-keychain -p "$kc_pw" "$keychain" | |
| echo "$CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12" | |
| security import "$RUNNER_TEMP/cert.p12" -k "$keychain" -P "$CERT_PASSWORD" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$kc_pw" "$keychain" >/dev/null | |
| # Prepend the build keychain to the search list so codesign resolves the identity. | |
| security list-keychains -d user -s "$keychain" $(security list-keychains -d user | sed 's/[",]//g') | |
| rm -f "$RUNNER_TEMP/cert.p12" | |
| - name: Build + sign universal app | |
| if: steps.creds.outputs.present == 'true' | |
| env: | |
| SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} | |
| run: | | |
| set -euo pipefail | |
| tag="${GITHUB_REF_NAME}" | |
| ./packaging/macos/build-app.sh --universal --timestamp \ | |
| --identity "$SIGN_IDENTITY" --keychain "$SIGN_KEYCHAIN" \ | |
| --version "${tag#v}" --build "${GITHUB_RUN_NUMBER}" | |
| - name: Notarize + staple | |
| if: steps.creds.outputs.present == 'true' | |
| env: | |
| NOTARY_KEY_P8: ${{ secrets.MACOS_NOTARY_API_KEY_P8 }} | |
| NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_API_KEY_ID }} | |
| NOTARY_ISSUER: ${{ secrets.MACOS_NOTARY_API_ISSUER_ID }} | |
| run: | | |
| set -euo pipefail | |
| echo "$NOTARY_KEY_P8" | base64 --decode > "$RUNNER_TEMP/AuthKey.p8" | |
| ./packaging/macos/notarize.sh \ | |
| --app target/macos-app/GlassMcp.app \ | |
| --key "$RUNNER_TEMP/AuthKey.p8" \ | |
| --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER" | |
| rm -f "$RUNNER_TEMP/AuthKey.p8" | |
| - name: Build + notarize .dmg | |
| if: steps.creds.outputs.present == 'true' | |
| env: | |
| SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} | |
| NOTARY_KEY_P8: ${{ secrets.MACOS_NOTARY_API_KEY_P8 }} | |
| NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_API_KEY_ID }} | |
| NOTARY_ISSUER: ${{ secrets.MACOS_NOTARY_API_ISSUER_ID }} | |
| run: | | |
| set -euo pipefail | |
| tag="${GITHUB_REF_NAME}" | |
| echo "$NOTARY_KEY_P8" | base64 --decode > "$RUNNER_TEMP/AuthKey.p8" | |
| ./packaging/macos/make-dmg.sh --app target/macos-app/GlassMcp.app --out dist --version "${tag}" | |
| ./packaging/macos/notarize.sh \ | |
| --dmg "dist/glass-mcp-${tag}-universal-apple-darwin.dmg" \ | |
| --identity "$SIGN_IDENTITY" --keychain "$SIGN_KEYCHAIN" \ | |
| --key "$RUNNER_TEMP/AuthKey.p8" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER" | |
| rm -f "$RUNNER_TEMP/AuthKey.p8" | |
| - name: Package | |
| if: steps.creds.outputs.present == 'true' | |
| run: | | |
| set -euo pipefail | |
| tag="${GITHUB_REF_NAME}" | |
| name="glass-mcp-${tag}-universal-apple-darwin" | |
| mkdir -p dist | |
| # ditto (not zip) preserves the bundle layout + signature. | |
| ditto -c -k --keepParent target/macos-app/GlassMcp.app "dist/${name}.zip" | |
| ( cd dist && for f in *.zip *.dmg; do shasum -a 256 "$f" > "$f.sha256"; done ) | |
| - name: Attest build provenance | |
| if: steps.creds.outputs.present == 'true' | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 | |
| with: | |
| subject-path: | | |
| dist/*.zip | |
| dist/*.dmg | |
| - name: Upload to draft release | |
| if: steps.creds.outputs.present == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG: ${{ github.ref_name }} | |
| run: gh release upload "$TAG" dist/*.zip dist/*.zip.sha256 dist/*.dmg dist/*.dmg.sha256 --clobber | |
| # Flip the draft live only after every build+upload job succeeded, so the published | |
| # release carries all its assets atomically. If any upload job failed, this job is skipped | |
| # and the draft stays unpublished (re-run the workflow to retry). The macOS skip-gate | |
| # composes cleanly: when its secrets are absent the macos job still *succeeds* (its real | |
| # steps are skipped), so publish proceeds with the Linux/Windows assets. | |
| publish: | |
| needs: [linux, windows, macos] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Publish the draft release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG: ${{ github.ref_name }} | |
| # Reaffirm the prerelease flag at publish (idempotent) so a hyphenated tag stays a | |
| # pre-release even if its draft was created before this flag existed. --latest=false on | |
| # a pre-release keeps it from taking the "Latest" badge; a final tag flips both live. | |
| run: | | |
| set -euo pipefail | |
| case "$TAG" in | |
| *-*) gh release edit "$TAG" --draft=false --prerelease --latest=false ;; | |
| *) gh release edit "$TAG" --draft=false --latest ;; | |
| esac |