fix(ci): route critical supply-chain findings through review gate (#68833)

Let the scanner report critical findings without failing. The review-label
gate owns the action-required status and blocking result, allowing the
ci-reviewed label rerun to clear both CI and the PR comment.
This commit is contained in:
ethernet
2026-07-21 14:51:26 -04:00
committed by GitHub
parent a88512b114
commit 4dd535c8ea
4 changed files with 53 additions and 39 deletions
+3 -2
View File
@@ -158,12 +158,13 @@ jobs:
review-labels:
name: Review label gate
needs: detect
if: needs.detect.outputs.event_name == 'pull_request' && (needs.detect.outputs.ci_review == 'true' || needs.detect.outputs.mcp_catalog == 'true')
needs: [detect, supply-chain]
if: always() && needs.detect.outputs.event_name == 'pull_request' && (needs.detect.outputs.ci_review == 'true' || needs.detect.outputs.mcp_catalog == 'true' || needs.supply-chain.outputs.critical_findings == 'true')
uses: ./.github/workflows/review-labels.yml
with:
ci_review: ${{ needs.detect.outputs.ci_review == 'true' }}
mcp_catalog: ${{ needs.detect.outputs.mcp_catalog == 'true' }}
supply_chain: ${{ needs.supply-chain.outputs.critical_findings == 'true' }}
secrets: inherit
osv-scanner:
+12 -6
View File
@@ -27,6 +27,10 @@ on:
description: Whether the MCP catalog / installer changed.
type: boolean
default: false
supply_chain:
description: Whether the critical supply-chain scan found a risk requiring review.
type: boolean
default: false
outputs:
ci_reviewed:
description: Whether the ci-reviewed label is present. Empty when neither input was true.
@@ -42,7 +46,7 @@ permissions:
jobs:
check:
name: Review label gate
if: inputs.ci_review || inputs.mcp_catalog
if: inputs.ci_review || inputs.mcp_catalog || inputs.supply_chain
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
@@ -75,15 +79,17 @@ jobs:
env:
CI_REVIEW: ${{ inputs.ci_review }}
MCP_CATALOG: ${{ inputs.mcp_catalog }}
SUPPLY_CHAIN: ${{ inputs.supply_chain }}
LABEL_PRESENT: ${{ steps.label-check.outputs.ci_reviewed }}
run: |
set -euo pipefail
ARGS=""
if [ "$CI_REVIEW" = "true" ]; then ARGS="$ARGS --ci-review"; fi
if [ "$MCP_CATALOG" = "true" ]; then ARGS="$ARGS --mcp-catalog"; fi
if [ "$LABEL_PRESENT" = "true" ]; then ARGS="$ARGS --label-present"; fi
args=()
if [ "$CI_REVIEW" = "true" ]; then args+=(--ci-review); fi
if [ "$MCP_CATALOG" = "true" ]; then args+=(--mcp-catalog); fi
if [ "$SUPPLY_CHAIN" = "true" ]; then args+=(--supply-chain); fi
if [ "$LABEL_PRESENT" = "true" ]; then args+=(--label-present); fi
python3 scripts/ci/emit_review_status.py $ARGS --output "$GITHUB_OUTPUT"
python3 scripts/ci/emit_review_status.py "${args[@]}" --output "$GITHUB_OUTPUT"
- name: Fail on missing label
if: steps.label-check.outputs.ci_reviewed != 'true'
+17 -27
View File
@@ -16,11 +16,12 @@ name: Supply Chain Audit
# ``review-labels.yml`` so it can be rerun independently.
#
# Outputs:
# review_status — JSON array of status objects consumed by the review
# comment assembler (scripts/ci/assemble_review_comment.py).
# Each job (``scan``, ``dep-bounds``) emits its own
# array; an ``aggregate`` job merges them into the
# workflow-level output.
# review_status — JSON array of status objects consumed by the review
# comment assembler (scripts/ci/assemble_review_comment.py).
# critical_findings — "true" when the narrow critical-pattern scan found
# something. The review-label gate consumes this and
# owns the action-required result, so adding
# ``ci-reviewed`` can heal the run on rerun.
on:
workflow_call:
@@ -41,6 +42,9 @@ on:
review_status:
description: JSON array of review status objects for the review comment assembler.
value: ${{ jobs.aggregate.outputs.review_status }}
critical_findings:
description: Whether the critical-pattern scan found a risk requiring maintainer review.
value: ${{ jobs.aggregate.outputs.critical_findings }}
permissions:
pull-requests: write
@@ -54,6 +58,7 @@ jobs:
timeout-minutes: 15
outputs:
review_status: ${{ steps.emit-status.outputs.review_status }}
critical_findings: ${{ steps.scan.outputs.found }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -165,33 +170,15 @@ jobs:
python3 - <<'PYEOF'
import json, os
found = os.environ.get("FOUND", "") == "true"
if found:
with open("/tmp/findings.md", encoding="utf-8") as f:
detail = f.read()
status = [{
"source": "supply chain",
"results": [{
"kind": "error",
"title": "Critical supply chain risk",
"summary": "Critical supply chain risk patterns detected in this PR.",
"detail": detail,
"how_to_fix": "Review the flagged code carefully. If intentional, add the `ci-reviewed` label."
}]
}]
else:
status = []
# The review-label gate renders and blocks critical findings. Keep
# this scan a fact-finder so adding ci-reviewed can rerun the gate
# without requiring the scanner itself to fail again.
status = []
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
f.write(f"review_status={json.dumps(status)}\n")
PYEOF
- name: Fail on critical findings
if: steps.scan.outputs.found == 'true'
run: |
echo "::error::CRITICAL supply chain risk patterns detected in this PR. See the review comment for details."
exit 1
dep-bounds:
name: Check PyPI dependency upper bounds
@@ -279,12 +266,14 @@ jobs:
timeout-minutes: 15
outputs:
review_status: ${{ steps.merge.outputs.review_status }}
critical_findings: ${{ steps.merge.outputs.critical_findings }}
steps:
- name: Merge review statuses
id: merge
env:
SCAN_STATUS: ${{ needs.scan.outputs.review_status }}
DEP_STATUS: ${{ needs.dep-bounds.outputs.review_status }}
CRITICAL_FINDINGS: ${{ needs.scan.outputs.critical_findings }}
run: |
python3 - <<'PYEOF'
import json, os
@@ -303,4 +292,5 @@ jobs:
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
f.write(f"review_status={json.dumps(merged)}\n")
f.write("critical_findings=" + os.environ.get("CRITICAL_FINDINGS", "false") + "\n")
PYEOF
+21 -4
View File
@@ -18,8 +18,8 @@ The ``source`` field is the workflow name that declared the status; the
assembler uses it to exclude the corresponding job from the synthesized
❌ Error list (the job already has its own status section).
The array can contain 0, 1, or 2 results — one per lane that ran
(``ci_review``, ``mcp_catalog``). When the ``ci-reviewed`` label is
The array can contain 0 to 3 results — one per lane that ran
(``ci_review``, ``mcp_catalog``, ``supply_chain``). When the ``ci-reviewed`` label is
present, the kind is ``info``; when missing, it's ``action_required``
with the verification checklist.
"""
@@ -44,6 +44,7 @@ SOURCE = "review-label-gate"
def build_results(
ci_review: bool,
mcp_catalog: bool,
supply_chain: bool,
label_present: bool,
) -> list[dict]:
"""Build the list of result objects for this source."""
@@ -99,16 +100,28 @@ def build_results(
),
})
if supply_chain and not label_present:
results.append({
"kind": "action_required",
"title": "Critical supply chain risk",
"summary": "Critical supply chain risk patterns were detected in this PR.",
"how_to_fix": (
"Review the flagged code carefully. If it is intentional, add the "
"`ci-reviewed` label to confirm maintainer review."
),
})
return results
def build_statuses(
ci_review: bool,
mcp_catalog: bool,
supply_chain: bool,
label_present: bool,
) -> list[dict]:
"""Build the full review_status array (one entry with a results list)."""
results = build_results(ci_review, mcp_catalog, label_present)
results = build_results(ci_review, mcp_catalog, supply_chain, label_present)
if not results:
return []
return [{"source": SOURCE, "results": results}]
@@ -120,13 +133,17 @@ def main() -> int:
help="Whether CI-sensitive files changed.")
parser.add_argument("--mcp-catalog", action="store_true",
help="Whether the MCP catalog / installer changed.")
parser.add_argument("--supply-chain", action="store_true",
help="Whether the critical supply-chain scanner found a risk.")
parser.add_argument("--label-present", action="store_true",
help="Whether the ci-reviewed label is present.")
parser.add_argument("--output", default="-",
help="Output file ('-' for stdout, or a GITHUB_OUTPUT path).")
args = parser.parse_args()
statuses = build_statuses(args.ci_review, args.mcp_catalog, args.label_present)
statuses = build_statuses(
args.ci_review, args.mcp_catalog, args.supply_chain, args.label_present
)
json_str = json.dumps(statuses)
if args.output == "-":