Status-rollup comment
June 2, 2026 · View on GitHub
Every agent-authored status update on a <tracker> issue (the import
receipt, each sync pass, CVE allocation, dedupe merges, fix-PR
announcements, etc.) lands in one single rollup comment per
tracker. Each pass appends a new entry to that comment instead of
posting a fresh one. The result: scrolling a tracker's timeline shows
one rollup comment plus the human discussion, not twenty bot comments
drowning out the actual conversation.
This file is the canonical shape + upsert recipe. Skills (sync-*,
import-*, allocate-*, deduplicate-*, fix-*) reference this
file instead of re-specifying the shape.
The rollup comment shape
One comment per tracker, identified by an opening HTML marker on the first line:
<!-- airflow-s status rollup v1 — all bot-authored status updates fold into this single comment. -->
<details><summary>YYYY-MM-DD · @user · <Action></summary>
<entry body>
</details>
---
<details><summary>YYYY-MM-DD · @user · <Action></summary>
<entry body>
</details>
Rules (all load-bearing — breaking any of them breaks GitHub's Markdown rendering):
-
First line is the marker.
<!-- airflow-s status rollup v1 — … -->identifies the comment as the rollup. Detection is anchored onairflow-s status rollup vso futurev2bumps remain findable. -
Every entry is its own
<details>block. Including the very first one (the import receipt). There is no always-visible preamble above the first<details>. The reason: every entry — current and historical — is collapsed by default per the user's rule; nothing is promoted above the fold. -
Open tag is one line. Write
<details><summary>…</summary>on a single line. Do not split<details>onto its own line and<summary>onto the next — that variant renders reliably on github.com too, but the one-line form is what the skills emit and detection passes match on. -
Summary contains three fields,
·-separated, in this order:YYYY-MM-DD · @handle · <Action>. No trailing text, no parenthetical headlines, nothing else. The details-open arrow on github.com already consumes horizontal space; keep the summary scannable at a glance. Optional fourth field in parentheses is allowed only for disambiguation — see Summary — action labels below. -
Exactly one blank line after
<summary>…</summary>. Markdown inside a<details>needs a blank line after the open to render. Two blank lines is fine; zero blank lines silently suppresses all markdown inside. -
Exactly one blank line before
</details>. Same reason, other end. -
No leading whitespace on any line inside the entry. A leading space or tab turns the line into a preformatted-code block and wrecks the rendering for every subsequent line. When a sync proposal pastes multi-line content into an entry, left-trim every line before writing.
-
Entries are separated by a bare
---on its own line, with one blank line on each side:</details> --- <details><summary>…</summary>The ruler is GitHub's
<hr>. Skip the blank lines around it and the preceding</details>will stay attached. -
Chronological order — newest at the bottom. New entries append; the comment grows downward. A reader opens the rollup and scrolls to the latest entry at the end.
Summary — action labels
Each skill emits one of the following <Action> strings so the summary
line tells the reader at a glance what the entry represents:
| Emitting skill | <Action> value | Optional parenthetical |
|---|---|---|
security-issue-import | Import | class + reporter, e.g. Import (Report, Jane Doe) |
security-issue-sync (ordinary pass) | Sync | one-phrase headline, e.g. Sync (pr merged → fix released) |
security-issue-sync (escalation, Step 4) | Sync | Sync (Step 4 escalation) |
security-issue-sync (reformat-only, migrating legacy comments) | Reformat | Reformat (N legacy comments folded) |
security-cve-allocate | CVE allocated | the allocated ID, e.g. CVE allocated (CVE-2026-40913) |
security-issue-deduplicate, on the kept tracker | Merge (kept) | dropped side's number, e.g. Merge (kept) (from #305) |
security-issue-deduplicate, on the dropped tracker | Merge (dropped) | kept side's number, e.g. Merge (dropped) (into #244) |
security-issue-fix | Fix PR | upstream PR number, e.g. Fix PR (<upstream>#65346) |
The parenthetical is optional; include it when it adds information a scroller actually wants (the CVE ID, the dedupe counterpart, the PR number). Do not restate the same fact inside the entry body and in the parenthetical; the body carries the detail, the summary carries the headline.
The entry body
Inside the <details> block, write what the skill used to write in
its pre-collapse body — the bold headline, the **Next:** line, the
reporter-notification line, the full rationale. The entire body is
already inside <details>, so the "keep visible part under six
lines" rule from the legacy status-comment shape no longer
applies. Write what the auditor needs to reconstruct the decision;
the scroller sees the summary only and only expands entries they
care about.
That said — brevity still wins. Do not pad. Do not restate the previous entry. Each entry is incremental: what changed in this pass, what comes next, what the reporter now knows. Earlier state lives in earlier entries.
Required elements inside every entry body:
- Bold headline as the first line — the same bold-first-line rule
the old pre-collapse comments used. Example:
**Sync 2026-04-21 — pr merged → fix released.**. This is what a reader sees first when they expand the entry. **Next:**line — one sentence on what comes next. Omit only when the entry is terminal (e.g. dedupe on the dropped side, the "all triage continues on #" line replaces**Next:**).- Reporter-notification line when applicable — one of the four forms from the legacy spec (see each skill's dedicated section).
Outside that required frame, the content is free-form markdown.
Clickable <tracker> references (the Linking tracker issues and
PRs rule in AGENTS.md) apply everywhere, same
as before.
Upsert recipe — append to an existing rollup, or create one
Every skill that emits a status update runs this recipe. The steps
assume the skill has already composed <new-entry> — the full
<details>…</details> block for this pass, with no leading/trailing
blank lines.
1. Find the existing rollup comment
gh issue view <N> --repo <tracker> \
--json comments \
--jq '.comments[] | select(.body | startswith("<!-- airflow-s status rollup v")) | {id: .id, body: .body, url: .url}'
The matching comment is the rollup. If the query returns nothing,
there is no rollup yet (expected on a fresh tracker where
security-issue-import has not run, or on a legacy tracker that
pre-dates this convention).
Use the first match chronologically if the query somehow returns more than one — two rollups is a bug; surface it to the user and let them pick which one to keep.
2a. Append to an existing rollup
Construct the new body by concatenating the old body + a ruler + the new entry, with exactly one blank line on each side of the ruler:
<old body>
---
<new entry>
Write the new body to a temp file and PATCH the comment:
python3 - <<'PY' > /tmp/rollup-body.md
import pathlib, subprocess, json, textwrap
old = subprocess.check_output(
["gh", "api", "repos/<tracker>/issues/comments/<comment-id>", "--jq", ".body"],
text=True,
).rstrip("\n")
new_entry = pathlib.Path("/tmp/new-entry.md").read_text().rstrip("\n")
print(old + "\n\n---\n\n" + new_entry)
PY
jq -Rs '{body: .}' /tmp/rollup-body.md > /tmp/rollup-patch.json
gh api -X PATCH repos/<tracker>/issues/comments/<comment-id> --input /tmp/rollup-patch.json
The -X PATCH repos/<tracker>/issues/comments/<id> form is the only
reliable way; gh issue comment --edit-last does not target an
arbitrary comment, and the --input flag is needed because
--field body=@file URL-encodes the newlines in the body.
2b. Create a new rollup
Only if Step 1 returned no existing rollup. Prepend the marker line and emit the new entry as the rollup's first entry:
<!-- airflow-s status rollup v1 — all bot-authored status updates fold into this single comment. -->
<new entry>
Post as a regular comment via gh issue comment --body-file:
gh issue comment <N> --repo <tracker> --body-file /tmp/rollup-body.md
Capture the returned comment URL + ID so subsequent passes in the same run can append without re-searching.
Migrating legacy comments into a rollup
Trackers created before this convention carry one or more bot-authored status comments as separate top-level comments. Every sync pass runs a fold-legacy sub-step: detect each legacy bot comment, move its content into the rollup as its own entry, and delete (or minimise) the original.
Detecting a legacy bot comment
A comment is a candidate for folding when all of the following hold:
- Not already a rollup. Its body does not start with
<!-- airflow-s status rollup v. - Author is on the security-team roster. Cross-check
.comments[].author.loginagainst the collaborator list (seeoperations.md) or the project's roster declared in<project-config>/release-trains.md. Human discussion from an external reporter is never folded; their content stays as a top-level comment. - Body matches one of the bot-shape prefixes (case-sensitive,
first ~500 characters of the body):
**Sync**Status update**Merged**Closing as duplicate**Split for scope clarity**Imported on**Process-step escalation**Allocated CVE/**CVE allocated/**Sync … — CVE- Legacy bare-text prefixes (no leading
**):Sync status (,Sync YYYY-MM-DD,Status update - Content tells when the prefix is idiosyncratic:
security-issue-sync skill,re-triage,Reporter notification still pending,Outstanding — Step, a verbatimgenerate-cve-jsonembed block.
A comment that matches 1 + 2 + 3 is foldable. A comment that matches only 1 + 2 (team-member comment with no bot-shape prefix) is regular human discussion — leave it alone.
Folding a legacy comment into the rollup
For each foldable legacy comment, in chronological order:
-
Reconstruct the entry shape. Take the legacy body and wrap it in the rollup's
<details>envelope:<details><summary><createdAt date> · @<author.login> · <Action></summary> <legacy body — verbatim, left-trimmed> </details><createdAt date>is the first 10 chars of the legacy comment'screatedAt(YYYY-MM-DD).<Action>is derived from the legacy body's prefix via the table in Summary — action labels above; when the prefix does not map cleanly, useSyncand tag the fold asReformat (N legacy comments folded)on the overall rollup entry the sync is about to write.- Left-trim every line before pasting. Legacy comments that
were hand-edited sometimes carry stray indentation (see
airflow-s#244's 2026-04-20 comment, which hadon most lines); leaving that indentation inside a<details>turns the whole entry into a preformatted-code block.
-
Append the reconstructed entry to the rollup, using the upsert recipe above (Step 2a). Preserve the original order by appending oldest-first.
-
Delete the legacy comment once the rollup PATCH succeeds:
gh api -X DELETE repos/<tracker>/issues/comments/<legacy-comment-id>Only delete after the append lands — if the PATCH fails, the content is still on the tracker via the legacy comment and the fold can be retried on the next pass. Never delete first and hope the append works.
The fold-legacy sub-step is a proposal, not an auto-apply
Like every other skill action, surface each proposed fold as a
numbered item in the skill's Step 2 proposal. Show the legacy
comment's URL, its first ~3 lines, and the derived <Action> for
the summary. The user may accept all, accept some, or reject (for
example when a legacy comment has inline discussion from a reporter
mixed into a status update — in that case leave it alone, it is not
a pure bot comment).
When a tracker has no rollup yet but has many legacy comments
The fold path still works: create the rollup (Step 2b) with the
oldest foldable legacy comment as its first entry, then append the
rest (Step 2a), then append the current pass's new entry last. The
recap reports "created new rollup comment on #
Hard rules
- Never touch a human-authored comment. Step 2 of the detection (author is on the security-team roster) is not optional. A reporter quoting a status update in their own words is not bot content — it is their message.
- Never delete a legacy comment before the append succeeds. If the PATCH lands a partial body (e.g. truncated), the only recoverable artefact is the original legacy comment.
- Never rewrite the content of a folded entry. Copy the legacy
body verbatim inside the
<details>block (with left-trim applied to fix indentation-induced rendering bugs, but nothing else). Paraphrasing a historical status update rewrites the audit trail. - Never promote a newer entry above an older one. The rollup is chronological. A sync pass that appends out of order hides the timeline.
- Never write markdown with leading spaces inside a
<details>block. A single stray indentation breaks rendering for every subsequent line of the entry. Compose entries with all lines flush-left. - Never create two rollups on the same tracker. If Step 1 of the
upsert finds more than one
<!-- airflow-s status rollup vmarker, stop and ask the user which to keep — the cheapest recovery is a manual merge, not a silent overwrite. - Never name or describe other ASF projects' vulnerabilities in a
rollup entry body, even when the reporter or your own signal
mining has surfaced them. Cross-project observations belong in
the private mail channel they arrived on — not in the tracker.
See the "Other ASF projects — never name or describe their
vulnerabilities" subsection of
AGENTS.mdfor the full rule, the why, and the grep-list self-check to run before posting. Summarise load-bearing cross-project context in de-identified form ("the reporter has filed similar reports with other ASF projects") rather than naming the project.
Referenced by
.claude/skills/security-issue-import/SKILL.md— creates the rollup with the first entry..claude/skills/security-issue-sync/SKILL.md— appends per-sync entries and runs the fold-legacy sub-step..claude/skills/security-cve-allocate/SKILL.md— appends the CVE-allocation entry..claude/skills/security-issue-deduplicate/SKILL.md— appends the merge entry on both trackers..claude/skills/security-issue-fix/SKILL.md— appends the fix-PR entry.