From e0cb3cef93e07414409ec819a139bb0475557970 Mon Sep 17 00:00:00 2001 From: Vincent Koc <25068+vincentkoc@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:57:32 -0700 Subject: [PATCH] fix(release): gate evidence reuse on trusted lineage --- .github/workflows/full-release-validation.yml | 2 + .../find-reusable-release-validation.sh | 27 ++++++++++++ .../find-reusable-release-validation.test.ts | 42 ++++++++++++++++++- 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/.github/workflows/full-release-validation.yml b/.github/workflows/full-release-validation.yml index 0e719b329154..d7bf986c2f7b 100644 --- a/.github/workflows/full-release-validation.yml +++ b/.github/workflows/full-release-validation.yml @@ -276,6 +276,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} + WORKFLOW_REF: ${{ github.ref_name }} RELEASE_PROFILE: ${{ inputs.release_profile }} RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }} PROVIDER: ${{ inputs.provider }} @@ -309,6 +310,7 @@ jobs: bash workflow/scripts/github/find-reusable-release-validation.sh \ --target-sha "$TARGET_SHA" \ --workflow-sha "$GITHUB_SHA" \ + --workflow-ref "$WORKFLOW_REF" \ --release-profile "$RELEASE_PROFILE" \ --run-release-soak "$RUN_RELEASE_SOAK" \ --inputs-json "$inputs_json" \ diff --git a/scripts/github/find-reusable-release-validation.sh b/scripts/github/find-reusable-release-validation.sh index 543b43142458..53cacf241d6a 100755 --- a/scripts/github/find-reusable-release-validation.sh +++ b/scripts/github/find-reusable-release-validation.sh @@ -10,6 +10,7 @@ REPO="${GH_REPO:-}" WORKFLOW_FILE="full-release-validation.yml" TARGET_SHA="" VERIFIER_WORKFLOW_SHA="" +WORKFLOW_REF="" RELEASE_PROFILE="" RUN_RELEASE_SOAK="false" INPUTS_JSON="" @@ -24,6 +25,7 @@ VALIDATOR="${OPENCLAW_RELEASE_CI_SUMMARY_VALIDATOR:-${REPO_ROOT}/scripts/release usage() { cat >&2 <<'EOF' Usage: find-reusable-release-validation.sh --target-sha --workflow-sha \ + --workflow-ref \ --release-profile --inputs-json \ [--run-release-soak ] [--repo ] [--repo-dir ] \ [--workflow ] [--max-candidates ] [--github-output ] @@ -47,6 +49,10 @@ while [[ $# -gt 0 ]]; do VERIFIER_WORKFLOW_SHA="${2:-}" shift 2 ;; + --workflow-ref) + WORKFLOW_REF="${2:-}" + shift 2 + ;; --release-profile) RELEASE_PROFILE="${2:-}" shift 2 @@ -116,6 +122,13 @@ if [[ ! "$VERIFIER_WORKFLOW_SHA" =~ ^[0-9a-f]{40}$ ]]; then echo "Expected --workflow-sha to be a full lowercase commit SHA; got: ${VERIFIER_WORKFLOW_SHA}" >&2 exit 2 fi +if [[ "$WORKFLOW_REF" != "main" ]]; then + expected_release_ref="release-ci/${VERIFIER_WORKFLOW_SHA:0:12}-" + if [[ ! "$WORKFLOW_REF" =~ ^release-ci/[0-9a-f]{12}-[1-9][0-9]*$ ]] || + [[ "$WORKFLOW_REF" != "$expected_release_ref"* ]]; then + no_reuse "workflow ref is not a canonical SHA-pinned release ref" + fi +fi if [[ -z "$REPO" ]]; then echo "Expected --repo or GH_REPO." >&2 exit 2 @@ -134,6 +147,20 @@ if ! expected_inputs="$(jq -Sc 'if type == "object" then . else error("expected exit 2 fi +workflow_lineage="" +if ! workflow_lineage="$( + gh api "repos/${REPO}/compare/${VERIFIER_WORKFLOW_SHA}...main" +)"; then + no_reuse "could not verify workflow SHA against trusted main" +fi +if ! jq -e \ + --arg workflow_sha "$VERIFIER_WORKFLOW_SHA" ' + (.status == "ahead" or .status == "identical") + and .merge_base_commit.sha == $workflow_sha + ' <<< "$workflow_lineage" >/dev/null; then + no_reuse "workflow SHA is not on trusted main lineage" +fi + # Exact-target reuse still requires internally consistent version stamps # (for example package.json must agree with the macOS plist). if ! (cd "$REPO_DIR" && node "$PREFLIGHT" --macos-versions-only >&2); then diff --git a/test/scripts/find-reusable-release-validation.test.ts b/test/scripts/find-reusable-release-validation.test.ts index d72777c62c44..2ea11c1299fb 100644 --- a/test/scripts/find-reusable-release-validation.test.ts +++ b/test/scripts/find-reusable-release-validation.test.ts @@ -409,8 +409,18 @@ function runResolver(args: { runReleaseSoak?: string; targetSha: string; validatorPath: string; + verifierOnMain?: boolean; verifierSha?: string; + workflowRef?: string; }) { + const verifierSha = args.verifierSha ?? VERIFIER_SHA; + writeFileSync( + fixtureName(args.fixtures, `repos/${REPOSITORY}/compare/${verifierSha}...main`), + JSON.stringify({ + merge_base_commit: { sha: args.verifierOnMain === false ? "f".repeat(40) : verifierSha }, + status: args.verifierOnMain === false ? "diverged" : "ahead", + }), + ); return spawnSync( "bash", [ @@ -418,7 +428,9 @@ function runResolver(args: { "--target-sha", args.targetSha, "--workflow-sha", - args.verifierSha ?? VERIFIER_SHA, + verifierSha, + "--workflow-ref", + args.workflowRef ?? "main", "--release-profile", args.releaseProfile ?? "full", "--run-release-soak", @@ -459,6 +471,34 @@ function parseOutput(output: string): Record { } describe("scripts/github/find-reusable-release-validation.sh", () => { + it("rejects noncanonical release refs and workflow SHAs outside trusted main", () => { + const { origin, priorSha } = createRepo(); + const clone = cloneHead(origin); + const record = normalizedEvidence({ targetSha: priorSha }); + const { binDir, fixtures, validatorPath } = setUpFixtures([{ record, runId: "111" }]); + + const forgedRef = runResolver({ + binDir, + fixtures, + repoDir: clone, + targetSha: priorSha, + validatorPath, + workflowRef: "release-ci/not-trusted", + }); + expect(parseOutput(forgedRef.stdout)).toMatchObject({ reuse: "false" }); + + const untrustedSha = runResolver({ + binDir, + fixtures, + repoDir: clone, + targetSha: priorSha, + validatorPath, + verifierOnMain: false, + workflowRef: `release-ci/${VERIFIER_SHA.slice(0, 12)}-123`, + }); + expect(parseOutput(untrustedSha.stdout)).toMatchObject({ reuse: "false" }); + }); + it("reuses pre-tooling trusted-main evidence for the exact target", () => { const { origin, priorSha } = createRepo(); const clone = cloneHead(origin);