From 460ec9ff2fb1d487cffba0f3ae4a795642d5154d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Thu, 16 Jul 2026 10:59:13 +0100 Subject: [PATCH] fix(release): validate updater signing from workflow revision (#1726) --- .../scripts/release-workflow-contract_test.py | 20 ++++++++ .github/workflows/release.yml | 8 ++- apps/cli/src/release-config.test.ts | 2 +- ...elease-backfill-and-desktop-diagnostics.md | 5 ++ docs/decisions/INDEX.md | 2 +- docs/desktop-tauri-signing.md | 2 + scripts/release-desktop.test.sh | 1 + scripts/release/updater-manifest.test.sh | 21 -------- scripts/release/updater-signing-ready.sh | 3 +- scripts/release/updater-signing-ready.test.sh | 51 +++++++++++++++++++ 10 files changed, 90 insertions(+), 25 deletions(-) create mode 100755 scripts/release/updater-signing-ready.test.sh diff --git a/.github/scripts/release-workflow-contract_test.py b/.github/scripts/release-workflow-contract_test.py index 1b86919930..12cc40b4e3 100644 --- a/.github/scripts/release-workflow-contract_test.py +++ b/.github/scripts/release-workflow-contract_test.py @@ -81,6 +81,26 @@ class ReleaseWorkflowContractTest(unittest.TestCase): self.assertIn("if: ${{ !inputs.dry_run", block) self.assertNotIn("inputs.backfill_tag == ''", block) + def test_updater_signing_validation_uses_workflow_control_revision(self) -> None: + build_desktop = job_block("build-desktop") + self.assertIn("ref: ${{ needs.prepare.outputs.ref }}", build_desktop) + + detect = step_block("Detect Tauri updater signing input") + self.assertIn("GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}", detect) + self.assertIn("gh api", detect) + self.assertIn("$RUNNER_TEMP/updater-signing-ready.sh", detect) + self.assertIn( + "scripts/release/updater-signing-ready.sh?ref=${{ github.workflow_sha }}", + detect, + ) + self.assertIn('if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]', detect) + self.assertLess( + detect.index('if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]'), + detect.index("gh api"), + ) + self.assertIn('bash "$helper"', detect) + self.assertNotIn("bash scripts/release/updater-signing-ready.sh", detect) + def test_macos_dmg_build_has_retry_timeout_and_diagnostics(self) -> None: build = step_block("Build Tauri desktop app") self.assertIn("timeout-minutes: 70", build) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7511cbc161..1b638b5249 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -772,6 +772,7 @@ jobs: - name: Detect Tauri updater signing input env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} run: | set -euo pipefail @@ -780,7 +781,12 @@ jobs: echo "::notice::Tauri updater signing key is absent; signed updater artifacts and latest.json will be skipped." exit 0 fi - if ! bash scripts/release/updater-signing-ready.sh "${{ matrix.platform }}"; then + helper="$RUNNER_TEMP/updater-signing-ready.sh" + gh api \ + --header "Accept: application/vnd.github.raw+json" \ + "repos/${{ github.repository }}/contents/scripts/release/updater-signing-ready.sh?ref=${{ github.workflow_sha }}" \ + > "$helper" + if ! bash "$helper" "${{ matrix.platform }}"; then echo "::error::Updater signing prerequisites failed for ${{ matrix.platform }}." >&2 exit 1 fi diff --git a/apps/cli/src/release-config.test.ts b/apps/cli/src/release-config.test.ts index 21a4e66823..39fd31e65a 100644 --- a/apps/cli/src/release-config.test.ts +++ b/apps/cli/src/release-config.test.ts @@ -365,7 +365,7 @@ describe("release desktop artifacts", () => { expect(signingDocs).toContain("distinct from macOS and Windows code-signing credentials"); expect(signingDocs).toContain("Installers remain available"); expect(signingDocs).toContain("`latest.json` is not"); - expect(workflow).toContain('scripts/release/updater-signing-ready.sh "${{ matrix.platform }}"'); + expect(workflow).toContain('bash "$helper" "${{ matrix.platform }}"'); expect(updaterSigningScript).not.toContain("MACOS_SIGNING_ENABLED"); expect(updaterSigningScript).not.toContain("WINDOWS_SIGNING_ENABLED"); expect(signingDocs).toContain("Missing OS credentials do not block Tauri-signed updater"); diff --git a/docs/decisions/0029-release-backfill-and-desktop-diagnostics.md b/docs/decisions/0029-release-backfill-and-desktop-diagnostics.md index 35e6079c7a..551b8ffda0 100644 --- a/docs/decisions/0029-release-backfill-and-desktop-diagnostics.md +++ b/docs/decisions/0029-release-backfill-and-desktop-diagnostics.md @@ -2,6 +2,7 @@ **Status:** accepted **Date:** 2026-07-01 +**Amended:** 2026-07-16 **Area:** infra, workflow ## Context @@ -14,6 +15,8 @@ The first desktop macOS failure mode we observed happened after Rust compilation Add a maintainer-only release backfill mode to `.github/workflows/release.yml`. The `backfill_tag` input accepts the current highest existing `vX.Y.Z` release tag, verifies that the tag exists, and verifies that all release-critical manifests at that tag match the version. It then uses the existing tag as the build and publish ref. In this mode the workflow skips version bumping, changelog generation, release PR creation, and tag creation, while still running the build, GitHub release, npm publish, and Homebrew update jobs. +Keep artifact and application inputs pinned to that immutable release tag, but source release control-plane helpers from `github.workflow_sha`. This allows a corrected workflow revision to recover a broken tag without changing the tag or silently rebuilding application source from main. + Add macOS desktop packaging guardrails to the Tauri build job. macOS DMG builds now run with a bounded timeout and a retry, and failed macOS desktop builds upload diagnostics that include disk state, `hdiutil info`, related packaging processes, bundle directory listings, and Tauri's generated `bundle_dmg.sh` when present. Keep desktop installer builds on native OS runners. Linux container images remain useful for Linux CI and e2e dependency stability, but they are not the mechanism for macOS DMG recovery. @@ -24,6 +27,8 @@ Release recovery becomes explicit and repeatable for tag-only or partially publi The release workflow has more branching logic, and maintainers must choose between normal release, desktop validation, dry run, and tag backfill modes intentionally. The workflow contract is covered by a small CI test so later YAML changes do not silently remove the recovery path or macOS diagnostics. +Backfills therefore combine immutable tagged application inputs with current workflow control logic. Control-plane helper changes must remain compatible with the tagged inputs they validate. + macOS package failures should now leave actionable artifacts instead of only the final Tauri command line. The retry may add time to a failing macOS job, but the step-level timeout caps total time. ## Alternatives Considered diff --git a/docs/decisions/INDEX.md b/docs/decisions/INDEX.md index 9025337f5c..90397f716f 100644 --- a/docs/decisions/INDEX.md +++ b/docs/decisions/INDEX.md @@ -34,7 +34,7 @@ Read individual ADRs for full context. Create new ones via `/record decision` or | 0026 | [Tauri desktop shell over native runtime](0026-tauri-desktop-shell.md) | accepted (amended 2026-07-15) | frontend, backend, cli, infra | 2026-06-23 | | 0027 | [Replayable schema migrations across SQLite and Postgres](0027-replayable-schema-migrations.md) | accepted | backend | 2026-06-24 | | 0028 | [Backend-owned task-create last-used preferences](0028-task-create-last-used-source-of-truth.md) | accepted (amended by 0041) | backend, frontend | 2026-06-29 | -| 0029 | [Release backfill and desktop diagnostics](0029-release-backfill-and-desktop-diagnostics.md) | accepted | infra, workflow | 2026-07-01 | +| 0029 | [Release backfill and desktop diagnostics](0029-release-backfill-and-desktop-diagnostics.md) | accepted (amended 2026-07-16) | infra, workflow | 2026-07-01 | | 0030 | [Workspace-scoped integration settings](0030-workspace-scoped-integration-settings.md) | accepted | backend, frontend | 2026-07-01 | | 0031 | [Office skill reference files](0031-office-skill-reference-files.md) | accepted | backend | 2026-07-06 | | 0032 | [Configurable worktree branch names](0032-configurable-worktree-branch-names.md) | accepted | backend, frontend | 2026-07-07 | diff --git a/docs/desktop-tauri-signing.md b/docs/desktop-tauri-signing.md index 78c555b794..45870badf5 100644 --- a/docs/desktop-tauri-signing.md +++ b/docs/desktop-tauri-signing.md @@ -6,6 +6,8 @@ Unsigned desktop artifacts may require manual OS security bypasses and must not Use `desktop_validation_only=true` for maintainer inspection builds. That mode uploads workflow artifacts but does not publish a GitHub release, npm packages, Homebrew updates, or public container tags. +Release backfills build application code and artifacts from the requested immutable tag. Updater signing-readiness validation is control-plane logic and runs from `github.workflow_sha`, so a workflow fix on main can recover an existing tag without moving it or substituting main-branch application source. + ## macOS Signing inputs: diff --git a/scripts/release-desktop.test.sh b/scripts/release-desktop.test.sh index a91a08dfcc..b894c60fcc 100644 --- a/scripts/release-desktop.test.sh +++ b/scripts/release-desktop.test.sh @@ -225,4 +225,5 @@ fi grep -q "Checksum verification failed" "$ERR_FILE" || fail "verify-desktop-assets did not explain checksum mismatch" pass "verify-desktop-assets rejects checksum mismatches" +bash "$ROOT_DIR/scripts/release/updater-signing-ready.test.sh" bash "$ROOT_DIR/scripts/release/updater-manifest.test.sh" diff --git a/scripts/release/updater-manifest.test.sh b/scripts/release/updater-manifest.test.sh index f181eafb33..8df521107d 100755 --- a/scripts/release/updater-manifest.test.sh +++ b/scripts/release/updater-manifest.test.sh @@ -3,7 +3,6 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" SCRIPT="$ROOT_DIR/scripts/release/updater-manifest.mjs" -SIGNING_READY_SCRIPT="$ROOT_DIR/scripts/release/updater-signing-ready.sh" TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT @@ -224,23 +223,3 @@ if generate_manifest "$partial_assets" --allow-unsigned >"$TMP_DIR/out" 2>"$TMP_ fi grep -q "Incomplete updater artifact set" "$TMP_DIR/err" || fail "partial set error was not actionable" pass "partial signed updater feeds fail closed" - -TAURI_SIGNING_PRIVATE_KEY=test-key MACOS_SIGNING_ENABLED=false \ - bash "$SIGNING_READY_SCRIPT" macos-arm64 >/dev/null -pass "macOS updater artifacts require only the updater key" - -TAURI_SIGNING_PRIVATE_KEY=test-key WINDOWS_SIGNING_ENABLED=false \ - bash "$SIGNING_READY_SCRIPT" windows-x64 >/dev/null -pass "Windows updater artifacts require only the updater key" - -TAURI_SIGNING_PRIVATE_KEY=test-key MACOS_SIGNING_ENABLED=false WINDOWS_SIGNING_ENABLED=false \ - bash "$SIGNING_READY_SCRIPT" linux-arm64 >/dev/null -pass "Linux updater artifacts require only the updater key" - -if env -u TAURI_SIGNING_PRIVATE_KEY bash "$SIGNING_READY_SCRIPT" macos-arm64 \ - >"$TMP_DIR/out" 2>"$TMP_DIR/err"; then - fail "updater artifacts should require the Tauri updater key on every platform" -fi -grep -q "require TAURI_SIGNING_PRIVATE_KEY" "$TMP_DIR/err" || \ - fail "missing updater key failure was not actionable" -pass "every updater artifact requires the Tauri updater key" diff --git a/scripts/release/updater-signing-ready.sh b/scripts/release/updater-signing-ready.sh index 613df329b4..3bfe4ce2c1 100755 --- a/scripts/release/updater-signing-ready.sh +++ b/scripts/release/updater-signing-ready.sh @@ -10,9 +10,10 @@ if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then fi case "$platform" in - macos-aarch64 | macos-x86_64 | windows-x86_64 | linux-x86_64 | linux-aarch64) ;; + macos-arm64 | macos-x64 | linux-x64 | linux-arm64 | windows-x64) ;; *) echo "Unsupported updater platform: $platform" >&2 + echo "Expected one of: macos-arm64, macos-x64, linux-x64, linux-arm64, windows-x64" >&2 exit 1 ;; esac diff --git a/scripts/release/updater-signing-ready.test.sh b/scripts/release/updater-signing-ready.test.sh new file mode 100755 index 0000000000..b87b8d30b8 --- /dev/null +++ b/scripts/release/updater-signing-ready.test.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SCRIPT="$ROOT_DIR/scripts/release/updater-signing-ready.sh" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +valid_platforms=(macos-arm64 macos-x64 linux-x64 linux-arm64 windows-x64) +for platform in "${valid_platforms[@]}"; do + TAURI_SIGNING_PRIVATE_KEY=test-key \ + MACOS_SIGNING_ENABLED=false \ + WINDOWS_SIGNING_ENABLED=false \ + bash "$SCRIPT" "$platform" >"$TMP_DIR/out" 2>"$TMP_DIR/err" || \ + fail "updater signing readiness rejected release platform $platform" + grep -q "complete for $platform" "$TMP_DIR/out" || \ + fail "updater signing readiness did not confirm $platform" +done + +unsupported_platforms=( + macos-aarch64 + macos-x86_64 + linux-x86_64 + linux-aarch64 + windows-x86_64 +) +misspelled_platforms=(linux-x65) +for platform in "${unsupported_platforms[@]}" "${misspelled_platforms[@]}"; do + if TAURI_SIGNING_PRIVATE_KEY=test-key bash "$SCRIPT" "$platform" \ + >"$TMP_DIR/out" 2>"$TMP_DIR/err"; then + fail "updater signing readiness accepted unsupported platform $platform" + fi + grep -q "Unsupported updater platform: $platform" "$TMP_DIR/err" || \ + fail "unsupported platform error did not identify $platform" + grep -q "macos-arm64, macos-x64, linux-x64, linux-arm64, windows-x64" \ + "$TMP_DIR/err" || fail "unsupported platform error did not list accepted values" +done + +if env -u TAURI_SIGNING_PRIVATE_KEY bash "$SCRIPT" linux-x64 \ + >"$TMP_DIR/out" 2>"$TMP_DIR/err"; then + fail "updater signing readiness accepted a missing updater key" +fi +grep -q "require TAURI_SIGNING_PRIVATE_KEY" "$TMP_DIR/err" || \ + fail "missing updater key error was not actionable" + +echo "PASS: updater signing readiness accepts the release matrix and only requires the updater key"