Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions .github/workflows/claude-author-automerge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,25 @@ name: Claude-author Auto-merge (reusable)
# changes (lesson 2026-05-04, wxa_vpn#250) and was narrowed
# 2026-05-20.
#
# Customer-copy diffs (legal / marketing / pricing wording) are NOT
# Customer-copy diffs (legal / marketing / pricing wording) are MOSTLY not
# detected by the global regex here — repo layouts differ (techrecon:
# site/src/**; wxa-astro: src/content/** Tina JSON), so a central glob
# would over-block some repos and miss others. The enforcement for this
# policy category is the caller's .github/risk-paths.yml `sensitive:`
# list (item 1 above): marketing-site repos MUST list their
# wording-bearing trees there. Lesson 2026-07-02 from
# wording-bearing trees there.
#
# EXCEPTION: `^docs/legal/.*` IS matched centrally (added 2026-07-27).
# Unlike a marketing tree, docs/legal/ is a fixed conventional path, so it
# cannot over-block — a repo without that directory is unaffected — and it
# needs a DURABLE gate rather than a per-repo one. safe-paths-automerge.yml
# also refuses these paths, but that is a revocation after the fact and
# races this workflow's arm; matching here means a Claude-authored legal
# change is never armed in the first place. Lesson 2026-07-25,
# whois-api-llc/wxa_vpn#1268: an AUP change asserting all bulk deliveries
# are watermarked (false for .mmdb and .parquet) auto-merged unreviewed.
# Legal wording is the archetypal not-revertable-in-one-cycle category:
# CI cannot check whether a clause is TRUE. Lesson 2026-07-02 from
# whois-api-llc/techrecon#405: a marketing-copy PR auto-merge-armed
# because techrecon's sensitive: list had no site/ entries; fixed in
# techrecon#410 + wxa-astro#63. Claude Review still reviews copy
Expand Down Expand Up @@ -673,6 +685,22 @@ jobs:
echo "::error::PR changes 3000+ files — file listing is truncated and the risk scan cannot classify every path. Apply the '${BYPASS_LABEL:-auto-merge-approved}' label or merge manually."
exit 1
fi
# Renames report ONLY their destination in `.filename`; the source
# is in `.previous_filename`. Without it, MOVING a risky file out
# of a risky path — `docs/legal/aup.md` → `docs/archive/aup.md`,
# or `src/auth/login.py` → `src/util/login.py` — reads as an
# ordinary change and escapes the gate, even though deleting a
# policy from its canonical location is exactly the edit that
# needs a human. Scanning both ends can only ADD risk
# classification, never remove it, so it always fails toward
# manual merge. Appended AFTER the 3000-file count so rename
# sources cannot push a large-but-listable PR over the cap.
# (Codex review round 9.)
renamed_from=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR}/files?per_page=100" \
--paginate --jq '.[] | select(.previous_filename) | .previous_filename' 2>/dev/null || echo "")
if [ -n "$renamed_from" ]; then
changed=$(printf '%s\n%s' "$changed" "$renamed_from")
fi
# Risk-tier glob patterns (kept in sync with global CLAUDE.md policy).
# If you add a category, also update install-automerge-policy.sh and
# the CLAUDE.md policy block.
Expand All @@ -684,6 +712,7 @@ jobs:
^(.*/)?migrations(/|$)
.*\.sql$
^(.*/)?(billing|payment[s]?|pricing|invoice[s]?|subscription[s]?|checkout|refund[s]?)(/|\.(py|go|ts|tsx|js|jsx)$|$)
^docs/legal/.*
(^|/)naf(/|\.(py|go|ts|js)$)
(^|/)main\.go$
(^|/)Dockerfile(\..*)?$
Expand Down
88 changes: 84 additions & 4 deletions .github/workflows/pr-codex-review.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
name: PR Codex Review

# OpenAI Codex CLI as a third-party reviewer for vendor diversity.
# Runs ONLY on risk:sensitive PRs (per .github/risk-paths.yml). Pairs with
# Claude Review (which runs on every PR via pr-review.yml) to give
# Runs on risk:sensitive AND risk:standard PRs (per .github/risk-paths.yml)
# — i.e. every class that can reach main without a human reading the diff.
# Pairs with Claude Review (which runs on every PR via pr-review.yml) to give
# independent-model coverage on high-impact code paths.
#
# Actual spend is gated a second time inside codex-review.yml by
# codex-gate.mjs (small diffs and docs/tests-only diffs skip), so the
# widening to `standard` does not mean a Codex run on every PR.

# ready_for_review is required in the type list: every draft-phase run
# (opened, synchronize) skips on the draft gate below, so without it a PR
Expand All @@ -13,8 +18,9 @@ on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
# Skip the entire Codex track on bootstrap-paths. The classifier labels
# these risk:blocked, and this caller only reviews risk:sensitive — so
# running it would burn a classify job just to produce a skipped review.
# these risk:blocked, and this caller reviews only sensitive/standard —
# so running it would burn a classify job just to produce a skipped
# review.
# Bootstrap-path PRs always require admin merge by design, so a
# secondary review adds no signal.
#
Expand Down Expand Up @@ -52,9 +58,83 @@ jobs:
if: github.event.pull_request.draft == false
uses: topcoder1/ci-workflows/.github/workflows/pr-classify.yml@main

# ---------------------------------------------------------------------
# TWO jobs, never both. They differ ONLY in job id, and that difference is
# load-bearing: the check name GitHub publishes is "<job id> / Codex
# Review", and claude-author-automerge.yml's `codex_check_name` input
# (default "review / Codex Review") names THIS job's check as the one
# whose SUCCESS bypasses the risk-tier manual-merge gate.
#
# That bypass trusts a conclusion, not a review. When codex-gate.mjs
# decides to skip (diff under SIZE_THRESHOLD, or docs/tests-only), the
# review STEPS are skipped but the JOB still concludes `success` — so a
# skipped review is indistinguishable from a passed one at the check-run
# level.
#
# Routing `standard` through the same job id would therefore have turned
# a coverage fix into a security regression: a repo whose risk-paths.yml
# `sensitive:` list is empty (the shipped default) classifies a small
# change to an auth / billing / migration path as `standard`, the central
# risk regex in claude-author-automerge.yml still flags it risky=1, and
# today that combination has NO Codex check at all, so the risk gate
# holds and a human merges it. Emitting `review / Codex Review = success`
# for it — without Codex having read a line — would auto-merge it.
# (Codex review round 2 caught exactly this on the first draft of this
# change.)
#
# So: `sensitive` keeps job id `review` and its existing bypass semantics,
# unchanged. `standard` gets a DIFFERENT job id, publishing
# "review_standard / Codex Review", which matches no `codex_check_name`
# and can never satisfy the bypass. Pure added review coverage, zero new
# auto-merge surface.
#
# PRE-EXISTING and NOT addressed here: the same skip-reads-as-success hole
# already applies to `sensitive` PRs on the `review` job. Narrowing that
# means changing check-conclusion semantics fleet-wide (40+ repos) and is
# a deliberate design decision, not a side effect of this change.
review:
needs: classify
if: needs.classify.outputs.risk_class == 'sensitive'
uses: topcoder1/ci-workflows/.github/workflows/codex-review.yml@main
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

# `standard` is the fallback class for any path matching no rule AND a
# class claude-author-automerge.yml will auto-merge — so without this it
# is the largest body of code reaching main with no second-model read.
# Lesson 2026-07-26, whois-api-llc/wxa_vpn#1270 (+839 lines under src/**,
# classified `standard`): CI passed it clean; a manual Codex run
# afterwards found four P1s, including a forgeable-identity hole that let
# a customer attribute a leaked artifact to a competitor. #1273 (+786,
# also `standard`) had the same exposure.
#
# Spend stays bounded by codex-review.yml's own codex-gate.mjs (skips
# small diffs and docs/tests-only diffs), so this is not "Codex on every
# PR" — the cost gate decides, not the risk class.
#
# SCOPE — this lane is ADVISORY, not a merge gate. Read this before
# assuming a standard PR cannot merge unreviewed.
# GitHub's auto-merge waits only for REQUIRED status checks.
# "review_standard / Codex Review" is not in any repo's required-checks
# ruleset and is not consulted by claude-author-automerge.yml, so a
# standard PR whose required checks go green first CAN merge before this
# lane finishes. Findings then land on an already-merged PR — useful as
# follow-up signal, but it does not prevent the #1270 class outright.
# (Codex review round 3 raised exactly this; recorded rather than
# silently overclaimed.)
#
# Making it blocking is a deliberate fleet operation, not a flag here:
# a required check must report on EVERY PR or it bricks them on
# "Expected — Waiting for status to be reported" forever. Since neither
# lane runs for risk:blocked or draft PRs, that needs an always()-gated
# summary job over both lanes (accepting `skipped` only with classifier
# proof) promoted to the required-checks ruleset in each caller repo.
# That is a per-repo ruleset change across 40+ repos with a real
# brick-the-fleet failure mode — an operator decision, deliberately not
# taken as a side effect of adding coverage.
review_standard:
needs: classify
if: needs.classify.outputs.risk_class == 'standard'
uses: topcoder1/ci-workflows/.github/workflows/codex-review.yml@main
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
Loading
Loading