From a73e60e99db78c87d85c688d03e65c3c3d5a940c Mon Sep 17 00:00:00 2001 From: Ricardo Frantz <4220623+ricardofrantz@users.noreply.github.com> Date: Sat, 14 Nov 2026 00:22:39 +0100 Subject: [PATCH 1/5] ci: review-gate, Scorecard, Dependabot cooldown and CodeRabbit inheritance Changes: - review-gate.yml (waits for the CodeRabbit and cubic check runs on the head commit; fails if cubic did not complete or CodeRabbit was off or errored; warns when CodeRabbit is rate limited or skips a later push after a finished review) and scorecard.yml (OpenSSF Scorecard, weekly, SARIF upload); permissions are least-privilege and every action is pinned to a full commit SHA, same files as machine-made/pi-effort --- .github/workflows/review-gate.yml | 76 +++++++++++++++++++++++++++++++ .github/workflows/scorecard.yml | 42 +++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 .github/workflows/review-gate.yml create mode 100644 .github/workflows/scorecard.yml diff --git a/.github/workflows/review-gate.yml b/.github/workflows/review-gate.yml new file mode 100644 index 0000000..767f7f4 --- /dev/null +++ b/.github/workflows/review-gate.yml @@ -0,0 +1,76 @@ +# Fails when cubic did not complete a review, or when CodeRabbit was off or errored. +# Passes with a warning when CodeRabbit is only rate limited. +# Reads check runs only. Check titles are matched as text: update them here +# when a vendor renames one. +name: review-gate +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] +permissions: + checks: read + pull-requests: read # list the PR commits +jobs: + review-gate: + # Bot-authored dependency PRs and draft PRs: the job is skipped, which passes a required check. + if: >- + github.event.pull_request.user.login != 'dependabot[bot]' && + github.event.pull_request.user.login != 'renovate[bot]' && + github.event.pull_request.draft == false + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Wait for both bots, then decide + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + SHA: ${{ github.event.pull_request.head.sha }} + PR: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + # Latest check run of one app on this commit, as JSON; "null" when none or on an API error. + latest() { + gh api "repos/$REPO/commits/$SHA/check-runs?app_id=$1&filter=latest&per_page=100" \ + --jq '[.check_runs[] | {id, status, conclusion, title: .output.title}] | max_by(.id)' \ + 2>/dev/null || echo null + } + # True when CodeRabbit finished a review on any commit of this PR. + cr_reviewed_before() { + local c + for c in $(gh api "repos/$REPO/pulls/$PR/commits?per_page=100" --jq '.[].sha' 2>/dev/null); do + [ "$(gh api "repos/$REPO/commits/$c/check-runs?app_id=347564&filter=latest" \ + --jq '[.check_runs[] | select(.output.title=="Finished" and .conclusion=="success")] | length' 2>/dev/null || echo 0)" != 0 ] && return 0 + done + return 1 + } + cr=null; cu=null + for i in $(seq 1 36); do # 36 x 30 s = 18 min + cr=$(latest 347564) # CodeRabbit app + cu=$(latest 1082092) # cubic app + [ "$(jq -r '.status' <<<"$cr")" = completed ] && [ "$(jq -r '.status' <<<"$cu")" = completed ] && break + sleep 30 + done + echo "CodeRabbit: $cr"; echo "cubic: $cu" + cr_title=$(jq -r 'if .status=="completed" then .title else "missing" end' <<<"$cr") + cr_concl=$(jq -r '.conclusion // "none"' <<<"$cr") + cu_title=$(jq -r 'if .status=="completed" then .title else "missing" end' <<<"$cu") + cu_concl=$(jq -r '.conclusion // "none"' <<<"$cu") + if [ "$cu_title" != "AI review completed" ] || [ "$cu_concl" != success ]; then + echo "::error::cubic did not complete a review: $cu_title ($cu_concl)"; exit 1 + fi + case "$cr_concl" in + failure|cancelled|timed_out|action_required|none) echo "::error::CodeRabbit check did not pass: $cr_title ($cr_concl)"; exit 1 ;; + esac + case "$cr_title" in + "Finished") ;; + "Review rate limited") echo "::warning::CodeRabbit rate limited; cubic reviewed." ;; + "Path filters excluded"*) echo "::notice::CodeRabbit: $cr_title" ;; + # CodeRabbit reviews the opening push only (auto_incremental_review false); cubic reviews every push. + # Accept the skip only when CodeRabbit finished a review on an earlier commit of this PR. + "Review skipped: incremental reviews are disabled") + if cr_reviewed_before; then + echo "::warning::CodeRabbit reviews the opening push only; comment '@coderabbitai review' for this commit." + else + echo "::error::CodeRabbit never finished a review on this PR; comment '@coderabbitai review'."; exit 1 + fi ;; + *) echo "::error::CodeRabbit did not review: $cr_title ($cr_concl)"; exit 1 ;; + esac diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..5083b25 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,42 @@ +# OpenSSF Scorecard: supply-chain checks, weekly and on branch-protection changes. +# Based on the template in https://github.com/ossf/scorecard-action +name: Scorecard supply-chain security +on: + branch_protection_rule: + schedule: + - cron: '17 4 * * 1' + push: + branches: [main] + workflow_dispatch: +permissions: + contents: read +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + security-events: write # upload SARIF to code scanning + id-token: write # publish results to the OpenSSF API + contents: read + actions: read + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Run analysis + uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + - name: Upload artifact + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + - name: Upload to code scanning + uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 + with: + sarif_file: results.sarif From 75988ef05485725a1964788112b3c898914d03a5 Mon Sep 17 00:00:00 2001 From: Ricardo Frantz <4220623+ricardofrantz@users.noreply.github.com> Date: Sat, 14 Nov 2026 00:32:35 +0100 Subject: [PATCH 2/5] ci: review-gate pages the PR commits, polls once more at the end, and requires success for Finished Commit listing follows pagination; the last poll no longer sleeps; a Finished title needs a success conclusion and stale is rejected; gh errors reach the log. --- .github/workflows/review-gate.yml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/review-gate.yml b/.github/workflows/review-gate.yml index 767f7f4..8e3ebb1 100644 --- a/.github/workflows/review-gate.yml +++ b/.github/workflows/review-gate.yml @@ -27,27 +27,29 @@ jobs: PR: ${{ github.event.pull_request.number }} run: | set -euo pipefail - # Latest check run of one app on this commit, as JSON; "null" when none or on an API error. + # Latest check run of one app on this commit, as JSON; "null" when none or on an API error + # (gh prints the error text to the log). latest() { gh api "repos/$REPO/commits/$SHA/check-runs?app_id=$1&filter=latest&per_page=100" \ --jq '[.check_runs[] | {id, status, conclusion, title: .output.title}] | max_by(.id)' \ - 2>/dev/null || echo null + || echo null } - # True when CodeRabbit finished a review on any commit of this PR. + # True when CodeRabbit finished a review on any commit of this PR + # (the commits endpoint returns at most 250 commits). cr_reviewed_before() { local c - for c in $(gh api "repos/$REPO/pulls/$PR/commits?per_page=100" --jq '.[].sha' 2>/dev/null); do + for c in $(gh api --paginate "repos/$REPO/pulls/$PR/commits?per_page=100" --jq '.[].sha'); do [ "$(gh api "repos/$REPO/commits/$c/check-runs?app_id=347564&filter=latest" \ - --jq '[.check_runs[] | select(.output.title=="Finished" and .conclusion=="success")] | length' 2>/dev/null || echo 0)" != 0 ] && return 0 + --jq '[.check_runs[] | select(.output.title=="Finished" and .conclusion=="success")] | length' || echo 0)" != 0 ] && return 0 done return 1 } cr=null; cu=null - for i in $(seq 1 36); do # 36 x 30 s = 18 min + for i in $(seq 1 36); do # 36 polls, 30 s apart, the last at 17.5 min cr=$(latest 347564) # CodeRabbit app cu=$(latest 1082092) # cubic app [ "$(jq -r '.status' <<<"$cr")" = completed ] && [ "$(jq -r '.status' <<<"$cu")" = completed ] && break - sleep 30 + if [ "$i" -lt 36 ]; then sleep 30; fi done echo "CodeRabbit: $cr"; echo "cubic: $cu" cr_title=$(jq -r 'if .status=="completed" then .title else "missing" end' <<<"$cr") @@ -58,10 +60,11 @@ jobs: echo "::error::cubic did not complete a review: $cu_title ($cu_concl)"; exit 1 fi case "$cr_concl" in - failure|cancelled|timed_out|action_required|none) echo "::error::CodeRabbit check did not pass: $cr_title ($cr_concl)"; exit 1 ;; + failure|cancelled|timed_out|action_required|stale|none) echo "::error::CodeRabbit check did not pass: $cr_title ($cr_concl)"; exit 1 ;; esac case "$cr_title" in - "Finished") ;; + "Finished") + [ "$cr_concl" = success ] || { echo "::error::CodeRabbit finished without success ($cr_concl)"; exit 1; } ;; "Review rate limited") echo "::warning::CodeRabbit rate limited; cubic reviewed." ;; "Path filters excluded"*) echo "::notice::CodeRabbit: $cr_title" ;; # CodeRabbit reviews the opening push only (auto_incremental_review false); cubic reviews every push. From 33d585b9e5cfb3fe90e6da6d9e0df17aaac4c395 Mon Sep 17 00:00:00 2001 From: Ricardo Frantz <4220623+ricardofrantz@users.noreply.github.com> Date: Sat, 14 Nov 2026 00:43:45 +0100 Subject: [PATCH 3/5] ci: review-gate treats a failed history lookup as no earlier review gh api prints the HTTP error body to stdout, so the fallback value was appended to it and the count test could pass; the value is now replaced on failure. --- .github/workflows/review-gate.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/review-gate.yml b/.github/workflows/review-gate.yml index 8e3ebb1..f5949ed 100644 --- a/.github/workflows/review-gate.yml +++ b/.github/workflows/review-gate.yml @@ -27,20 +27,23 @@ jobs: PR: ${{ github.event.pull_request.number }} run: | set -euo pipefail - # Latest check run of one app on this commit, as JSON; "null" when none or on an API error - # (gh prints the error text to the log). + # Latest check run of one app on this commit, as JSON; "null" when none or on an API error. + # gh prints an HTTP error body to stdout, so on failure the value is replaced, not appended to; + # the error text on stderr stays in the log. latest() { - gh api "repos/$REPO/commits/$SHA/check-runs?app_id=$1&filter=latest&per_page=100" \ - --jq '[.check_runs[] | {id, status, conclusion, title: .output.title}] | max_by(.id)' \ - || echo null + local out + out=$(gh api "repos/$REPO/commits/$SHA/check-runs?app_id=$1&filter=latest&per_page=100" \ + --jq '[.check_runs[] | {id, status, conclusion, title: .output.title}] | max_by(.id)') || out=null + echo "$out" } # True when CodeRabbit finished a review on any commit of this PR # (the commits endpoint returns at most 250 commits). cr_reviewed_before() { - local c + local c n for c in $(gh api --paginate "repos/$REPO/pulls/$PR/commits?per_page=100" --jq '.[].sha'); do - [ "$(gh api "repos/$REPO/commits/$c/check-runs?app_id=347564&filter=latest" \ - --jq '[.check_runs[] | select(.output.title=="Finished" and .conclusion=="success")] | length' || echo 0)" != 0 ] && return 0 + n=$(gh api "repos/$REPO/commits/$c/check-runs?app_id=347564&filter=latest" \ + --jq '[.check_runs[] | select(.output.title=="Finished" and .conclusion=="success")] | length') || n=0 + [ "$n" != 0 ] && return 0 done return 1 } From 145d241a8f9f79100cd1fed79c457aa6e3241fb6 Mon Sep 17 00:00:00 2001 From: Ricardo Frantz <4220623+ricardofrantz@users.noreply.github.com> Date: Sat, 14 Nov 2026 01:20:23 +0100 Subject: [PATCH 4/5] ci: review-gate and Scorecard fixes from the bot reviews (copy of pi-effort 479a885) Commit listing paginated, gh error bodies no longer pass the review count, Finished needs success, CodeRabbit lookup filtered by check name, documented skip titles accepted, re-run hint, 15 min Scorecard timeout. --- .github/workflows/review-gate.yml | 15 ++++++++------- .github/workflows/scorecard.yml | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/review-gate.yml b/.github/workflows/review-gate.yml index f5949ed..2b39630 100644 --- a/.github/workflows/review-gate.yml +++ b/.github/workflows/review-gate.yml @@ -29,10 +29,11 @@ jobs: set -euo pipefail # Latest check run of one app on this commit, as JSON; "null" when none or on an API error. # gh prints an HTTP error body to stdout, so on failure the value is replaced, not appended to; - # the error text on stderr stays in the log. + # the error text on stderr stays in the log. $2 (optional) is the exact check name: + # CodeRabbit publishes other checks under the same app, e.g. CodeRabbit Security. latest() { local out - out=$(gh api "repos/$REPO/commits/$SHA/check-runs?app_id=$1&filter=latest&per_page=100" \ + out=$(gh api "repos/$REPO/commits/$SHA/check-runs?app_id=$1${2:+&check_name=$2}&filter=latest&per_page=100" \ --jq '[.check_runs[] | {id, status, conclusion, title: .output.title}] | max_by(.id)') || out=null echo "$out" } @@ -41,7 +42,7 @@ jobs: cr_reviewed_before() { local c n for c in $(gh api --paginate "repos/$REPO/pulls/$PR/commits?per_page=100" --jq '.[].sha'); do - n=$(gh api "repos/$REPO/commits/$c/check-runs?app_id=347564&filter=latest" \ + n=$(gh api "repos/$REPO/commits/$c/check-runs?app_id=347564&check_name=CodeRabbit&filter=latest" \ --jq '[.check_runs[] | select(.output.title=="Finished" and .conclusion=="success")] | length') || n=0 [ "$n" != 0 ] && return 0 done @@ -49,7 +50,7 @@ jobs: } cr=null; cu=null for i in $(seq 1 36); do # 36 polls, 30 s apart, the last at 17.5 min - cr=$(latest 347564) # CodeRabbit app + cr=$(latest 347564 CodeRabbit) # CodeRabbit app, review check cu=$(latest 1082092) # cubic app [ "$(jq -r '.status' <<<"$cr")" = completed ] && [ "$(jq -r '.status' <<<"$cu")" = completed ] && break if [ "$i" -lt 36 ]; then sleep 30; fi @@ -69,14 +70,14 @@ jobs: "Finished") [ "$cr_concl" = success ] || { echo "::error::CodeRabbit finished without success ($cr_concl)"; exit 1; } ;; "Review rate limited") echo "::warning::CodeRabbit rate limited; cubic reviewed." ;; - "Path filters excluded"*) echo "::notice::CodeRabbit: $cr_title" ;; + "Path filters excluded"*|"No files to review") echo "::notice::CodeRabbit: $cr_title" ;; # CodeRabbit reviews the opening push only (auto_incremental_review false); cubic reviews every push. # Accept the skip only when CodeRabbit finished a review on an earlier commit of this PR. - "Review skipped: incremental reviews are disabled") + "Review skipped: incremental reviews are disabled"|"Incremental review skipped") if cr_reviewed_before; then echo "::warning::CodeRabbit reviews the opening push only; comment '@coderabbitai review' for this commit." else - echo "::error::CodeRabbit never finished a review on this PR; comment '@coderabbitai review'."; exit 1 + echo "::error::CodeRabbit never finished a review on this PR; comment '@coderabbitai review', then re-run this job once that review has finished."; exit 1 fi ;; *) echo "::error::CodeRabbit did not review: $cr_title ($cr_concl)"; exit 1 ;; esac diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 5083b25..3f277aa 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -14,6 +14,7 @@ jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest + timeout-minutes: 15 permissions: security-events: write # upload SARIF to code scanning id-token: write # publish results to the OpenSSF API From 7ec0fc2938c40fa04caa8c63122f069b38f75bff Mon Sep 17 00:00:00 2001 From: Ricardo Frantz <4220623+ricardofrantz@users.noreply.github.com> Date: Sat, 14 Nov 2026 01:36:40 +0100 Subject: [PATCH 5/5] ci: review-gate and Scorecard fixes, round 2 (copy of pi-effort 73f514f) A failed commit listing gets its own error, a PR-scoped concurrency group cancels superseded gate runs, the historical lookup reads all check attempts, the failure message asks for a full review, and a manual Scorecard run on a non-default branch is skipped. --- .github/workflows/review-gate.yml | 17 ++++++++++++----- .github/workflows/scorecard.yml | 2 ++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/review-gate.yml b/.github/workflows/review-gate.yml index 2b39630..d2861f8 100644 --- a/.github/workflows/review-gate.yml +++ b/.github/workflows/review-gate.yml @@ -9,6 +9,10 @@ on: permissions: checks: read pull-requests: read # list the PR commits +# A newer push to the same PR cancels the run that still polls the older commit. +concurrency: + group: review-gate-${{ github.event.pull_request.number }} + cancel-in-progress: true jobs: review-gate: # Bot-authored dependency PRs and draft PRs: the job is skipped, which passes a required check. @@ -30,7 +34,7 @@ jobs: # Latest check run of one app on this commit, as JSON; "null" when none or on an API error. # gh prints an HTTP error body to stdout, so on failure the value is replaced, not appended to; # the error text on stderr stays in the log. $2 (optional) is the exact check name: - # CodeRabbit publishes other checks under the same app, e.g. CodeRabbit Security. + # CodeRabbit publishes other checks under the same app, for example CodeRabbit Security. latest() { local out out=$(gh api "repos/$REPO/commits/$SHA/check-runs?app_id=$1${2:+&check_name=$2}&filter=latest&per_page=100" \ @@ -40,9 +44,12 @@ jobs: # True when CodeRabbit finished a review on any commit of this PR # (the commits endpoint returns at most 250 commits). cr_reviewed_before() { - local c n - for c in $(gh api --paginate "repos/$REPO/pulls/$PR/commits?per_page=100" --jq '.[].sha'); do - n=$(gh api "repos/$REPO/commits/$c/check-runs?app_id=347564&check_name=CodeRabbit&filter=latest" \ + local shas c n + shas=$(gh api --paginate "repos/$REPO/pulls/$PR/commits?per_page=100" --jq '.[].sha') \ + || { echo "::error::could not list the PR commits"; exit 1; } + for c in $shas; do + # filter=all: a later failed attempt on a commit does not hide its finished review + n=$(gh api "repos/$REPO/commits/$c/check-runs?app_id=347564&check_name=CodeRabbit&filter=all&per_page=100" \ --jq '[.check_runs[] | select(.output.title=="Finished" and .conclusion=="success")] | length') || n=0 [ "$n" != 0 ] && return 0 done @@ -77,7 +84,7 @@ jobs: if cr_reviewed_before; then echo "::warning::CodeRabbit reviews the opening push only; comment '@coderabbitai review' for this commit." else - echo "::error::CodeRabbit never finished a review on this PR; comment '@coderabbitai review', then re-run this job once that review has finished."; exit 1 + echo "::error::CodeRabbit never finished a review on this PR; comment '@coderabbitai full review', then re-run this job once that review has finished."; exit 1 fi ;; *) echo "::error::CodeRabbit did not review: $cr_title ($cr_concl)"; exit 1 ;; esac diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3f277aa..e0d0239 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -15,6 +15,8 @@ jobs: name: Scorecard analysis runs-on: ubuntu-latest timeout-minutes: 15 + # Scorecard rejects a manual run on a non-default branch + if: github.event_name != 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch permissions: security-events: write # upload SARIF to code scanning id-token: write # publish results to the OpenSSF API