From ed605ba3fe086d3007acfc197bdec9d10afe0ec8 Mon Sep 17 00:00:00 2001 From: Reinhold <310554180+reinhold-ph@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:36:00 +0000 Subject: [PATCH 1/2] infra: make thin v6 candidate builds immutable --- .github/workflows/ci.yml | 2 + .github/workflows/thin-v6-macos-arm64.yml | 145 +++++++++++++++++--- scripts/test-thin-v6-workflow-efficiency.sh | 93 +++++++++++++ 3 files changed, 221 insertions(+), 19 deletions(-) create mode 100755 scripts/test-thin-v6-workflow-efficiency.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0a81d86ae..325db88abf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,6 +78,8 @@ jobs: run: scripts/test-release-ref-contract.sh - name: Desktop release candidate contract run: scripts/test-desktop-release-candidate.sh + - name: Thin v6 workflow efficiency contract + run: scripts/test-thin-v6-workflow-efficiency.sh - name: Mobile release contract run: | scripts/test-mobile-release-contract.sh diff --git a/.github/workflows/thin-v6-macos-arm64.yml b/.github/workflows/thin-v6-macos-arm64.yml index f15af9b6cb..84f3127577 100644 --- a/.github/workflows/thin-v6-macos-arm64.yml +++ b/.github/workflows/thin-v6-macos-arm64.yml @@ -1,27 +1,96 @@ name: Thin v6 macOS ARM64 candidate on: - push: - branches: - - feat/thin-v6-runtime-plan workflow_dispatch: + inputs: + source_sha: + description: Full 40-character commit SHA whose source CI already passed + required: true + type: string permissions: + actions: read contents: read concurrency: - group: thin-v6-macos-arm64-${{ github.ref }} + group: thin-v6-macos-arm64-${{ inputs.source_sha }} cancel-in-progress: false jobs: + verify_source: + name: Verify immutable source and CI + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + source_sha: ${{ steps.source.outputs.source_sha }} + steps: + - name: Require a full commit SHA + env: + SOURCE_SHA: ${{ inputs.source_sha }} + run: | + set -euo pipefail + [[ "$SOURCE_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || { + echo "::error::source_sha must be a full 40-character commit SHA" + exit 1 + } + + - name: Check out requested source + uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3 + with: + ref: ${{ inputs.source_sha }} + fetch-depth: 1 + persist-credentials: false + + - name: Resolve immutable source SHA + id: source + env: + SOURCE_SHA: ${{ inputs.source_sha }} + run: | + set -euo pipefail + [[ "$SOURCE_SHA" =~ ^[0-9a-fA-F]{40}$ ]] + resolved=$(git rev-parse 'HEAD^{commit}') + [[ "${resolved,,}" == "${SOURCE_SHA,,}" ]] || { + echo "::error::checkout resolved to $resolved instead of $SOURCE_SHA" + exit 1 + } + echo "source_sha=$resolved" >> "$GITHUB_OUTPUT" + + - name: Require successful source CI for exact SHA + env: + GH_TOKEN: ${{ github.token }} + SOURCE_SHA: ${{ steps.source.outputs.source_sha }} + run: | + set -euo pipefail + gh api --method GET \ + "repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs" \ + -f head_sha="$SOURCE_SHA" \ + -f status=completed \ + -f per_page=100 > "$RUNNER_TEMP/ci-runs.json" + python3 - "$RUNNER_TEMP/ci-runs.json" <<'PY' + import json, os, pathlib, sys + + source_sha = os.environ["SOURCE_SHA"] + runs = json.loads(pathlib.Path(sys.argv[1]).read_text())["workflow_runs"] + matches = [ + run for run in runs + if run.get("head_sha") == source_sha and run.get("conclusion") == "success" + ] + if not matches: + raise SystemExit(f"source CI has not passed for exact SHA {source_sha}") + selected = max(matches, key=lambda run: run["run_number"]) + print(f"verified CI run {selected['html_url']} for {source_sha}") + PY + build: name: Build isolated thin v6 Buzz.app + needs: verify_source if: github.repository == 'Peakhunter/buzz' runs-on: macos-26 timeout-minutes: 90 env: TARGET: aarch64-apple-darwin CANDIDATE_ID: thin-v6 + SOURCE_SHA: ${{ needs.verify_source.outputs.source_sha }} MACOSX_DEPLOYMENT_TARGET: "10.15" CMAKE_OSX_DEPLOYMENT_TARGET: "10.15" CMAKE_POLICY_VERSION_MINIMUM: "3.5" @@ -37,7 +106,7 @@ jobs: - name: Check out exact candidate source uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3 with: - ref: ${{ github.sha }} + ref: ${{ needs.verify_source.outputs.source_sha }} fetch-depth: 1 persist-credentials: false @@ -65,7 +134,7 @@ jobs: id: native-toolchain run: echo "id=$(scripts/desktop-native-toolchain-id.sh macos)" >> "$GITHUB_OUTPUT" - - name: Compute exact release Cargo cache key + - name: Compute compatible release Cargo cache base id: cargo-key env: NATIVE_TOOLCHAIN_ID: ${{ steps.native-toolchain.outputs.id }} @@ -76,19 +145,30 @@ jobs: --target "$TARGET" \ --features default \ --native-inputs "$NATIVE_TOOLCHAIN_ID") - echo "key=thin-v6-$key" >> "$GITHUB_OUTPUT" + echo "base=thin-v6-$key" >> "$GITHUB_OUTPUT" - - name: Restore exact Cargo cache - id: cargo-restore + - name: Restore Cargo dependency cache + id: cargo-deps-restore uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: | ~/.cargo/registry ~/.cargo/git + key: thin-v6-cargo-deps-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Cargo.lock', 'desktop/src-tauri/Cargo.lock', 'rust-toolchain.toml') }} + restore-keys: | + thin-v6-cargo-deps-${{ runner.os }}-${{ runner.arch }}- + + - name: Restore source-sensitive Cargo target cache + id: cargo-target-restore + uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: | target desktop/src-tauri/target !desktop/src-tauri/target/**/release/bundle - key: ${{ steps.cargo-key.outputs.key }} + key: ${{ steps.cargo-key.outputs.base }}-${{ needs.verify_source.outputs.source_sha }} + restore-keys: | + ${{ steps.cargo-key.outputs.base }}- - name: Generate isolated non-updating candidate config env: @@ -186,7 +266,7 @@ jobs: set -euo pipefail OUT="$RUNNER_TEMP/thin-v6-artifact" mkdir -p "$OUT" - ZIP="$OUT/Buzz-v6-candidate-arm64-${GITHUB_SHA}.zip" + ZIP="$OUT/Buzz-v6-candidate-arm64-${SOURCE_SHA}.zip" ditto -c -k --keepParent "$APP_PATH" "$ZIP" export OUT ZIP python3 - <<'PY' @@ -220,7 +300,7 @@ jobs: "generated_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(), "source": { "repository": os.environ["GITHUB_REPOSITORY"], - "commit": os.environ["GITHUB_SHA"], + "commit": os.environ["SOURCE_SHA"], "canonical_base": os.environ["CANONICAL_BASE_SHA"], "adr_anchor": os.environ["ADR_ANCHOR_SHA"], "ref": os.environ["GITHUB_REF"], @@ -279,26 +359,53 @@ jobs: - name: Upload one-day verification artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: thin-v6-macos-arm64-${{ github.sha }} + name: thin-v6-macos-arm64-${{ needs.verify_source.outputs.source_sha }} path: ${{ runner.temp }}/thin-v6-artifact/ if-no-files-found: error retention-days: 1 compression-level: 0 - - name: Save exact Cargo cache - if: always() && steps.cargo-restore.outputs.cache-hit != 'true' + - name: Publish cache telemetry + if: ${{ !cancelled() }} + env: + PNPM_HIT: ${{ steps.pnpm-restore.outputs.cache-hit }} + PNPM_RESTORED_KEY: ${{ steps.pnpm-restore.outputs.cache-primary-key }} + CARGO_DEPS_HIT: ${{ steps.cargo-deps-restore.outputs.cache-hit }} + CARGO_DEPS_RESTORED_KEY: ${{ steps.cargo-deps-restore.outputs.cache-primary-key }} + CARGO_TARGET_HIT: ${{ steps.cargo-target-restore.outputs.cache-hit }} + CARGO_TARGET_RESTORED_KEY: ${{ steps.cargo-target-restore.outputs.cache-primary-key }} + CARGO_TARGET_EXACT_KEY: ${{ steps.cargo-key.outputs.base }}-${{ needs.verify_source.outputs.source_sha }} + run: | + { + echo "## Thin v6 candidate cache telemetry" + echo "- Source SHA: \`$SOURCE_SHA\`" + echo "- pnpm exact hit: \`${PNPM_HIT:-false}\` (restored: \`${PNPM_RESTORED_KEY:-none}\`)" + echo "- Cargo dependencies exact hit: \`${CARGO_DEPS_HIT:-false}\` (restored: \`${CARGO_DEPS_RESTORED_KEY:-none}\`)" + echo "- Cargo target exact key: \`$CARGO_TARGET_EXACT_KEY\`" + echo "- Cargo target exact hit: \`${CARGO_TARGET_HIT:-false}\` (restored: \`${CARGO_TARGET_RESTORED_KEY:-none}\`)" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Save source-sensitive Cargo target cache + if: success() && steps.cargo-target-restore.outputs.cache-hit != 'true' uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: | - ~/.cargo/registry - ~/.cargo/git target desktop/src-tauri/target !desktop/src-tauri/target/**/release/bundle - key: ${{ steps.cargo-key.outputs.key }} + key: ${{ steps.cargo-key.outputs.base }}-${{ needs.verify_source.outputs.source_sha }} + + - name: Save Cargo dependency cache + if: success() && steps.cargo-deps-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: thin-v6-cargo-deps-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Cargo.lock', 'desktop/src-tauri/Cargo.lock', 'rust-toolchain.toml') }} - name: Save pnpm store cache - if: always() && steps.pnpm-restore.outputs.cache-hit != 'true' + if: success() && steps.pnpm-restore.outputs.cache-hit != 'true' uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ${{ steps.pnpm-cache.outputs.store }} diff --git a/scripts/test-thin-v6-workflow-efficiency.sh b/scripts/test-thin-v6-workflow-efficiency.sh new file mode 100755 index 0000000000..558964a9b0 --- /dev/null +++ b/scripts/test-thin-v6-workflow-efficiency.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +workflow="$repo_root/.github/workflows/thin-v6-macos-arm64.yml" + +python3 - "$workflow" <<'PY' +from __future__ import annotations + +import pathlib +import re +import sys + +path = pathlib.Path(sys.argv[1]) +text = path.read_text(encoding="utf-8") +ci_text = (path.parent / "ci.yml").read_text(encoding="utf-8") +if "scripts/test-thin-v6-workflow-efficiency.sh" not in ci_text: + raise SystemExit("source CI must run the thin-v6 workflow efficiency contract") + + +def require(pattern: str, message: str, *, flags: int = 0) -> None: + if re.search(pattern, text, flags) is None: + raise SystemExit(message) + + +def reject(pattern: str, message: str, *, flags: int = 0) -> None: + if re.search(pattern, text, flags) is not None: + raise SystemExit(message) + + +on_match = re.search(r"(?ms)^on:\n(?P.*?)(?=^[a-zA-Z][^\n]*:\n)", text) +if on_match is None: + raise SystemExit("workflow trigger block missing") +on_body = on_match.group("body") +if re.search(r"(?m)^ push:", on_body): + raise SystemExit("candidate packaging must not run on push") +if not re.search(r"(?m)^ workflow_dispatch:", on_body): + raise SystemExit("candidate packaging must be manually dispatched") +if not re.search(r"(?ms)^ workflow_dispatch:\n.*?^ source_sha:\n.*?^ required: true$", on_body): + raise SystemExit("workflow_dispatch must require source_sha") + +require( + r"(?m)^ group: thin-v6-macos-arm64-\$\{\{ inputs\.source_sha \}\}$", + "concurrency must deduplicate the requested source SHA", +) +require( + r"(?ms)^ verify_source:\n.*?^[ ]{6}- name: Resolve immutable source SHA\n.*?\[\[ \"\$SOURCE_SHA\" =~ \^\[0-9a-fA-F\]\{40\}\$ \]\]", + "a preflight job must reject non-full source identities", +) +require( + r"(?ms)^ verify_source:\n.*?actions/workflows/ci\.yml/runs.*?head_sha", + "preflight must verify source CI for the exact SHA", +) +require( + r"(?ms)^ build:\n.*?^ needs: verify_source$", + "the costly build must depend on source verification", +) +require( + r"(?ms)^ - name: Check out exact candidate source\n.*?^ ref: \$\{\{ needs\.verify_source\.outputs\.source_sha \}\}$", + "the build must check out the verified immutable SHA", +) +reject( + r"\$\{\{ github\.sha \}\}", + "github.sha must not substitute for the explicitly requested source SHA", +) + +require( + r"(?ms)^ - name: Restore Cargo dependency cache\n.*?^ path: \|\n ~/\.cargo/registry\n ~/\.cargo/git\n", + "Cargo downloads must be cached separately from compiler outputs", +) +require( + r"(?ms)^ - name: Restore source-sensitive Cargo target cache\n.*?^ key: .*needs\.verify_source\.outputs\.source_sha.*?^ restore-keys: \|", + "compiler outputs need an exact source key plus compatible restore prefix", +) +require( + r"(?m)^ if: success\(\) && steps\.cargo-target-restore\.outputs\.cache-hit != 'true'$", + "Cargo target cache may be saved only after a successful build", +) +reject( + r"(?m)^ if: always\(\).*cache-hit", + "failed or cancelled runs must not save caches", +) +require( + r"(?m)^ - name: Publish cache telemetry$", + "candidate workflow must expose cache hit/miss telemetry", +) +require( + r"GITHUB_STEP_SUMMARY", + "cache telemetry must be visible in the workflow summary", +) + +print("thin-v6 workflow efficiency contract passed") +PY From b6b7b63a33a6a778307d969dae15e963aae73310 Mon Sep 17 00:00:00 2001 From: Reinhold <310554180+reinhold-ph@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:46:54 +0000 Subject: [PATCH 2/2] ci: isolate thin v6 workflow contract --- .github/workflows/ci.yml | 2 -- .../workflows/thin-v6-workflow-contract.yml | 22 +++++++++++++++++++ scripts/test-thin-v6-workflow-efficiency.sh | 16 +++++++++++--- 3 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/thin-v6-workflow-contract.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 325db88abf..c0a81d86ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,8 +78,6 @@ jobs: run: scripts/test-release-ref-contract.sh - name: Desktop release candidate contract run: scripts/test-desktop-release-candidate.sh - - name: Thin v6 workflow efficiency contract - run: scripts/test-thin-v6-workflow-efficiency.sh - name: Mobile release contract run: | scripts/test-mobile-release-contract.sh diff --git a/.github/workflows/thin-v6-workflow-contract.yml b/.github/workflows/thin-v6-workflow-contract.yml new file mode 100644 index 0000000000..220bc5b89d --- /dev/null +++ b/.github/workflows/thin-v6-workflow-contract.yml @@ -0,0 +1,22 @@ +name: Thin v6 workflow contract + +on: + pull_request: + paths: + - .github/workflows/thin-v6-macos-arm64.yml + - .github/workflows/thin-v6-workflow-contract.yml + - scripts/test-thin-v6-workflow-efficiency.sh + +permissions: + contents: read + +jobs: + contract: + name: Thin v6 workflow efficiency contract + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - run: scripts/test-thin-v6-workflow-efficiency.sh diff --git a/scripts/test-thin-v6-workflow-efficiency.sh b/scripts/test-thin-v6-workflow-efficiency.sh index 558964a9b0..2d8471c502 100755 --- a/scripts/test-thin-v6-workflow-efficiency.sh +++ b/scripts/test-thin-v6-workflow-efficiency.sh @@ -13,9 +13,19 @@ import sys path = pathlib.Path(sys.argv[1]) text = path.read_text(encoding="utf-8") -ci_text = (path.parent / "ci.yml").read_text(encoding="utf-8") -if "scripts/test-thin-v6-workflow-efficiency.sh" not in ci_text: - raise SystemExit("source CI must run the thin-v6 workflow efficiency contract") +contract_path = path.parent / "thin-v6-workflow-contract.yml" +if not contract_path.exists(): + raise SystemExit("path-scoped thin-v6 workflow contract is missing") +contract_text = contract_path.read_text(encoding="utf-8") +if "scripts/test-thin-v6-workflow-efficiency.sh" not in contract_text: + raise SystemExit("path-scoped CI must run the thin-v6 workflow efficiency contract") +for changed_path in ( + ".github/workflows/thin-v6-macos-arm64.yml", + ".github/workflows/thin-v6-workflow-contract.yml", + "scripts/test-thin-v6-workflow-efficiency.sh", +): + if changed_path not in contract_text: + raise SystemExit(f"path-scoped CI trigger is missing {changed_path}") def require(pattern: str, message: str, *, flags: int = 0) -> None: