fix(release): validate updater signing from workflow revision (#1726)
This commit is contained in:
@@ -81,6 +81,26 @@ class ReleaseWorkflowContractTest(unittest.TestCase):
|
|||||||
self.assertIn("if: ${{ !inputs.dry_run", block)
|
self.assertIn("if: ${{ !inputs.dry_run", block)
|
||||||
self.assertNotIn("inputs.backfill_tag == ''", 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:
|
def test_macos_dmg_build_has_retry_timeout_and_diagnostics(self) -> None:
|
||||||
build = step_block("Build Tauri desktop app")
|
build = step_block("Build Tauri desktop app")
|
||||||
self.assertIn("timeout-minutes: 70", build)
|
self.assertIn("timeout-minutes: 70", build)
|
||||||
|
|||||||
@@ -772,6 +772,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Detect Tauri updater signing input
|
- name: Detect Tauri updater signing input
|
||||||
env:
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
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."
|
echo "::notice::Tauri updater signing key is absent; signed updater artifacts and latest.json will be skipped."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
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
|
echo "::error::Updater signing prerequisites failed for ${{ matrix.platform }}." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ describe("release desktop artifacts", () => {
|
|||||||
expect(signingDocs).toContain("distinct from macOS and Windows code-signing credentials");
|
expect(signingDocs).toContain("distinct from macOS and Windows code-signing credentials");
|
||||||
expect(signingDocs).toContain("Installers remain available");
|
expect(signingDocs).toContain("Installers remain available");
|
||||||
expect(signingDocs).toContain("`latest.json` is not");
|
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("MACOS_SIGNING_ENABLED");
|
||||||
expect(updaterSigningScript).not.toContain("WINDOWS_SIGNING_ENABLED");
|
expect(updaterSigningScript).not.toContain("WINDOWS_SIGNING_ENABLED");
|
||||||
expect(signingDocs).toContain("Missing OS credentials do not block Tauri-signed updater");
|
expect(signingDocs).toContain("Missing OS credentials do not block Tauri-signed updater");
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
**Status:** accepted
|
**Status:** accepted
|
||||||
**Date:** 2026-07-01
|
**Date:** 2026-07-01
|
||||||
|
**Amended:** 2026-07-16
|
||||||
**Area:** infra, workflow
|
**Area:** infra, workflow
|
||||||
|
|
||||||
## Context
|
## 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.
|
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.
|
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.
|
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.
|
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.
|
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
|
## Alternatives Considered
|
||||||
|
|||||||
@@ -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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 0032 | [Configurable worktree branch names](0032-configurable-worktree-branch-names.md) | accepted | backend, frontend | 2026-07-07 |
|
||||||
|
|||||||
@@ -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.
|
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
|
## macOS
|
||||||
|
|
||||||
Signing inputs:
|
Signing inputs:
|
||||||
|
|||||||
@@ -225,4 +225,5 @@ fi
|
|||||||
grep -q "Checksum verification failed" "$ERR_FILE" || fail "verify-desktop-assets did not explain checksum mismatch"
|
grep -q "Checksum verification failed" "$ERR_FILE" || fail "verify-desktop-assets did not explain checksum mismatch"
|
||||||
pass "verify-desktop-assets rejects checksum mismatches"
|
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"
|
bash "$ROOT_DIR/scripts/release/updater-manifest.test.sh"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ set -euo pipefail
|
|||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
SCRIPT="$ROOT_DIR/scripts/release/updater-manifest.mjs"
|
SCRIPT="$ROOT_DIR/scripts/release/updater-manifest.mjs"
|
||||||
SIGNING_READY_SCRIPT="$ROOT_DIR/scripts/release/updater-signing-ready.sh"
|
|
||||||
TMP_DIR="$(mktemp -d)"
|
TMP_DIR="$(mktemp -d)"
|
||||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
|
||||||
@@ -224,23 +223,3 @@ if generate_manifest "$partial_assets" --allow-unsigned >"$TMP_DIR/out" 2>"$TMP_
|
|||||||
fi
|
fi
|
||||||
grep -q "Incomplete updater artifact set" "$TMP_DIR/err" || fail "partial set error was not actionable"
|
grep -q "Incomplete updater artifact set" "$TMP_DIR/err" || fail "partial set error was not actionable"
|
||||||
pass "partial signed updater feeds fail closed"
|
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"
|
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
case "$platform" in
|
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 "Unsupported updater platform: $platform" >&2
|
||||||
|
echo "Expected one of: macos-arm64, macos-x64, linux-x64, linux-arm64, windows-x64" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
+51
@@ -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"
|
||||||
Reference in New Issue
Block a user