diff --git a/.github/workflows/review-gate.yml b/.github/workflows/review-gate.yml new file mode 100644 index 0000000..d2861f8 --- /dev/null +++ b/.github/workflows/review-gate.yml @@ -0,0 +1,90 @@ +# 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 +# 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. + 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. + # 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, 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" \ + --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 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 + return 1 + } + 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) # 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 + 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|stale|none) echo "::error::CodeRabbit check did not pass: $cr_title ($cr_concl)"; exit 1 ;; + esac + case "$cr_title" in + "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"*|"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"|"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 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 new file mode 100644 index 0000000..e0d0239 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,45 @@ +# 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 + 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 + 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