Skip to content

Commit a333e4d

Browse files
FilipMasarclaude
andauthored
feat: add reusable workflow that keeps AGENTS.md accurate in the PR that changed it (#311)
Fixes apify/integrations-team#98 ## What A new reusable workflow that keeps `AGENTS.md` accurate. When a PR makes `AGENTS.md` wrong, a bot commits the fix **to that PR's own branch** and comments what it changed. ## Why We already have `claude-md-maintenance.yml`, which runs after merge and opens its own PR. Nobody merges those PRs, so the doc goes stale anyway. Fixing the doc in the PR that broke it means the change lands together with the code. ## How it works 1. Someone opens or updates a PR. 2. A shell gate decides whether to run. It skips drafts, forks, bot commits, and PRs where the branch has already moved on. If the repo doesn't use the standard layout, the run **fails** and the error says how to fix it. 3. Claude reads what the PR changed and compares it against the current `AGENTS.md`. It edits the file **only** if the PR made something in it wrong — a renamed script, a removed dependency, a new top-level directory. 4. If the file changed, the bot commits it to the PR branch (signed) and posts a comment saying what it changed and why. 5. If nothing was wrong, it does nothing. Its reasoning still goes into the run summary, so a quiet run is not a silent one. The agent only reads and writes files. Committing and commenting are ordinary steps after it finishes, so when something breaks it's clear which part did it. ## How to use it Nothing happens until a repo opts in — this is `workflow_call` only. Onboarding is two steps. **1. The layout.** Two regular files in the repo root, neither a symlink. `AGENTS.md` — the whole doc `CLAUDE.md` — first line is a pointer to `AGENTS.md` ```markdown @AGENTS.md ``` **2. The workflow file.** ```yaml name: Keep AGENTS.md accurate on: pull_request: types: [opened, synchronize, reopened, ready_for_review] permissions: contents: write pull-requests: write jobs: update-agents-md: uses: apify/workflows/.github/workflows/agents-md-maintenance.yml@main secrets: ANTHROPIC_API_KEY: ${{ secrets.YOUR_ANTHROPIC_API_KEY }} ``` For our teams integration repos, both steps are in apify/integrations-team#113 Tested various scenarios in a throwaway repo https://github.com/apify/claude-md-lab --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent b062d71 commit a333e4d

1 file changed

Lines changed: 248 additions & 0 deletions

File tree

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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

Comments
 (0)