diff --git a/.github/workflows/generate-index.yml b/.github/workflows/generate-index.yml index 2f2d24a..167566f 100644 --- a/.github/workflows/generate-index.yml +++ b/.github/workflows/generate-index.yml @@ -1,6 +1,10 @@ name: Generate Protocol Index on: + # Tracking the generator at @main means the next run uses it — but a generator change upstream + # raises no event here, so nothing regenerates until a local change does. Run this by hand after a + # generator change that alters the output. + workflow_dispatch: push: branches: [main] # The index is a function of the protocols *and* of the generator version pinned below, so a @@ -9,16 +13,72 @@ on: - 'protocols/**' - '.github/workflows/generate-index.yml' +# The push uses the App installation token, not GITHUB_TOKEN, so GITHUB_TOKEN needs no write +# access. Granting it would hand every step a second write-capable credential for nothing. permissions: - contents: write + contents: read + +# Two merges in quick succession would otherwise run concurrently, and the second push would be +# rejected as non-fast-forward: its checkout predates the first run's index commit. Queue instead of +# cancelling — a cancelled run leaves the index describing the previous commit. +concurrency: + group: generate-index + cancel-in-progress: false jobs: generate-index: runs-on: ubuntu-latest + # The App credentials live in this environment, whose deployment branch policy allows only + # `main`. A pull request from a branch in this repository could otherwise read them — same-repo + # PRs do receive secrets — and they mint a token that bypasses branch protection. + environment: index-generation steps: + # The require-pr-review ruleset blocks github-actions[bot], and GitHub Actions itself cannot be + # granted a bypass: bypass actors of type Integration must be GitHub Apps installed on the + # organisation, and Actions is not one. A purpose-built App is — this one holds `contents: + # write` and nothing else, and its token expires after an hour. + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ secrets.INDEX_APP_ID }} + private-key: ${{ secrets.INDEX_APP_PRIVATE_KEY }} + # Request the scope explicitly rather than inheriting whatever the installation holds, so + # granting the App another permission later cannot silently widen this token. + permission-contents: write + - uses: actions/checkout@v4 - # @v0 is a moving tag, so this repository tracks the standard without a PR per release — - # which is the point: an index generated by a stale generator is an index that disagrees with - # the standard it claims to follow. Pin @v0.3.0 or a commit SHA instead if you need the action - # to be immutable, and accept updating it by hand. - - uses: waldronlab/agent-protocol-standard/actions/generate-index@v0 + with: + token: ${{ steps.app-token.outputs.token }} + # The branch tip, not the event SHA. Serialising runs is not enough on its own: a queued + # run would still check out the commit that triggered it and push a non-fast-forward once + # `main` had moved. It also means the index describes `main` as it is, not as it was. + ref: main + # The email prefix below is the bot's user id, which is NOT the App registration id held in + # INDEX_APP_ID. Getting it wrong still pushes, but GitHub does not link the commit to the bot. + - name: Resolve the App's bot user id + id: bot + env: + GH_TOKEN: ${{ github.token }} + SLUG: ${{ steps.app-token.outputs.app-slug }} + run: | + # Assigned separately, not interpolated into echo: `echo "id=$(gh api ...)"` exits 0 even + # when the lookup fails, writing an empty id, and attribution is then silently wrong. + set -euo pipefail + id="$(gh api "/users/${SLUG}[bot]" --jq .id)" + [ -n "$id" ] || { echo "::error::could not resolve the bot user id for ${SLUG}[bot]"; exit 1; } + echo "id=$id" >> "$GITHUB_OUTPUT" + + # @main, not a release tag. @v0 resolved to a commit from before the Python rewrite, so this + # workflow was silently running the old R generator while CI stayed green. A generator that + # has fallen behind produces an index that disagrees with the standard it claims to follow. + - uses: waldronlab/agent-protocol-standard/actions/generate-index@main + with: + # The checkout above is pinned to `main`, so the generator must build `protocol_url` + # values from `main` too, or a workflow_dispatch from another branch would index main's + # content under that ref's URLs. Passed as an input because GITHUB_REF_NAME is + # runner-provided and a workflow cannot reliably override it. + ref: main + # Attribute the commit to the App that pushes it, rather than to github-actions[bot], + # which no longer has anything to do with this push. + committer-name: ${{ steps.app-token.outputs.app-slug }}[bot] + committer-email: ${{ steps.bot.outputs.id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 34acf45..b6572e6 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -10,4 +10,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: waldronlab/agent-protocol-standard/actions/validate-protocols@v0 + # @main, not a release tag. @v0 predated the Python rewrite, so every pull request here was + # being checked by the old R validator — the Pydantic rules were enforcing nothing, silently. + - uses: waldronlab/agent-protocol-standard/actions/validate-protocols@main diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 862792a..629f897 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ End the file with a `## History & Reviews` section as demonstrated in the templa ## Step 4: Validate and Submit Open a pull request. **You can rely on CI for validation.** The repository's automated testing infrastructure will check your syntax and tell you precisely what is wrong. Focus on getting the scientific correctness right. -*(To run the validator locally before pushing, clone `waldronlab/agent-protocol-standard` beside this repository and, from here, run `Rscript ../agent-protocol-standard/scripts/validate-protocol.R protocols`. It takes the protocols directory, not a single file, and validates every protocol in it.)* +*(To run the validator locally before pushing, clone `waldronlab/agent-protocol-standard` beside this repository, install its dependencies with `pip install 'pydantic>=2.0.0' pyyaml`, and from here run `python3 ../agent-protocol-standard/scripts/validate_protocol.py protocols`. It takes the protocols directory, not a single file, and validates every protocol in it.)* ---