Skip to content
Closed
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
10 changes: 8 additions & 2 deletions .github/required-checks.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
#
# Ownership:
# ci.yml — Actionlint, ShellCheck, Lint Python (ruff), Hook Tests,
# Script Tests, llm-review, merge-pipeline-checks
# Script Tests, llm-review
# review-gate.yml — review-gate (the always-runs, fail-closed per-SHA
# coverage + dangling summary gate; story 3ee4 swapped
# it IN, replacing the no-op merge-pipeline-checks
# umbrella. merge-pipeline-checks the *job* is retained
# in ci.yml — it still fires on the sub-PR path — it is
# only removed from the MAIN ruleset required set here.)
# ticket-platform-matrix.yml — Ticket tests (linux-bash4), Ticket tests (macos-bash3),
# Ticket tests (alpine-busybox)

Expand All @@ -26,7 +32,7 @@ Lint Python (ruff)
Hook Tests
Script Tests
llm-review
merge-pipeline-checks
review-gate

# ── ticket-platform-matrix.yml checks ────────────────────────────────────────
Ticket tests (linux-bash4)
Expand Down
29 changes: 15 additions & 14 deletions .github/workflows/review-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ name: review-gate
# success only if the script decides it, never the GH scheduler. The job has NO
# code_changed/path skip: it runs on every base==main PR (DD1).
#
# ROLLOUT (current): MODE=warnit LOGS sub-check violations (::warning::) but does
# NOT block, and is NOT yet a required status check. It SHADOWS the existing
# advisory review-coverage-invariant / dangling-references workflows so warn-mode
# behavior can be compared against the real outcome on live PR2 traffic before
# cutover. GO-LIVE is the enforce-flip (story 3ee4), an admin step:
# 1. flip DSO_REVIEW_GATE_MODE to `enforce` below;
# 2. add `review-gate` to .github/required-checks.txt and REMOVE the superseded
# merge-pipeline-checks (+ the standalone coverage/dangling contexts);
# 3. provision `review-gate` into the live MAIN ruleset (admin token) — the W1
# round-trip drift test then keeps required-checks.txt == live in sync.
# Until step 2/3, this check is advisory (visible, non-blocking).
# ROLLOUT (current): MODE=enforcea sub-check violation or precondition (exit 78)
# makes the gate RED and blocks merge. The enforce-flip (story 3ee4) completed the
# three-step go-live:
# 1. DSO_REVIEW_GATE_MODE flipped to `enforce` below;
# 2. `review-gate` added to .github/required-checks.txt and the superseded
# merge-pipeline-checks removed from the MAIN ruleset required set (the
# standalone coverage/dangling contexts were never individually required — I-4);
# 3. `review-gate` provisioned into the live MAIN ruleset (admin token) — the W1
# round-trip drift test (R5/R8) keeps required-checks.txt == live in sync.
# The prior warn-mode shadow showed ZERO false BLOCKs on live PR2 traffic before
# this cutover.

on:
pull_request:
Expand Down Expand Up @@ -77,9 +77,10 @@ jobs:
# (Admin exemption is identity-based per ADR-0022: a covering PR merged by
# a designated bypass-actor — ruleset.bypass_user_ids — is reviewed-
# equivalent inside rc_sha_is_reviewed. No ledger env, no signing key.)
# ROLLOUT KNOB — flip to `enforce` at go-live (see header). This single
# knob is propagated to BOTH sub-checks by review-gate.sh.
DSO_REVIEW_GATE_MODE: warn
# ROLLOUT KNOB — flipped to `enforce` at go-live (story 3ee4). This single
# knob is propagated to BOTH sub-checks by review-gate.sh. In enforce mode
# a sub-check precondition (exit 78) or violation makes the gate RED.
DSO_REVIEW_GATE_MODE: enforce
run: |
# review-gate.sh maps each sub-check's precondition (exit 78) through
# precondition-gate.sh with the gate mode internally, so in warn mode the
Expand Down
2 changes: 1 addition & 1 deletion plugins/dso/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dso",
"version": "1.17.142",
"version": "1.17.143",
"description": "Workflow infrastructure plugin for Claude Code projects",
"commands": "./commands/",
"skills": "./skills/",
Expand Down
12 changes: 7 additions & 5 deletions plugins/dso/scripts/promote-ruleset-required.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,17 @@ NON_INTERACTIVE=0
# The check-contexts that this script manages, partitioned by target ruleset.
# Each ruleset gets only the checks that are emitted on PRs it covers:
#
# merge-pipeline-checks → fires on session→main PRs, belongs on the
# main-branch ruleset.
# review-sub-pr → fires only on PRs targeting sub-PR branches,
# belongs on the sub-PR ruleset.
# review-gate → the always-runs, fail-closed per-SHA coverage + dangling
# summary gate; fires on every base==main PR, belongs on the
# main-branch ruleset. Swapped IN for the no-op
# merge-pipeline-checks umbrella at the story-3ee4 enforce-flip.
# review-sub-pr → fires only on PRs targeting sub-PR branches, belongs on the
# sub-PR ruleset.
#
# Adding review-sub-pr to the main-branch ruleset would deadlock every PR
# to main (the check never reports on those PRs).
MAIN_STAGED_CHECKS=(
"merge-pipeline-checks"
"review-gate"
)
SUB_PR_STAGED_CHECKS=(
"review-sub-pr"
Expand Down
8 changes: 7 additions & 1 deletion plugins/dso/scripts/update-required-checks-manifest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ CHECKS_FILE="$REPO_ROOT/.github/required-checks.txt"
# ruleset would deadlock every PR to main. Enforcement of review-sub-pr lives
# on the sub-PR ruleset (16961402), populated by provision-ruleset.sh from
# the sub-PR branch-patterns source-of-truth file (config/sub-pr-branch-patterns.txt).
#
# review-gate (story 3ee4) is the always-runs, fail-closed per-SHA coverage +
# dangling summary gate that replaced the no-op merge-pipeline-checks umbrella on
# the MAIN ruleset. merge-pipeline-checks is intentionally NOT in this list: the
# job is retained in ci.yml (it still fires on the sub-PR path) but is no longer a
# required context on the main ruleset, so this bootstrap must not re-add it.
REQUIRED_ENTRIES=(
"merge-pipeline-checks"
"review-gate"
)

# Parse arguments
Expand Down
13 changes: 7 additions & 6 deletions tests/scripts/test-promote-ruleset-required.sh
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ assert_pass_if_clean "test_unknown_arg_exits_1"
_snapshot_fail
_dr_tmpdir="$(_make_tmpdir)"
mkdir -p "$_dr_tmpdir/.github"
printf 'review-sub-pr\nmerge-pipeline-checks\n' > "$_dr_tmpdir/.github/required-checks.txt"
printf 'review-sub-pr\nreview-gate\n' > "$_dr_tmpdir/.github/required-checks.txt"

# Minimal gh stub (only repo view for auto-detect; not used when --repo is explicit)
_gh_stub="$_dr_tmpdir/gh"
Expand All @@ -112,10 +112,11 @@ _dr_out="$(DSO_DRY_RUN=1 PATH="$_dr_tmpdir:$PATH" bash "$SCRIPT" \

assert_eq "test_dry_run_stage_outputs: exits 0" "0" "$_dr_exit"
# The observation-window ruleset targets main, so it stages only MAIN_STAGED_CHECKS
# (merge-pipeline-checks). review-sub-pr is staged into the sub-PR ruleset directly
# via --promote-to-required (no observation window for sub-PR checks).
assert_contains "test_dry_run_stage_outputs: mentions merge-pipeline-checks" \
"merge-pipeline-checks" "$_dr_out"
# (review-gate — swapped IN for merge-pipeline-checks at the story-3ee4 enforce-flip).
# review-sub-pr is staged into the sub-PR ruleset directly via --promote-to-required
# (no observation window for sub-PR checks).
assert_contains "test_dry_run_stage_outputs: mentions review-gate" \
"review-gate" "$_dr_out"
# Negative assertion: review-sub-pr must NOT appear in the main observation payload,
# or it will deadlock every PR to main once enforcement promotes.
_review_subpr_absent="yes"
Expand All @@ -135,7 +136,7 @@ assert_pass_if_clean "test_dry_run_stage_outputs"
_snapshot_fail
_dp_tmpdir="$(_make_tmpdir)"
mkdir -p "$_dp_tmpdir/.github"
printf 'review-sub-pr\nmerge-pipeline-checks\n' > "$_dp_tmpdir/.github/required-checks.txt"
printf 'review-sub-pr\nreview-gate\n' > "$_dp_tmpdir/.github/required-checks.txt"

# Write ruleset JSON fixtures to files (avoids heredoc variable-expansion issues)
_ruleset_list_file="$_dp_tmpdir/ruleset-list.json"
Expand Down
29 changes: 18 additions & 11 deletions tests/scripts/test-validate-required-checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,11 @@ assert_pass_if_clean "test_empty_workflows_dir_with_checks_exits_nonzero"

UPDATE_SCRIPT="$REPO_ROOT/plugins/dso/scripts/update-required-checks-manifest.sh"

# -- test_update_manifest_adds_merge_pipeline_checks --------------------------
# update-required-checks-manifest.sh adds 'merge-pipeline-checks' to the
# main-branch manifest when it is missing. review-sub-pr is intentionally NOT
# in REQUIRED_ENTRIES — see the rationale comment in
# -- test_update_manifest_adds_review_gate ------------------------------------
# update-required-checks-manifest.sh adds 'review-gate' to the main-branch
# manifest when it is missing (story 3ee4 swapped review-gate IN for the no-op
# merge-pipeline-checks umbrella). review-sub-pr is intentionally NOT in
# REQUIRED_ENTRIES — see the rationale comment in
# update-required-checks-manifest.sh (it fires only on PRs targeting sub-PR
# branches and would deadlock every PR to main if required there).
_snapshot_fail
Expand All @@ -192,20 +193,26 @@ EOF

rc6=0
bash "$UPDATE_SCRIPT" --checks-file "$MANIFEST6" 2>/dev/null || rc6=$?
assert_eq "test_update_manifest_adds_merge_pipeline_checks exit" "0" "$rc6"
assert_eq "test_update_manifest_adds_review_gate exit" "0" "$rc6"

# Verify 'merge-pipeline-checks' was added to the manifest.
# Verify 'review-gate' was added to the manifest.
added6=false
grep -q 'merge-pipeline-checks' "$MANIFEST6" 2>/dev/null && added6=true
assert_eq "test_update_manifest_adds_merge_pipeline_checks entry-present" "true" "$added6"
grep -qx 'review-gate' "$MANIFEST6" 2>/dev/null && added6=true
assert_eq "test_update_manifest_adds_review_gate entry-present" "true" "$added6"

# Negative assertion: the superseded merge-pipeline-checks must NOT be re-added
# by this script — it is no longer a required context on the main ruleset.
mpc_absent6=true
grep -qx 'merge-pipeline-checks' "$MANIFEST6" 2>/dev/null && mpc_absent6=false
assert_eq "test_update_manifest_adds_review_gate merge-pipeline-checks absent" "true" "$mpc_absent6"

# Negative assertion: review-sub-pr must NOT be added by this script — it
# belongs on the sub-PR ruleset, not the main-branch manifest.
not_added6=true
grep -q '^review-sub-pr$' "$MANIFEST6" 2>/dev/null && not_added6=false
assert_eq "test_update_manifest_adds_merge_pipeline_checks review-sub-pr absent" "true" "$not_added6"
assert_eq "test_update_manifest_adds_review_gate review-sub-pr absent" "true" "$not_added6"

assert_pass_if_clean "test_update_manifest_adds_merge_pipeline_checks"
assert_pass_if_clean "test_update_manifest_adds_review_gate"

# -- test_update_manifest_idempotent ------------------------------------------
# update-required-checks-manifest.sh is a no-op when both entries already
Expand All @@ -218,7 +225,7 @@ MANIFEST7="$(mktemp "$TMP_DIR7/required-checks.XXXXXX")"
cat > "$MANIFEST7" <<'EOF'
# pre-populated checks
review-sub-pr
merge-pipeline-checks
review-gate
ShellCheck
EOF

Expand Down
Loading