From 157ea376111a42dbc83950fb10c3c3dd520e664d Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Tue, 2 Jun 2026 20:53:48 +0000 Subject: [PATCH 1/7] ci(gate): add pull_request_review trigger to qa-review and security-review (internal#760) The qa-review and security-review gates previously only ran on pull_request_target (opened, synchronize, reopened). This meant a team member's APPROVE review did not flip the gate until the next push or a slash-command refire. Add pull_request_review: types: [submitted] to both workflows so the gate re-evaluates immediately when a review is submitted. Key design points: - The if: guard is updated to allow both event types. - The BASE-ref checkout trust boundary is preserved (ref: default_branch). - PR_NUMBER extraction already works for pull_request_review events via github.event.pull_request.number. - Context-name byte-match: Gitea maps both pull_request_target and pull_request_review to the same (pull_request) check-run suffix, evidence: existing sop-tier-check.yml model + branch-protection docs. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/qa-review.yml | 13 ++++++++++++- .gitea/workflows/security-review.yml | 14 +++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/qa-review.yml b/.gitea/workflows/qa-review.yml index 90a94c77e..d3b7674f2 100644 --- a/.gitea/workflows/qa-review.yml +++ b/.gitea/workflows/qa-review.yml @@ -9,6 +9,12 @@ # Triggers on: # - `pull_request_target`: opened, synchronize, reopened # → initial status posts when PR opens / re-pushes +# - `pull_request_review`: submitted +# → re-evaluate when a review is submitted so the gate flips +# immediately (no wait for the next push or slash-command). +# The status CONTEXT NAME this path posts MUST byte-match the +# branch-protection required context — Gitea maps both event +# types to the same `(pull_request)` check-run suffix. # - comment refires are handled by `review-refire-comments.yml` # → a single issue_comment dispatcher prevents every SOP/review # comment from enqueueing separate qa/security/tier jobs on @@ -85,6 +91,8 @@ name: qa-review on: pull_request_target: types: [opened, synchronize, reopened] + pull_request_review: + types: [submitted] permissions: contents: read @@ -96,10 +104,13 @@ jobs: approved: # Gate the job: # - On pull_request_target events: always run. + # - On pull_request_review events: run so the gate flips when a + # team member submits an APPROVE review (no push needed). # Comment-triggered refires live in review-refire-comments.yml. Keeping # this workflow PR-only avoids comment-triggered queue storms. if: | - github.event_name == 'pull_request_target' + github.event_name == 'pull_request_target' || + github.event_name == 'pull_request_review' runs-on: ubuntu-latest steps: - name: Privilege check (A1.1 — INFORMATIONAL log only, NOT a gate) diff --git a/.gitea/workflows/security-review.yml b/.gitea/workflows/security-review.yml index e905a401e..7582a67a6 100644 --- a/.gitea/workflows/security-review.yml +++ b/.gitea/workflows/security-review.yml @@ -6,12 +6,19 @@ # # See `qa-review.yml` header for the full A1-α / A1.1 / A4 / A5 design # rationale; everything below is identical in shape. +# +# A1-α addendum (internal#760): `pull_request_review` trigger added so the +# security gate flips immediately when a team member submits an APPROVE +# review. The status CONTEXT NAME MUST byte-match the branch-protection +# required context — Gitea maps both event types to `(pull_request)`. name: security-review on: pull_request_target: types: [opened, synchronize, reopened] + pull_request_review: + types: [submitted] permissions: contents: read @@ -21,10 +28,15 @@ permissions: jobs: # bp-exempt: PR security review bot signal; required merge state is enforced by CI / all-required. approved: + # Gate the job: + # - On pull_request_target events: always run. + # - On pull_request_review events: run so the gate flips when a + # team member submits an APPROVE review (no push needed). # Comment-triggered refires live in review-refire-comments.yml. Keeping # this workflow PR-only avoids comment-triggered queue storms. if: | - github.event_name == 'pull_request_target' + github.event_name == 'pull_request_target' || + github.event_name == 'pull_request_review' runs-on: ubuntu-latest steps: - name: Privilege check (A1.1 — INFORMATIONAL log only, NOT a gate) -- 2.52.0 From 5be8d191272f2471f304be6691184e7a4bc5d21b Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Tue, 2 Jun 2026 21:37:44 +0000 Subject: [PATCH 2/7] ci(gate): explicitly POST BP-required status context on pull_request_review (internal#760) CR2 live verification (REQUEST_CHANGES 8302) exposed that Gitea 1.22.6 auto-publishes (pull_request_review) context suffix for this event, while branch-protection requires (pull_request_target). The gate therefore never flipped on review submission. Fix: on pull_request_review events, after running review-check.sh, an additional step explicitly POSTs a commit status with the exact context name branch-protection requires: qa-review / approved (pull_request_target) security-review / approved (pull_request_target) Changes per workflow: - Add statuses: write permission (needed for POST /statuses/{sha}). - Add id: eval to the review-check step so the POST step can read its outcome. - Add "Post required status context on pull_request_review" step that runs if: always() so it fires whether review-check passed or failed. - Trust boundary preserved: same BASE-ref checkout, same trusted script, no PR-head code executed. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/qa-review.yml | 72 ++++++++++++++++++++++++++-- .gitea/workflows/security-review.yml | 70 +++++++++++++++++++++++++-- 2 files changed, 133 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/qa-review.yml b/.gitea/workflows/qa-review.yml index d3b7674f2..89dcac899 100644 --- a/.gitea/workflows/qa-review.yml +++ b/.gitea/workflows/qa-review.yml @@ -12,9 +12,13 @@ # - `pull_request_review`: submitted # → re-evaluate when a review is submitted so the gate flips # immediately (no wait for the next push or slash-command). -# The status CONTEXT NAME this path posts MUST byte-match the -# branch-protection required context — Gitea maps both event -# types to the same `(pull_request)` check-run suffix. +# CR2 live verification (REQUEST_CHANGES 8302) confirmed Gitea +# 1.22.6 posts `qa-review / approved (pull_request_review)` for +# this event, while branch-protection requires the +# `(pull_request_target)` variant. Therefore the +# pull_request_review path EXPLICITLY POSTS the required context +# via the API rather than relying on the job's auto-published +# context name. Trust boundary preserved (BASE ref, no PR-head). # - comment refires are handled by `review-refire-comments.yml` # → a single issue_comment dispatcher prevents every SOP/review # comment from enqueueing separate qa/security/tier jobs on @@ -97,7 +101,7 @@ on: permissions: contents: read pull-requests: read - secrets: read + statuses: write jobs: # bp-exempt: PR review bot signal; required merge state is enforced by CI / all-required. @@ -110,7 +114,7 @@ jobs: # this workflow PR-only avoids comment-triggered queue storms. if: | github.event_name == 'pull_request_target' || - github.event_name == 'pull_request_review' + (github.event_name == 'pull_request_review' && github.event.review.state == 'approved') runs-on: ubuntu-latest steps: - name: Privilege check (A1.1 — INFORMATIONAL log only, NOT a gate) @@ -154,6 +158,7 @@ jobs: ref: ${{ github.event.repository.default_branch }} - name: Evaluate qa-review + id: eval env: GITEA_TOKEN: ${{ secrets.SOP_TIER_CHECK_TOKEN || secrets.GITHUB_TOKEN }} GITEA_HOST: git.moleculesai.app @@ -168,3 +173,60 @@ jobs: REVIEW_CHECK_DEBUG: '0' REVIEW_CHECK_STRICT: '0' run: bash .gitea/scripts/review-check.sh + + - name: Post required status context on pull_request_review + # CR2 8302: Gitea 1.22.6 auto-publishes (pull_request_review) context + # for this event, but branch-protection requires (pull_request_target). + # We explicitly POST the BP-required context so the gate flips. + # Trust boundary: same BASE-ref script result, no PR-head code. + if: github.event_name == 'pull_request_review' && always() + env: + GITEA_TOKEN: ${{ secrets.SOP_TIER_CHECK_TOKEN || secrets.GITHUB_TOKEN }} + GITEA_HOST: git.moleculesai.app + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + EVAL_OUTCOME: ${{ steps.eval.outcome }} + run: | + set -euo pipefail + authfile=$(mktemp) + chmod 600 "$authfile" + printf 'header = "Authorization: token %s"\n' "$GITEA_TOKEN" > "$authfile" + + prfile=$(mktemp) + code=$(curl -sS -o "$prfile" -w '%{http_code}' -K "$authfile" \ + "https://${GITEA_HOST}/api/v1/repos/${REPO}/pulls/${PR_NUMBER}") + if [ "$code" != "200" ]; then + echo "::error::GET /pulls/${PR_NUMBER} returned HTTP ${code}" + rm -f "$prfile" "$authfile" + exit 1 + fi + head_sha=$(jq -r '.head.sha // ""' "$prfile") + rm -f "$prfile" + + if [ "$EVAL_OUTCOME" = "success" ]; then + status_state="success" + description="Approved via pull_request_review trigger" + else + status_state="failure" + description="Review check failed via pull_request_review trigger" + fi + + body=$(jq -nc \ + --arg state "$status_state" \ + --arg context "qa-review / approved (pull_request_target)" \ + --arg description "$description" \ + '{state:$state, context:$context, description:$description}') + + post_code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \ + -K "$authfile" -H "Content-Type: application/json" \ + -d "$body" \ + "https://${GITEA_HOST}/api/v1/repos/${REPO}/statuses/${head_sha}") + + rm -f "$authfile" + + if [ "$post_code" != "200" ] && [ "$post_code" != "201" ]; then + echo "::error::POST /statuses/${head_sha} returned HTTP ${post_code}" + exit 1 + fi + + echo "::notice::posted ${status_state} for context=\"qa-review / approved (pull_request_target)\" on sha=${head_sha}" diff --git a/.gitea/workflows/security-review.yml b/.gitea/workflows/security-review.yml index 7582a67a6..2eb4d31d5 100644 --- a/.gitea/workflows/security-review.yml +++ b/.gitea/workflows/security-review.yml @@ -9,8 +9,12 @@ # # A1-α addendum (internal#760): `pull_request_review` trigger added so the # security gate flips immediately when a team member submits an APPROVE -# review. The status CONTEXT NAME MUST byte-match the branch-protection -# required context — Gitea maps both event types to `(pull_request)`. +# review. CR2 live verification (REQUEST_CHANGES 8302) confirmed Gitea +# 1.22.6 posts `security-review / approved (pull_request_review)` for this +# event, while branch-protection requires `(pull_request_target)`. Therefore +# the pull_request_review path EXPLICITLY POSTS the required context via +# the API rather than relying on the job's auto-published context name. +# Trust boundary preserved (BASE ref, no PR-head). name: security-review @@ -23,7 +27,7 @@ on: permissions: contents: read pull-requests: read - secrets: read + statuses: write jobs: # bp-exempt: PR security review bot signal; required merge state is enforced by CI / all-required. @@ -36,7 +40,7 @@ jobs: # this workflow PR-only avoids comment-triggered queue storms. if: | github.event_name == 'pull_request_target' || - github.event_name == 'pull_request_review' + (github.event_name == 'pull_request_review' && github.event.review.state == 'approved') runs-on: ubuntu-latest steps: - name: Privilege check (A1.1 — INFORMATIONAL log only, NOT a gate) @@ -69,6 +73,7 @@ jobs: ref: ${{ github.event.repository.default_branch }} - name: Evaluate security-review + id: eval env: GITEA_TOKEN: ${{ secrets.SOP_TIER_CHECK_TOKEN || secrets.GITHUB_TOKEN }} GITEA_HOST: git.moleculesai.app @@ -80,3 +85,60 @@ jobs: REVIEW_CHECK_DEBUG: '0' REVIEW_CHECK_STRICT: '0' run: bash .gitea/scripts/review-check.sh + + - name: Post required status context on pull_request_review + # CR2 8302: Gitea 1.22.6 auto-publishes (pull_request_review) context + # for this event, but branch-protection requires (pull_request_target). + # We explicitly POST the BP-required context so the gate flips. + # Trust boundary: same BASE-ref script result, no PR-head code. + if: github.event_name == 'pull_request_review' && always() + env: + GITEA_TOKEN: ${{ secrets.SOP_TIER_CHECK_TOKEN || secrets.GITHUB_TOKEN }} + GITEA_HOST: git.moleculesai.app + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + EVAL_OUTCOME: ${{ steps.eval.outcome }} + run: | + set -euo pipefail + authfile=$(mktemp) + chmod 600 "$authfile" + printf 'header = "Authorization: token %s"\n' "$GITEA_TOKEN" > "$authfile" + + prfile=$(mktemp) + code=$(curl -sS -o "$prfile" -w '%{http_code}' -K "$authfile" \ + "https://${GITEA_HOST}/api/v1/repos/${REPO}/pulls/${PR_NUMBER}") + if [ "$code" != "200" ]; then + echo "::error::GET /pulls/${PR_NUMBER} returned HTTP ${code}" + rm -f "$prfile" "$authfile" + exit 1 + fi + head_sha=$(jq -r '.head.sha // ""' "$prfile") + rm -f "$prfile" + + if [ "$EVAL_OUTCOME" = "success" ]; then + status_state="success" + description="Approved via pull_request_review trigger" + else + status_state="failure" + description="Review check failed via pull_request_review trigger" + fi + + body=$(jq -nc \ + --arg state "$status_state" \ + --arg context "security-review / approved (pull_request_target)" \ + --arg description "$description" \ + '{state:$state, context:$context, description:$description}') + + post_code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \ + -K "$authfile" -H "Content-Type: application/json" \ + -d "$body" \ + "https://${GITEA_HOST}/api/v1/repos/${REPO}/statuses/${head_sha}") + + rm -f "$authfile" + + if [ "$post_code" != "200" ] && [ "$post_code" != "201" ]; then + echo "::error::POST /statuses/${head_sha} returned HTTP ${post_code}" + exit 1 + fi + + echo "::notice::posted ${status_state} for context=\"security-review / approved (pull_request_target)\" on sha=${head_sha}" -- 2.52.0 From aaa5cbccfc43ff8be589410511799def254890dc Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Tue, 2 Jun 2026 22:10:59 +0000 Subject: [PATCH 3/7] ci(gate): uppercase APPROVED in pull_request_review job guard (internal#760) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CR2 live verification (review #8311) exposed that Gitea 1.22.6 uses uppercase 'APPROVED' for github.event.review.state, while the workflow job-level `if:` guard checked lowercase 'approved'. This caused the entire job to be SKIPPED on review submission, so neither the evaluator nor the explicit status-post step ran. Fix: 'approved' → 'APPROVED' in both qa-review.yml and security-review.yml. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/qa-review.yml | 2 +- .gitea/workflows/security-review.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/qa-review.yml b/.gitea/workflows/qa-review.yml index 89dcac899..ab1f9ec6b 100644 --- a/.gitea/workflows/qa-review.yml +++ b/.gitea/workflows/qa-review.yml @@ -114,7 +114,7 @@ jobs: # this workflow PR-only avoids comment-triggered queue storms. if: | github.event_name == 'pull_request_target' || - (github.event_name == 'pull_request_review' && github.event.review.state == 'approved') + (github.event_name == 'pull_request_review' && github.event.review.state == 'APPROVED') runs-on: ubuntu-latest steps: - name: Privilege check (A1.1 — INFORMATIONAL log only, NOT a gate) diff --git a/.gitea/workflows/security-review.yml b/.gitea/workflows/security-review.yml index 2eb4d31d5..556eff1ce 100644 --- a/.gitea/workflows/security-review.yml +++ b/.gitea/workflows/security-review.yml @@ -40,7 +40,7 @@ jobs: # this workflow PR-only avoids comment-triggered queue storms. if: | github.event_name == 'pull_request_target' || - (github.event_name == 'pull_request_review' && github.event.review.state == 'approved') + (github.event_name == 'pull_request_review' && github.event.review.state == 'APPROVED') runs-on: ubuntu-latest steps: - name: Privilege check (A1.1 — INFORMATIONAL log only, NOT a gate) -- 2.52.0 From 41fc7523f6ecc0a0a0918b737f658c4c18e4b1aa Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Tue, 2 Jun 2026 23:35:59 +0000 Subject: [PATCH 4/7] ci(gate): add diagnostic job to dump pull_request_review payload (RC 8321, internal#760) CR2 live verification shows the job-level guard still prevents the pull_request_review path from running. Rather than guess the 4th time, add a temporary diagnostic job that dumps toJSON(github.event) so we can see the exact key path Gitea 1.22.6 uses for review.state. Will be removed once the correct guard expression is determined. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/qa-review.yml | 15 +++++++++++++++ .gitea/workflows/security-review.yml | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/.gitea/workflows/qa-review.yml b/.gitea/workflows/qa-review.yml index ab1f9ec6b..33c16b474 100644 --- a/.gitea/workflows/qa-review.yml +++ b/.gitea/workflows/qa-review.yml @@ -104,6 +104,21 @@ permissions: statuses: write jobs: + # DIAGNOSTIC (RC 8321): dump the exact pull_request_review payload shape + # so we can write a correct job-level guard. CR2 will trigger this via a + # review submission; the log reveals the JSON path Gitea 1.22.6 actually + # uses for review.state. Removed after guard is fixed. + diagnostic: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request_review' + steps: + - name: Dump github.event JSON + run: | + echo "EVENT_NAME=${{ github.event_name }}" + echo "REVIEW_STATE=${{ github.event.review.state }}" + echo "ACTION=${{ github.event.action }}" + echo "FULL_EVENT=${{ toJSON(github.event) }}" + # bp-exempt: PR review bot signal; required merge state is enforced by CI / all-required. approved: # Gate the job: diff --git a/.gitea/workflows/security-review.yml b/.gitea/workflows/security-review.yml index 556eff1ce..251129476 100644 --- a/.gitea/workflows/security-review.yml +++ b/.gitea/workflows/security-review.yml @@ -30,6 +30,21 @@ permissions: statuses: write jobs: + # DIAGNOSTIC (RC 8321): dump the exact pull_request_review payload shape + # so we can write a correct job-level guard. CR2 will trigger this via a + # review submission; the log reveals the JSON path Gitea 1.22.6 actually + # uses for review.state. Removed after guard is fixed. + diagnostic: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request_review' + steps: + - name: Dump github.event JSON + run: | + echo "EVENT_NAME=${{ github.event_name }}" + echo "REVIEW_STATE=${{ github.event.review.state }}" + echo "ACTION=${{ github.event.action }}" + echo "FULL_EVENT=${{ toJSON(github.event) }}" + # bp-exempt: PR security review bot signal; required merge state is enforced by CI / all-required. approved: # Gate the job: -- 2.52.0 From 323aec4562c598f684feae67de4468f54792da05 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Tue, 2 Jun 2026 23:45:05 +0000 Subject: [PATCH 5/7] RC 8321: use pull_request_review_approved event (Gitea-specific) Gitea Actions does NOT support the GitHub-style `pull_request_review` catch-all event. Source-code audit of go-gitea/gitea main confirms: - modules/webhook/type.go AllEvents() lists only the specific review events: pull_request_review_approved, pull_request_review_rejected, pull_request_review_comment. The generic `pull_request_review` is marked FIXME and excluded. - services/actions/notifier.go builds the payload with review.type="pull_request_review_approved" (not review.state). There is no review.state field in the Gitea Actions payload. Therefore: - Replace `on: pull_request_review` with `on: pull_request_review_approved` - Replace job guard `github.event.review.state == 'APPROVED'` with the simpler `github.event_name == 'pull_request_review_approved'` - Remove diagnostic job (root cause found via source audit, not payload dump) - Update all comments referencing the old event name Same changes applied to both qa-review.yml and security-review.yml. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/qa-review.yml | 63 +++++++++++----------------- .gitea/workflows/security-review.yml | 52 +++++++++-------------- 2 files changed, 43 insertions(+), 72 deletions(-) diff --git a/.gitea/workflows/qa-review.yml b/.gitea/workflows/qa-review.yml index 33c16b474..6ab9cb4f0 100644 --- a/.gitea/workflows/qa-review.yml +++ b/.gitea/workflows/qa-review.yml @@ -9,20 +9,22 @@ # Triggers on: # - `pull_request_target`: opened, synchronize, reopened # → initial status posts when PR opens / re-pushes -# - `pull_request_review`: submitted -# → re-evaluate when a review is submitted so the gate flips -# immediately (no wait for the next push or slash-command). -# CR2 live verification (REQUEST_CHANGES 8302) confirmed Gitea -# 1.22.6 posts `qa-review / approved (pull_request_review)` for -# this event, while branch-protection requires the -# `(pull_request_target)` variant. Therefore the -# pull_request_review path EXPLICITLY POSTS the required context -# via the API rather than relying on the job's auto-published -# context name. Trust boundary preserved (BASE ref, no PR-head). -# - comment refires are handled by `review-refire-comments.yml` -# → a single issue_comment dispatcher prevents every SOP/review -# comment from enqueueing separate qa/security/tier jobs on -# Gitea 1.22.6 before job-level `if:` can skip them. +# - `pull_request_review_approved` +# → re-evaluate when a team member submits an APPROVE review so +# the gate flips immediately (no wait for the next push or +# slash-command). Gitea Actions uses the specific event name +# `pull_request_review_approved` (not the GitHub-style +# `pull_request_review` catch-all). Verified via Gitea source +# code audit (go-gitea/gitea main, modules/webhook/type.go + +# services/actions/notifier.go). The event payload carries +# `review.type="pull_request_review_approved"`; there is no +# `review.state` field. Branch-protection requires the +# `(pull_request_target)` context variant, so the +# pull_request_review_approved path EXPLICITLY POSTS the +# required context via the API. Trust boundary preserved +# (BASE ref, no PR-head). +# - comment refires are handled by `sop-checklist.yml` review-refire job +# → `/qa-recheck` slash-command re-evaluates this gate. # Workflow name = `qa-review` ; job name = `approved`. # The job's own pass/fail conclusion publishes the status context # `qa-review / approved ()` — NO `POST /statuses` call → NO @@ -95,8 +97,7 @@ name: qa-review on: pull_request_target: types: [opened, synchronize, reopened] - pull_request_review: - types: [submitted] + pull_request_review_approved: permissions: contents: read @@ -104,32 +105,16 @@ permissions: statuses: write jobs: - # DIAGNOSTIC (RC 8321): dump the exact pull_request_review payload shape - # so we can write a correct job-level guard. CR2 will trigger this via a - # review submission; the log reveals the JSON path Gitea 1.22.6 actually - # uses for review.state. Removed after guard is fixed. - diagnostic: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request_review' - steps: - - name: Dump github.event JSON - run: | - echo "EVENT_NAME=${{ github.event_name }}" - echo "REVIEW_STATE=${{ github.event.review.state }}" - echo "ACTION=${{ github.event.action }}" - echo "FULL_EVENT=${{ toJSON(github.event) }}" - # bp-exempt: PR review bot signal; required merge state is enforced by CI / all-required. approved: # Gate the job: # - On pull_request_target events: always run. - # - On pull_request_review events: run so the gate flips when a - # team member submits an APPROVE review (no push needed). - # Comment-triggered refires live in review-refire-comments.yml. Keeping - # this workflow PR-only avoids comment-triggered queue storms. + # - On pull_request_review_approved events: run so the gate flips + # immediately when a team member submits an APPROVE review. + # Comment-triggered refires live in sop-checklist.yml review-refire job. if: | github.event_name == 'pull_request_target' || - (github.event_name == 'pull_request_review' && github.event.review.state == 'APPROVED') + github.event_name == 'pull_request_review_approved' runs-on: ubuntu-latest steps: - name: Privilege check (A1.1 — INFORMATIONAL log only, NOT a gate) @@ -189,12 +174,12 @@ jobs: REVIEW_CHECK_STRICT: '0' run: bash .gitea/scripts/review-check.sh - - name: Post required status context on pull_request_review - # CR2 8302: Gitea 1.22.6 auto-publishes (pull_request_review) context + - name: Post required status context on pull_request_review_approved + # Gitea Actions auto-publishes (pull_request_review_approved) context # for this event, but branch-protection requires (pull_request_target). # We explicitly POST the BP-required context so the gate flips. # Trust boundary: same BASE-ref script result, no PR-head code. - if: github.event_name == 'pull_request_review' && always() + if: github.event_name == 'pull_request_review_approved' && always() env: GITEA_TOKEN: ${{ secrets.SOP_TIER_CHECK_TOKEN || secrets.GITHUB_TOKEN }} GITEA_HOST: git.moleculesai.app diff --git a/.gitea/workflows/security-review.yml b/.gitea/workflows/security-review.yml index 251129476..d1e91675a 100644 --- a/.gitea/workflows/security-review.yml +++ b/.gitea/workflows/security-review.yml @@ -7,22 +7,24 @@ # See `qa-review.yml` header for the full A1-α / A1.1 / A4 / A5 design # rationale; everything below is identical in shape. # -# A1-α addendum (internal#760): `pull_request_review` trigger added so the -# security gate flips immediately when a team member submits an APPROVE -# review. CR2 live verification (REQUEST_CHANGES 8302) confirmed Gitea -# 1.22.6 posts `security-review / approved (pull_request_review)` for this -# event, while branch-protection requires `(pull_request_target)`. Therefore -# the pull_request_review path EXPLICITLY POSTS the required context via -# the API rather than relying on the job's auto-published context name. -# Trust boundary preserved (BASE ref, no PR-head). +# A1-α addendum (internal#760): `pull_request_review_approved` trigger +# added so the security gate flips immediately when a team member submits +# an APPROVE review. Gitea Actions uses the specific event name +# `pull_request_review_approved` (not the GitHub-style `pull_request_review` +# catch-all). Verified via Gitea source code audit (go-gitea/gitea main, +# modules/webhook/type.go + services/actions/notifier.go). The event +# payload carries `review.type="pull_request_review_approved"`; there is +# no `review.state` field. Branch-protection requires the +# `(pull_request_target)` context variant, so the +# pull_request_review_approved path EXPLICITLY POSTS the required context +# via the API. Trust boundary preserved (BASE ref, no PR-head). name: security-review on: pull_request_target: types: [opened, synchronize, reopened] - pull_request_review: - types: [submitted] + pull_request_review_approved: permissions: contents: read @@ -30,32 +32,16 @@ permissions: statuses: write jobs: - # DIAGNOSTIC (RC 8321): dump the exact pull_request_review payload shape - # so we can write a correct job-level guard. CR2 will trigger this via a - # review submission; the log reveals the JSON path Gitea 1.22.6 actually - # uses for review.state. Removed after guard is fixed. - diagnostic: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request_review' - steps: - - name: Dump github.event JSON - run: | - echo "EVENT_NAME=${{ github.event_name }}" - echo "REVIEW_STATE=${{ github.event.review.state }}" - echo "ACTION=${{ github.event.action }}" - echo "FULL_EVENT=${{ toJSON(github.event) }}" - # bp-exempt: PR security review bot signal; required merge state is enforced by CI / all-required. approved: # Gate the job: # - On pull_request_target events: always run. - # - On pull_request_review events: run so the gate flips when a - # team member submits an APPROVE review (no push needed). - # Comment-triggered refires live in review-refire-comments.yml. Keeping - # this workflow PR-only avoids comment-triggered queue storms. + # - On pull_request_review_approved events: run so the gate flips + # immediately when a team member submits an APPROVE review. + # Comment-triggered refires live in sop-checklist.yml review-refire job. if: | github.event_name == 'pull_request_target' || - (github.event_name == 'pull_request_review' && github.event.review.state == 'APPROVED') + github.event_name == 'pull_request_review_approved' runs-on: ubuntu-latest steps: - name: Privilege check (A1.1 — INFORMATIONAL log only, NOT a gate) @@ -101,12 +87,12 @@ jobs: REVIEW_CHECK_STRICT: '0' run: bash .gitea/scripts/review-check.sh - - name: Post required status context on pull_request_review - # CR2 8302: Gitea 1.22.6 auto-publishes (pull_request_review) context + - name: Post required status context on pull_request_review_approved + # Gitea Actions auto-publishes (pull_request_review_approved) context # for this event, but branch-protection requires (pull_request_target). # We explicitly POST the BP-required context so the gate flips. # Trust boundary: same BASE-ref script result, no PR-head code. - if: github.event_name == 'pull_request_review' && always() + if: github.event_name == 'pull_request_review_approved' && always() env: GITEA_TOKEN: ${{ secrets.SOP_TIER_CHECK_TOKEN || secrets.GITHUB_TOKEN }} GITEA_HOST: git.moleculesai.app -- 2.52.0 From ca653d873347855a58b5630252a44abfe570499d Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Tue, 2 Jun 2026 23:56:47 +0000 Subject: [PATCH 6/7] RC 8324: use secrets.GITHUB_TOKEN for explicit status POST (Option B) The explicit POST to /repos/{R}/statuses/{sha} in the pull_request_review_approved path was returning HTTP 403 because SOP_TIER_CHECK_TOKEN lacks statuses:write scope. Fix: use secrets.GITHUB_TOKEN directly for the POST step. The workflow permissions block already grants statuses:write to the auto-injected GITHUB_TOKEN. The evaluation step continues to use SOP_TIER_CHECK_TOKEN || GITHUB_TOKEN since it only needs read scope (and SOP_TIER_CHECK_TOKEN's owner is in the qa/security teams, avoiding 403 on team-membership probes). Same change applied to both qa-review.yml and security-review.yml. 34 bash tests green. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/qa-review.yml | 11 ++++++++--- .gitea/workflows/security-review.yml | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.gitea/workflows/qa-review.yml b/.gitea/workflows/qa-review.yml index 6ab9cb4f0..41bbf8520 100644 --- a/.gitea/workflows/qa-review.yml +++ b/.gitea/workflows/qa-review.yml @@ -179,9 +179,14 @@ jobs: # for this event, but branch-protection requires (pull_request_target). # We explicitly POST the BP-required context so the gate flips. # Trust boundary: same BASE-ref script result, no PR-head code. + # + # TOKEN SCOPE FIX (RC 8324): uses secrets.GITHUB_TOKEN directly. + # SOP_TIER_CHECK_TOKEN lacks statuses:write scope → POST /statuses + # returns 403. The workflow permissions block already grants + # statuses:write to the auto-injected GITHUB_TOKEN. if: github.event_name == 'pull_request_review_approved' && always() env: - GITEA_TOKEN: ${{ secrets.SOP_TIER_CHECK_TOKEN || secrets.GITHUB_TOKEN }} + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITEA_HOST: git.moleculesai.app REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} @@ -205,10 +210,10 @@ jobs: if [ "$EVAL_OUTCOME" = "success" ]; then status_state="success" - description="Approved via pull_request_review trigger" + description="Approved via pull_request_review_approved trigger" else status_state="failure" - description="Review check failed via pull_request_review trigger" + description="Review check failed via pull_request_review_approved trigger" fi body=$(jq -nc \ diff --git a/.gitea/workflows/security-review.yml b/.gitea/workflows/security-review.yml index d1e91675a..671071a6c 100644 --- a/.gitea/workflows/security-review.yml +++ b/.gitea/workflows/security-review.yml @@ -92,9 +92,14 @@ jobs: # for this event, but branch-protection requires (pull_request_target). # We explicitly POST the BP-required context so the gate flips. # Trust boundary: same BASE-ref script result, no PR-head code. + # + # TOKEN SCOPE FIX (RC 8324): uses secrets.GITHUB_TOKEN directly. + # SOP_TIER_CHECK_TOKEN lacks statuses:write scope → POST /statuses + # returns 403. The workflow permissions block already grants + # statuses:write to the auto-injected GITHUB_TOKEN. if: github.event_name == 'pull_request_review_approved' && always() env: - GITEA_TOKEN: ${{ secrets.SOP_TIER_CHECK_TOKEN || secrets.GITHUB_TOKEN }} + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITEA_HOST: git.moleculesai.app REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} @@ -118,10 +123,10 @@ jobs: if [ "$EVAL_OUTCOME" = "success" ]; then status_state="success" - description="Approved via pull_request_review trigger" + description="Approved via pull_request_review_approved trigger" else status_state="failure" - description="Review check failed via pull_request_review trigger" + description="Review check failed via pull_request_review_approved trigger" fi body=$(jq -nc \ -- 2.52.0 From 801ab23ff5a1dc1519bf1ee65dcd1e9762475f50 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 3 Jun 2026 00:07:18 +0000 Subject: [PATCH 7/7] RC 8326: use STATUS_POST_TOKEN for explicit status POST (CTO grant) CTO granted a dedicated narrow-scoped STATUS_POST_TOKEN (msg d52cc72a, write:repository) for the explicit POST /statuses step on the pull_request_review_approved path. Security separation (deliberate, CTO-specified): - Evaluator step: SOP_TIER_CHECK_TOKEN || GITHUB_TOKEN (read-only) - Status POST step: STATUS_POST_TOKEN (write-only) This prevents the evaluator token from ever forging the status it computes. Eval reads; POST writes; never the same credential. Same change applied to qa-review.yml and security-review.yml. 34 bash tests green. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/qa-review.yml | 11 ++++++----- .gitea/workflows/security-review.yml | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.gitea/workflows/qa-review.yml b/.gitea/workflows/qa-review.yml index 41bbf8520..33aee528e 100644 --- a/.gitea/workflows/qa-review.yml +++ b/.gitea/workflows/qa-review.yml @@ -180,13 +180,14 @@ jobs: # We explicitly POST the BP-required context so the gate flips. # Trust boundary: same BASE-ref script result, no PR-head code. # - # TOKEN SCOPE FIX (RC 8324): uses secrets.GITHUB_TOKEN directly. - # SOP_TIER_CHECK_TOKEN lacks statuses:write scope → POST /statuses - # returns 403. The workflow permissions block already grants - # statuses:write to the auto-injected GITHUB_TOKEN. + # TOKEN FIX (RC 8326): uses STATUS_POST_TOKEN (CTO-granted, + # msg d52cc72a). Dedicated narrow-scoped write:repository token + # for the explicit status POST. Evaluator step stays on + # SOP_TIER_CHECK_TOKEN (read-only) per deliberate security + # separation: eval computes, POST writes, never the same cred. if: github.event_name == 'pull_request_review_approved' && always() env: - GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITEA_TOKEN: ${{ secrets.STATUS_POST_TOKEN }} GITEA_HOST: git.moleculesai.app REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} diff --git a/.gitea/workflows/security-review.yml b/.gitea/workflows/security-review.yml index 671071a6c..0ff5b79b6 100644 --- a/.gitea/workflows/security-review.yml +++ b/.gitea/workflows/security-review.yml @@ -93,13 +93,14 @@ jobs: # We explicitly POST the BP-required context so the gate flips. # Trust boundary: same BASE-ref script result, no PR-head code. # - # TOKEN SCOPE FIX (RC 8324): uses secrets.GITHUB_TOKEN directly. - # SOP_TIER_CHECK_TOKEN lacks statuses:write scope → POST /statuses - # returns 403. The workflow permissions block already grants - # statuses:write to the auto-injected GITHUB_TOKEN. + # TOKEN FIX (RC 8326): uses STATUS_POST_TOKEN (CTO-granted, + # msg d52cc72a). Dedicated narrow-scoped write:repository token + # for the explicit status POST. Evaluator step stays on + # SOP_TIER_CHECK_TOKEN (read-only) per deliberate security + # separation: eval computes, POST writes, never the same cred. if: github.event_name == 'pull_request_review_approved' && always() env: - GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITEA_TOKEN: ${{ secrets.STATUS_POST_TOKEN }} GITEA_HOST: git.moleculesai.app REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} -- 2.52.0