From dcbe583a4bf62d8aefd6fa25a26132311422ceb3 Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 17 Oct 2026 13:18:59 -0400 Subject: [PATCH] ci: publish the README through the required validate check Deploy keys are disabled for this org and GitHub Actions cannot be a ruleset bypass actor, so main is protected by requiring a passing validate check on every commit. The README workflow stages its regenerated commit on bot/readme-, dispatches validate there (allowed to touch generated files only for that bot branch), waits for success and fast-forwards main. Adds a catalog policy link to the README header and documents the branch protection. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/generate-readme.yml | 36 +++++++++++++++++++++++---- .github/workflows/validate-pr.yml | 9 ++++++- docs/policy.md | 6 ++++- scripts/check-pr.js | 12 ++++++--- templates/README.template.md | 2 ++ 5 files changed, 54 insertions(+), 11 deletions(-) diff --git a/.github/workflows/generate-readme.yml b/.github/workflows/generate-readme.yml index ed16ca3..37da53a 100644 --- a/.github/workflows/generate-readme.yml +++ b/.github/workflows/generate-readme.yml @@ -10,8 +10,12 @@ on: - 'scripts/**' workflow_dispatch: +# main requires a passing "validate" check on every commit, so the regenerated README is first pushed +# to a bot/readme-* branch, validated there, and then fast-forwarded onto main. permissions: contents: write + actions: write + checks: read concurrency: group: auto-generate-readme-main @@ -39,7 +43,9 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts - - name: Generate and push README/registry + - name: Generate, validate and publish README/registry + env: + GH_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -62,14 +68,34 @@ jobs: git add README.md dist/registry.json git commit -m "docs: auto-regenerate README and registry data" + sha="$(git rev-parse HEAD)" + branch="bot/readme-${sha:0:12}" - if git push origin HEAD:main; then + # Stage the commit and run the required "validate" check on it + git push --force origin "HEAD:refs/heads/${branch}" + gh workflow run validate-pr.yml --ref "${branch}" + conclusion="" + for i in $(seq 1 60); do + sleep 10 + conclusion="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${sha}/check-runs?check_name=validate" \ + --jq '[.check_runs[] | select(.status == "completed") | .conclusion][0] // ""')" + [ -n "${conclusion}" ] && break + done + + if [ "${conclusion}" != "success" ]; then + git push origin --delete "${branch}" || true + echo "validate on ${sha} finished with '${conclusion:-timeout}'; not publishing" >&2 + exit 1 + fi + + if git push origin "${sha}:refs/heads/main"; then + git push origin --delete "${branch}" || true exit 0 fi - echo "Push raced with another main update; retrying..." - sleep 2 + git push origin --delete "${branch}" || true + echo "main moved while validating; retrying..." done - echo "Failed to push regenerated README/registry after retries" >&2 + echo "Failed to publish regenerated README/registry after retries" >&2 exit 1 diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index a34065b..cb6aaa9 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -41,7 +41,14 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} BASE_REF: ${{ github.event.pull_request.base.sha || 'origin/main' }} - run: node scripts/check-pr.js --base "$BASE_REF" + # The README bot stages its regenerated README on bot/readme-* and dispatches this check + ALLOW_GENERATED: ${{ github.event_name == 'workflow_dispatch' && github.actor == 'github-actions[bot]' && startsWith(github.ref_name, 'bot/readme-') }} + run: | + if [ "$ALLOW_GENERATED" = "true" ]; then + node scripts/check-pr.js --base "$BASE_REF" --allow-generated + else + node scripts/check-pr.js --base "$BASE_REF" + fi - name: README and registry still generate run: | diff --git a/docs/policy.md b/docs/policy.md index db24823..809b2e6 100644 --- a/docs/policy.md +++ b/docs/policy.md @@ -49,13 +49,17 @@ Removal PRs are reviewed like any other PR; closing one keeps the entries until A weekly scout searches npm for packages published since V2 shipped that depend on `@opencode/*`, keeps public, licensed, non-archived, non-fork repositories that are not already listed or in an open PR, and ranks them by weekly downloads, then stars. It posts a **Scout** issue; a maintainer ticking a candidate's box makes the bot open a PR for it. Add a package or repo to [`discovery/declined.txt`](../discovery/declined.txt) to stop suggesting it. +## Branch protection + +`main` has a ruleset: every commit that lands on it must have a passing `validate` check, and force-pushes and deletion are blocked. Contributors and maintainers get the check through pull requests; bots get it by dispatching `validate` on their branch. Repository admins can merge a PR whose check is failing or pending (for a broken CI), but cannot push unvalidated commits directly. + ## Automation map | Workflow | When | What | | :--- | :--- | :--- | | `validate-pr.yml` | every PR | Required `validate` check | | `triage.yml` | PR opened/updated | Labels and a results comment (base-branch code only) | -| `generate-readme.yml` | push to main | README.md and dist/registry.json; fails without writing on any invalid entry | +| `generate-readme.yml` | push to main | README.md and dist/registry.json; fails without writing on any invalid entry. The regenerated commit is staged on `bot/readme-*`, validated, then fast-forwarded onto main | | `site.yml` | push to main, daily | Search site on GitHub Pages | | `health.yml` | daily | Health report issue, rename and removal PRs | | `reprobe.yml` | daily | Re-checks `needs-v2` PRs | diff --git a/scripts/check-pr.js b/scripts/check-pr.js index 363b6eb..d19b4d0 100644 --- a/scripts/check-pr.js +++ b/scripts/check-pr.js @@ -1,7 +1,9 @@ #!/usr/bin/env node /** * The required "validate" check for pull requests. - * Usage: node scripts/check-pr.js --base [--no-probe] + * Usage: node scripts/check-pr.js --base [--no-probe] [--allow-generated] + * + * --allow-generated is passed only for the README bot's own staged commits (see generate-readme.yml). * * Compares HEAD against the merge base with , analyzes the changed files, writes a report to * $GITHUB_STEP_SUMMARY (when set) and exits 1 on any blocking problem. @@ -11,7 +13,7 @@ const fs = require('fs'); const path = require('path'); const { execFileSync } = require('child_process'); const { loadCatalog } = require('./utils/catalog'); -const { DATA_FILE_RE, analyzePullRequest, formatReport } = require('./utils/pr-checks'); +const { DATA_FILE_RE, GENERATED_FILES, analyzePullRequest, formatReport } = require('./utils/pr-checks'); const ROOT = path.join(__dirname, '..'); const git = (...args) => execFileSync('git', args, { cwd: ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }); @@ -20,7 +22,8 @@ function parseArgs(argv) { const baseIndex = argv.indexOf('--base'); return { base: baseIndex !== -1 ? argv[baseIndex + 1] : 'origin/main', - probe: !argv.includes('--no-probe') + probe: !argv.includes('--no-probe'), + allowGenerated: argv.includes('--allow-generated') }; } @@ -42,7 +45,7 @@ function changedFiles(mergeBase) { } async function main() { - const { base, probe } = parseArgs(process.argv.slice(2)); + const { base, probe, allowGenerated } = parseArgs(process.argv.slice(2)); const mergeBase = git('merge-base', base, 'HEAD').trim(); const changes = changedFiles(mergeBase); @@ -61,6 +64,7 @@ async function main() { const baseEntries = entries.filter(e => !changedPaths.has(e.file)); const result = await analyzePullRequest({ changes, baseEntries, probe }); + if (allowGenerated) result.errors = result.errors.filter(e => !GENERATED_FILES.includes(e.file)); // Unparseable files anywhere in the catalog break README generation, so they always block for (const err of loadErrors) { if (!result.errors.some(e => e.file === err.file)) result.errors.push({ ...err, kind: 'metadata' }); diff --git a/templates/README.template.md b/templates/README.template.md index e87bd48..40fde77 100644 --- a/templates/README.template.md +++ b/templates/README.template.md @@ -45,6 +45,8 @@ 🔎 Search & filter the catalog  â€¢  âž• Submit an entry + â€¢  +📋 Catalog policy