|
| 1 | +name: Keep AGENTS.md accurate (reusable) |
| 2 | + |
| 3 | +# If a PR makes AGENTS.md stale, the bot commits the fix to that PR's branch and comments what |
| 4 | +# it changed. Order: shell gate -> agent edits the file -> commit -> comment. |
| 5 | +# |
| 6 | +# This is a recommended way to use it |
| 7 | +# |
| 8 | +# on: |
| 9 | +# pull_request: |
| 10 | +# types: [opened, synchronize, reopened, ready_for_review] |
| 11 | +# |
| 12 | +# permissions: |
| 13 | +# contents: write |
| 14 | +# pull-requests: write |
| 15 | +# |
| 16 | +# jobs: |
| 17 | +# update-agents-md: |
| 18 | +# uses: apify/workflows/.github/workflows/agents-md-maintenance.yml@main |
| 19 | +# secrets: |
| 20 | +# ANTHROPIC_API_KEY: ${{ secrets.YOUR_ANTHROPIC_API_KEY }} |
| 21 | +# |
| 22 | +# The repo must keep the doc in AGENTS.md, with CLAUDE.md a regular file whose first line is |
| 23 | +# `@AGENTS.md`. Anything else fails the run — see the Gate step. |
| 24 | + |
| 25 | +on: |
| 26 | + workflow_call: |
| 27 | + secrets: |
| 28 | + ANTHROPIC_API_KEY: |
| 29 | + required: true |
| 30 | + |
| 31 | +permissions: |
| 32 | + contents: write |
| 33 | + pull-requests: write |
| 34 | + |
| 35 | +jobs: |
| 36 | + update: |
| 37 | + name: Update AGENTS.md |
| 38 | + # Skip drafts, forks, bot PRs, and our own commits (otherwise we'd loop). |
| 39 | + if: >- |
| 40 | + github.event_name == 'pull_request' && |
| 41 | + github.event.pull_request.draft == false && |
| 42 | + github.event.pull_request.head.repo.full_name == github.repository && |
| 43 | + !endsWith(github.event.pull_request.user.login, '[bot]') && |
| 44 | + github.actor != 'github-actions[bot]' |
| 45 | + # Must stay job-level: at workflow level the group is claimed before `if` is evaluated, so a |
| 46 | + # run that ends up skipped would cancel a live one. |
| 47 | + concurrency: |
| 48 | + group: agents-md-${{ github.event.pull_request.number }} |
| 49 | + cancel-in-progress: true |
| 50 | + runs-on: ubuntu-latest |
| 51 | + timeout-minutes: 15 |
| 52 | + steps: |
| 53 | + - name: Checkout the PR branch |
| 54 | + uses: actions/checkout@v6 |
| 55 | + with: |
| 56 | + ref: ${{ github.event.pull_request.head.ref }} |
| 57 | + fetch-depth: 1 |
| 58 | + token: ${{ github.token }} |
| 59 | + |
| 60 | + - name: Gate |
| 61 | + id: gate |
| 62 | + env: |
| 63 | + HEAD_SHA: ${{ github.event.pull_request.head.sha }} |
| 64 | + run: | |
| 65 | + set -euo pipefail |
| 66 | +
|
| 67 | + # The branch may have moved since this run started — the newer push has its own run. |
| 68 | + if [ "$(git rev-parse HEAD)" != "$HEAD_SHA" ]; then |
| 69 | + echo "Branch moved on since this run started -> leaving it to the newer run." |
| 70 | + echo "run=false" >> "$GITHUB_OUTPUT"; exit 0 |
| 71 | + fi |
| 72 | +
|
| 73 | + # checks the correct layout |
| 74 | + layout_ok=true |
| 75 | + if [ ! -f AGENTS.md ] || [ -L AGENTS.md ] || [ ! -s AGENTS.md ]; then |
| 76 | + echo "::error::AGENTS.md must be a regular, non-empty file in the repo root, holding the doc." |
| 77 | + layout_ok=false |
| 78 | + fi |
| 79 | + if [ ! -f CLAUDE.md ] || [ -L CLAUDE.md ] \ |
| 80 | + || [ "$(head -n 1 CLAUDE.md | tr -d '\r')" != "@AGENTS.md" ]; then |
| 81 | + echo "::error::CLAUDE.md must be a regular file whose first line is exactly '@AGENTS.md'." |
| 82 | + layout_ok=false |
| 83 | + fi |
| 84 | + if [ "$layout_ok" != true ]; then |
| 85 | + echo "::error::Required layout: AGENTS.md a regular file holding the doc, and CLAUDE.md a" |
| 86 | + echo "::error::regular file whose first line is exactly '@AGENTS.md'." |
| 87 | + exit 1 |
| 88 | + fi |
| 89 | +
|
| 90 | + # Stop if the newest commit is from the bot. Otherwise the bot reacts to itself forever. |
| 91 | + if [ "$(git log -1 --format='%an')" = "github-actions[bot]" ]; then |
| 92 | + echo "Head commit is our own doc commit -> nothing to do." |
| 93 | + echo "run=false" >> "$GITHUB_OUTPUT"; exit 0 |
| 94 | + fi |
| 95 | +
|
| 96 | + echo "run=true" >> "$GITHUB_OUTPUT" |
| 97 | +
|
| 98 | + - name: Update AGENTS.md if this PR made it wrong |
| 99 | + id: agent |
| 100 | + if: steps.gate.outputs.run == 'true' |
| 101 | + uses: anthropics/claude-code-action@v1 |
| 102 | + env: |
| 103 | + GH_TOKEN: ${{ github.token }} |
| 104 | + with: |
| 105 | + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} |
| 106 | + github_token: ${{ github.token }} |
| 107 | + show_full_output: true |
| 108 | + prompt: | |
| 109 | + You are in a GitHub Actions workflow on pull request #${{ github.event.pull_request.number }} in |
| 110 | + ${{ github.repository }}. Your job: if this PR makes `AGENTS.md` inaccurate, fix `AGENTS.md` on |
| 111 | + disk. You do NOT commit, push, or comment — later steps do that. Editing nothing is a valid and |
| 112 | + common outcome. |
| 113 | +
|
| 114 | + `AGENTS.md` in the repo root is the real instruction file. `CLAUDE.md` is a one-line |
| 115 | + `@AGENTS.md` pointer at it and must be left exactly as it is. |
| 116 | +
|
| 117 | + ## Steps |
| 118 | + 1. Read `AGENTS.md` in the repo root. |
| 119 | + 2. Read the diff: `gh pr diff ${{ github.event.pull_request.number }}`. Where the diff is |
| 120 | + ambiguous, read the changed files themselves. Never infer from filenames. |
| 121 | + The diff is cumulative for the whole PR, and it may already contain an earlier |
| 122 | + `docs: update AGENTS.md` commit of your own, or an edit the PR author made by hand. |
| 123 | + Judge the file **as it is on disk now** against the code **as it is now** — an earlier |
| 124 | + pass having fixed something does not mean a later push didn't break it again. |
| 125 | + 3. Decide whether, once this PR merges, `AGENTS.md` states something **wrong, missing, or |
| 126 | + misleading** for someone working in this repo. |
| 127 | + 4. If yes, edit `AGENTS.md`. If no, edit nothing. |
| 128 | + 5. **Always** write `/tmp/summary.md`, either way, in the format below. If you edited nothing, |
| 129 | + that file is the only record of it — someone must be able to audit the decision. |
| 130 | +
|
| 131 | + ## Stale means a concrete factual mismatch |
| 132 | + - a documented command, script, or path that no longer exists or was renamed |
| 133 | + - a new entry point, top-level directory, or dependency that changes how the project is built or run |
| 134 | + - a convention this PR establishes or abandons that `AGENTS.md` contradicts |
| 135 | + - a documented feature this PR removes |
| 136 | +
|
| 137 | + ## NOT stale — be strict, because you are editing someone's branch uninvited |
| 138 | + - pure refactors, internal renames, formatting |
| 139 | + - new tests, fixtures, or CI tweaks that don't change how a developer works |
| 140 | + - additions `AGENTS.md` already covers at the right level of abstraction |
| 141 | + - anything where you would be rewording rather than correcting |
| 142 | +
|
| 143 | + If you cannot name the specific line in `AGENTS.md` that becomes wrong, it is not stale: change |
| 144 | + nothing, and say so in the summary file. |
| 145 | +
|
| 146 | + ## Editing rules |
| 147 | + - **Surgical.** Change only what this PR made wrong. Leave every accurate section byte-for-byte |
| 148 | + alone. Do not reformat, reorder, or reword for taste. |
| 149 | + - **Deleting is editing too.** If this PR removes a documented command, path, or feature, remove |
| 150 | + the claim. Do not leave a corrected-but-still-wrong sentence behind, and do not describe |
| 151 | + something as removed — just stop describing it. |
| 152 | + - **Keep it under 200 lines by not adding bulk — never by deleting content this PR did not make |
| 153 | + wrong.** If the file is already over the limit, note that in the summary and leave it. Trimming a |
| 154 | + bloated `AGENTS.md` is a deliberate, reviewable cleanup of its own; smuggling it into an |
| 155 | + unrelated PR is how a one-line rename turns into a 186-line deletion nobody asked for. |
| 156 | + - Never document something you have not read. |
| 157 | + - **Never create `AGENTS.md`.** It is guaranteed to exist — the workflow fails the run before |
| 158 | + reaching you if it doesn't. If you cannot read it, stop and say so in the summary. |
| 159 | + - Touch `AGENTS.md` and `/tmp/summary.md` only. Not `CLAUDE.md`, not any other file. |
| 160 | +
|
| 161 | + ## /tmp/summary.md |
| 162 | + Write it like a note to a colleague: plain and short, no preamble. Markdown bullets, each one |
| 163 | + line and under 20 words. Paths and commands in backticks, never a whole bullet in backticks. |
| 164 | +
|
| 165 | + **If you edited the file** — one bullet per correction, and nothing else. No list of what you |
| 166 | + checked, no "left alone", no near-misses you decided against, no unrelated observations. One |
| 167 | + correction usually means one bullet: |
| 168 | +
|
| 169 | + - `pnpm build` no longer exists — `scripts/build.sh` is now `scripts/compile.sh` |
| 170 | + - added the new `src/api/` entry point to Repository structure |
| 171 | +
|
| 172 | + Say what you changed, not the state you left behind: "removed the appendix" discloses your edit, |
| 173 | + "the appendix is absent" hides it. If you deleted more than you added, say why in the first |
| 174 | + bullet. |
| 175 | +
|
| 176 | + **If you edited nothing** — one to three bullets naming the claims you checked that still hold, |
| 177 | + so the "nothing to do" is auditable: |
| 178 | +
|
| 179 | + - `src/index.js` is still the only entry point; this PR adds none |
| 180 | + - `scripts/build.sh` untouched, so the Build section holds |
| 181 | +
|
| 182 | + Never report `CLAUDE.md` showing as modified in the working tree. The workflow resets it before |
| 183 | + you start — that is expected, and not yours to mention. |
| 184 | + claude_args: | |
| 185 | + --max-turns 50 |
| 186 | + --allowedTools "Read,Glob,Grep,Edit,Write,WebFetch,WebSearch,Bash(gh pr diff:*),Bash(gh pr view:*)" |
| 187 | +
|
| 188 | + - name: Decide whether there is anything to commit |
| 189 | + id: prep |
| 190 | + # always(), so a failed or cancelled agent still gets its reasoning into the run summary; |
| 191 | + # the step bails below without committing. |
| 192 | + if: always() && steps.gate.outputs.run == 'true' |
| 193 | + env: |
| 194 | + AGENT_OUTCOME: ${{ steps.agent.outcome }} |
| 195 | + run: | |
| 196 | + set -euo pipefail |
| 197 | +
|
| 198 | + { |
| 199 | + echo "## AGENTS.md" |
| 200 | + echo |
| 201 | + cat /tmp/summary.md 2>/dev/null || echo "_The agent left no summary. Treat its silence with suspicion._" |
| 202 | + } >> "$GITHUB_STEP_SUMMARY" |
| 203 | +
|
| 204 | + if [ "$AGENT_OUTCOME" != "success" ]; then |
| 205 | + echo "::warning::The agent step ended as '$AGENT_OUTCOME'. Whatever is on disk may be" |
| 206 | + echo "::warning::half-written, so there is nothing safe to commit." |
| 207 | + echo "commit=false" >> "$GITHUB_OUTPUT"; exit 0 |
| 208 | + fi |
| 209 | +
|
| 210 | + if [ ! -f AGENTS.md ] || [ -L AGENTS.md ] || [ ! -s AGENTS.md ]; then |
| 211 | + echo "::error::AGENTS.md is missing, empty, or no longer a regular file after the agent" |
| 212 | + echo "::error::ran. Not committing that." |
| 213 | + echo "commit=false" >> "$GITHUB_OUTPUT"; exit 0 |
| 214 | + fi |
| 215 | +
|
| 216 | + if [ -z "$(git status --porcelain -- AGENTS.md)" ]; then |
| 217 | + echo "The agent left AGENTS.md unchanged -> nothing to commit." |
| 218 | + echo "Reasoning is in the run summary." |
| 219 | + echo "commit=false" >> "$GITHUB_OUTPUT"; exit 0 |
| 220 | + fi |
| 221 | +
|
| 222 | + git diff --stat -- AGENTS.md |
| 223 | + echo "commit=true" >> "$GITHUB_OUTPUT" |
| 224 | +
|
| 225 | + - name: Commit AGENTS.md |
| 226 | + id: signed |
| 227 | + if: steps.prep.outputs.commit == 'true' |
| 228 | + uses: apify/actions/signed-commit@v1.4.0 |
| 229 | + with: |
| 230 | + github-token: ${{ github.token }} |
| 231 | + message: "docs: update AGENTS.md for this PR" |
| 232 | + add: AGENTS.md |
| 233 | + |
| 234 | + - name: Comment on the PR |
| 235 | + if: steps.signed.outputs.committed == 'true' |
| 236 | + env: |
| 237 | + GH_TOKEN: ${{ github.token }} |
| 238 | + PR: ${{ github.event.pull_request.number }} |
| 239 | + run: | |
| 240 | + set -euo pipefail |
| 241 | + { |
| 242 | + echo "### AGENTS.md updated" |
| 243 | + echo |
| 244 | + if [ -f /tmp/summary.md ]; then cat /tmp/summary.md; else echo "See the commit for what changed."; fi |
| 245 | + echo |
| 246 | + echo "Committed to this PR. Review it like any other commit. Feel free to edit it." |
| 247 | + } > /tmp/body.md |
| 248 | + gh pr comment "$PR" --body-file /tmp/body.md |
0 commit comments