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