diff --git a/.github/workflows/semantic-review.yml b/.github/workflows/semantic-review.yml index a23a60be4..7d39a4b4f 100644 --- a/.github/workflows/semantic-review.yml +++ b/.github/workflows/semantic-review.yml @@ -47,10 +47,13 @@ jobs: throw new Error(`ambiguous workflow_run pull request bindings: ${runPRs.length}`); } let prNumber = Number(runPRs[0]?.number || 0); - let eventBaseSha = runPRs[0]?.base?.sha || ""; + const eventBaseSha = runPRs[0]?.base?.sha || ""; const eventHeadSha = runPRs[0]?.head?.sha || ""; - const targetHeadSha = eventHeadSha || run.head_sha; + const targetHeadSha = run.head_sha; if (!/^[a-f0-9]{40}$/i.test(targetHeadSha)) throw new Error("invalid PR head sha"); + if (eventHeadSha && eventHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) { + core.notice("PR quality summary using workflow_run head_sha because workflow_run pull request head differs from the CI run head"); + } const factsArtifactPattern = /^quality-gate-facts-([a-f0-9]{40})-([a-f0-9]{40})$/i; const { data: artifactData } = await github.rest.actions.listWorkflowRunArtifacts({ @@ -71,11 +74,11 @@ jobs: if (artifactHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) { artifactError = "facts artifact head sha does not match verified PR head sha"; factsArtifactName = ""; - } else if (eventBaseSha && parsedBaseSha.toLowerCase() !== eventBaseSha.toLowerCase()) { - artifactError = "facts artifact base sha does not match workflow_run pull request base sha"; - factsArtifactName = ""; } else { artifactBaseSha = parsedBaseSha; + if (eventBaseSha && parsedBaseSha.toLowerCase() !== eventBaseSha.toLowerCase()) { + core.notice("PR quality summary using facts artifact base because workflow_run pull request base differs from the CI facts artifact base"); + } } } if (!prNumber) { @@ -123,7 +126,7 @@ jobs: core.setOutput("stale", "true"); return; } - const baseSha = eventBaseSha || artifactBaseSha || pr.base.sha; + const baseSha = artifactBaseSha || eventBaseSha || pr.base.sha; if (!/^[a-f0-9]{40}$/i.test(baseSha)) throw new Error("invalid PR base sha"); if ((eventBaseSha || artifactBaseSha) && pr.base.sha !== baseSha) { core.notice("PR quality summary skipped: workflow_run is stale for this PR base"); @@ -255,10 +258,13 @@ jobs: throw new Error(`ambiguous workflow_run pull request bindings: ${runPRs.length}`); } let prNumber = Number(runPRs[0]?.number || 0); - let eventBaseSha = runPRs[0]?.base?.sha || ""; + const eventBaseSha = runPRs[0]?.base?.sha || ""; const eventHeadSha = runPRs[0]?.head?.sha || ""; - const targetHeadSha = eventHeadSha || run.head_sha; + const targetHeadSha = run.head_sha; if (!/^[a-f0-9]{40}$/i.test(targetHeadSha)) throw new Error("invalid PR head sha"); + if (eventHeadSha && eventHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) { + core.notice("semantic review using workflow_run head_sha because workflow_run pull request head differs from the CI run head"); + } const factsArtifactPattern = /^quality-gate-facts-([a-f0-9]{40})-([a-f0-9]{40})$/i; const { data: artifactData } = await github.rest.actions.listWorkflowRunArtifacts({ @@ -279,11 +285,11 @@ jobs: if (artifactHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) { artifactError = "facts artifact head sha does not match verified PR head sha"; factsArtifactName = ""; - } else if (eventBaseSha && parsedBaseSha.toLowerCase() !== eventBaseSha.toLowerCase()) { - artifactError = "facts artifact base sha does not match workflow_run pull request base sha"; - factsArtifactName = ""; } else { artifactBaseSha = parsedBaseSha; + if (eventBaseSha && parsedBaseSha.toLowerCase() !== eventBaseSha.toLowerCase()) { + core.notice("semantic review using facts artifact base because workflow_run pull request base differs from the CI facts artifact base"); + } } } if (!prNumber) { @@ -331,7 +337,7 @@ jobs: core.setOutput("stale", "true"); return; } - const baseSha = eventBaseSha || artifactBaseSha || pr.base.sha; + const baseSha = artifactBaseSha || eventBaseSha || pr.base.sha; if (!/^[a-f0-9]{40}$/i.test(baseSha)) throw new Error("invalid PR base sha"); if ((eventBaseSha || artifactBaseSha) && pr.base.sha !== baseSha) { core.notice("semantic review skipped: workflow_run is stale for this PR base"); diff --git a/scripts/semantic-review-workflow.test.sh b/scripts/semantic-review-workflow.test.sh index 92864708e..0ce4cc450 100644 --- a/scripts/semantic-review-workflow.test.sh +++ b/scripts/semantic-review-workflow.test.sh @@ -179,7 +179,10 @@ fi require_in_step "$summary_verify_step" 'workflowPath !== ".github/workflows/ci.yml"' "PR quality summary must verify the triggering workflow path" require_in_step "$summary_verify_step" 'run.event !== "pull_request"' "PR quality summary must only handle pull_request workflow_run events" require_in_step "$summary_verify_step" 'run.repository.id !== context.payload.repository.id' "PR quality summary must verify workflow_run repository id" +require_in_step "$summary_verify_step" 'const targetHeadSha = run.head_sha' "PR quality summary must use the CI run head SHA as the verified PR head" +require_in_step "$summary_verify_step" 'eventHeadSha && eventHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()' "PR quality summary should tolerate mutable workflow_run PR head metadata" require_in_step "$summary_verify_step" 'factsArtifactPattern' "PR quality summary should use the base-bound facts artifact name when available" +require_in_step "$summary_verify_step" 'const baseSha = artifactBaseSha || eventBaseSha || pr.base.sha' "PR quality summary must prefer the CI-time artifact base SHA" require_in_step "$summary_verify_step" 'core.setOutput("artifact_error"' "PR quality summary must expose artifact binding failures" require_in_step "$summary_artifact_step" 'factsArtifactName' "PR quality summary artifact step must use the verified facts artifact binding" require_in_step "$summary_extract_facts_step" 'SEMANTIC_REVIEW_DECISION_OUT' "PR quality summary artifact verifier must write an infrastructure decision on verifier failure" @@ -198,8 +201,9 @@ require_in_step "$verify_step" 'workflowPath !== ".github/workflows/ci.yml"' "se require_in_step "$verify_step" 'run.repository.id !== context.payload.repository.id' "semantic-review must verify workflow_run repository id" require_in_step "$verify_step" 'run.event !== "pull_request"' "semantic-review must only handle pull_request workflow_run events" require_in_step "$verify_step" 'run.conclusion !== "success"' "semantic-review must only consume successful CI runs" -require_in_step "$verify_step" 'const eventHeadSha = runPRs[0]?.head?.sha || ""' "semantic-review must prefer workflow_run PR head when GitHub provides it" -require_in_step "$verify_step" 'const targetHeadSha = eventHeadSha || run.head_sha' "semantic-review target PR head must come from the workflow_run event" +require_in_step "$verify_step" 'const eventHeadSha = runPRs[0]?.head?.sha || ""' "semantic-review should inspect workflow_run PR head metadata" +require_in_step "$verify_step" 'const targetHeadSha = run.head_sha' "semantic-review target PR head must come from the completed CI run" +require_in_step "$verify_step" 'eventHeadSha && eventHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()' "semantic-review should tolerate mutable workflow_run PR head metadata" require_in_step "$verify_step" 'factsArtifactPattern' "semantic-review must use a base-bound facts artifact name" require_in_step "$verify_step" 'listWorkflowRunArtifacts' "semantic-review must read the workflow_run artifacts before resolving fallback base SHA" require_in_step "$verify_step" 'artifactHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()' "semantic-review must not let the artifact choose a different PR head" @@ -210,8 +214,8 @@ require_in_step "$verify_step" 'commit_sha: targetHeadSha' "semantic-review fall require_in_step "$verify_step" 'github.rest.pulls.list' "semantic-review must have a pull-list fallback when commit association is empty" require_in_step "$verify_step" 'candidatePRs.length > 1' "semantic-review must fail closed when commit-to-PR fallback is ambiguous" require_in_step "$verify_step" 'pr.head.sha !== targetHeadSha' "semantic-review must skip stale PR heads" -require_in_step "$verify_step" 'eventBaseSha && parsedBaseSha.toLowerCase() !== eventBaseSha.toLowerCase()' "semantic-review must reject mismatched event and artifact base SHAs" -require_in_step "$verify_step" 'const baseSha = eventBaseSha || artifactBaseSha' "semantic-review fallback must use the CI-time artifact base SHA" +require_in_step "$verify_step" 'eventBaseSha && parsedBaseSha.toLowerCase() !== eventBaseSha.toLowerCase()' "semantic-review should tolerate mutable workflow_run PR base metadata" +require_in_step "$verify_step" 'const baseSha = artifactBaseSha || eventBaseSha || pr.base.sha' "semantic-review must prefer the CI-time artifact base SHA" require_in_step "$verify_step" 'pr.base.sha !== baseSha' "semantic-review must skip stale PR bases" require_in_step "$verify_step" 'core.setOutput("run_id"' "semantic-review must pass verified workflow run id to publisher" require_in_step "$verify_step" 'core.setOutput("head_repo_id"' "semantic-review must pass verified head repo id"