Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions .claude/skills/sync-security-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -1410,6 +1410,132 @@ will change and *why*. Group them by category:
single preformatted block and hiding every link. Do not
indent entries for "readability".

- **Release-manager hand-off comment** — when this sync pass
proposes the `pr merged` → `fix released` label swap (Step 12),
**also** propose posting a separate hand-off comment that walks
the release manager through the rest of the lifecycle (Steps
13–15) end-to-end, on a single tracker page, without forcing them
to consult the rollup or external docs.

**This is its own first-class comment, not a rollup entry.** The
rollup is for the security team's audit trail and accumulates many
small entries; the hand-off comment is a one-shot orientation
surface for the release manager and must stay readable as a single
comment. Folding it into the rollup would bury the call-to-action
inside a `<details>` block.

**Trigger.** Fires *exactly once* per tracker, at the same sync
pass that proposes `pr merged` → `fix released`. Do not propose it
earlier — the tracker is not yet the release manager's
responsibility before that swap, and a hand-off comment posted at
`cve allocated` or `pr merged` would lose context by the time the
release actually ships. Do not propose it on subsequent runs once
it has already been posted (idempotency check below).

**Idempotency.** Before proposing, scan the issue's existing
comments for the marker
```
<!-- apache-steward: release-manager-handoff v1 -->
```
exactly. If a comment carrying this marker already exists, **do
not propose a re-post** — surface as *"hand-off comment already
posted on `<comment-url>` (skipping)"* in the observed-state dump
and move on. The marker is on line 1 of the comment body so a
literal `gh issue view --json comments --jq` filter can detect it
cheaply.

**Body source.** The comment body comes from the project's
configured CVE tool — the path is
`tools/<cve-tool>/release-manager-handoff-comment.md` where
`<cve-tool>` is the value of `cve_tool` in
[`<project-config>/project.md`](../../../<project-config>/project.md#cve-tooling)
(for projects on Vulnogram, that resolves to
[`tools/vulnogram/release-manager-handoff-comment.md`](../../../tools/vulnogram/release-manager-handoff-comment.md)).
The template is parameterised; the substitutions the skill
performs are listed in the template's HTML-comment header. Do not
fork or paraphrase the template body in the proposal — load it
verbatim, substitute the placeholders, post.

**Resolving placeholders.** All values come from configuration or
from the tracker itself, so there is no free-form drafting:

- `CVE_ID` — from the tracker's *CVE tool link* body field.
- `RM_HANDLE` — looked up via the three-source cascade in Step 2c
(project's *Known release managers* / Release Plan wiki / dev@
`[RESULT][VOTE]` thread). Same lookup the assignee swap uses;
do it once and reuse.
- `SECURITY_LIST`, `USERS_LIST`, `ANNOUNCE_LIST` — from
[`<project-config>/project.md`](../../../<project-config>/project.md#mailing-lists).
- `SOURCE_TAB_URL`, `EMAIL_TAB_URL` — substitute `<CVE-ID>` into
`cve_tool_record_url_template` (from project.md), append
`#source` / `#email` per [`tools/vulnogram/record.md`](../../../tools/vulnogram/record.md#record-urls).
- `JSON_ANCHOR_URL` — the deep link the `generate-cve-json` tool
prints on every regen (the
`https://github.com/<tracker>/issues/<N>#cve-json--paste-ready-for-<cve-id-slug>`
anchor).
- `ARCHIVE_SCAN_URL` — the project's PonyMail public-search URL
template (`ponymail_public_search_url_template` from project.md),
parameterised with the CVE ID.
- `FRAMEWORK_RECORD_MD_URL`, `FRAMEWORK_SYNC_SKILL_URL`,
`FRAMEWORK_README_URL` — absolute GitHub URLs into
`apache/airflow-steward` `main`, since the framework lives in a
submodule that does not render through the parent-repo viewer
(per the absolute-URL rule used elsewhere in this repo).
- `CANNED_RESPONSES_URL` — absolute GitHub URL into the tracker
repo's `<project-config>/canned-responses.md`.

**Apply mechanic** — see the *Release-manager hand-off comment*
bullet in Step 4 below; it is a fresh `gh issue comment`, not a
PATCH on the rollup.

**Recap.** Surface the new comment URL in the recap (Step 6) so
the user can click through and verify the post.

- **Publication-ready notification comment** — when this sync pass
proposes populating the *Public advisory URL* body field (Step 14
— see the *Advisory archived on `<users-list>`* row of the Step 1d
table), **also** propose posting a separate publication-ready
notification comment on the tracker. The comment tells the release
manager that the archive URL has been captured, the JSON has been
regenerated to include it as a `vendor-advisory` reference, and
the final paste + `READY` → `PUBLIC` move is now unblocked.

**Why a second comment instead of one comment with two states.**
The hand-off comment posted at Step 12 has `READY` as its
rendered-final state and `PUBLIC` as a "wait for follow-up"
pointer. The follow-up is exactly this notification. Splitting
the call-to-action into two comments (rather than nudging the RM
to re-read step 7 of the same comment from days ago) gives the
RM a fresh, dated surface for the second action and a working
`@`-mention notification.

**Trigger.** Fires *exactly once* per tracker, at the same sync
pass that proposes the *Public advisory URL* body update. Do not
propose it earlier (the URL is not yet captured) or repeatedly
(idempotency check below).

**Idempotency.** Before proposing, scan the issue's existing
comments for the marker
```
<!-- apache-steward: release-manager-publication-ready v1 -->
```
exactly. If a comment carrying this marker already exists, do not
re-post — surface as *"publication-ready comment already posted on
`<comment-url>` (skipping)"* and move on.

**Body source.** Same load-from-tool-doc model as the hand-off
comment — the body comes from
`tools/<cve-tool>/release-manager-publication-comment.md` (for
Vulnogram:
[`tools/vulnogram/release-manager-publication-comment.md`](../../../tools/vulnogram/release-manager-publication-comment.md)).
Placeholders substituted: `CVE_ID`, `RM_HANDLE`, `ARCHIVE_URL`
(the just-captured archive URL), `SOURCE_TAB_URL`,
`JSON_ANCHOR_URL`, `CVE_ORG_URL`
(`https://www.cve.org/CVERecord?id=<CVE-ID>`).

**Apply mechanic** — same as the hand-off comment: a fresh
`gh issue comment`, surfaced in the recap.

- **Draft email to reporter (other reasons)** — whenever the ball is in our
court on the email thread for any other reason (a question from the
reporter, a follow-up needed for triage, communicating a negative
Expand Down Expand Up @@ -1597,7 +1723,48 @@ before moving on to the next item. Use:
comment with `gh api -X DELETE
repos/<tracker>/issues/comments/<id>`. Never delete before the
PATCH lands.
- **Release-manager hand-off comment:** load the body template from
`tools/<cve-tool>/release-manager-handoff-comment.md`, substitute
the placeholders (per the *Release-manager hand-off comment*
bullet in Step 2b), write the result to a temp file, then post:

```bash
gh issue comment <N> --repo <tracker> \
--body-file <tmpfile>
```

This is a **fresh comment**, not a PATCH on the rollup. The
`<!-- apache-steward: release-manager-handoff v1 -->` marker on
line 1 of the template is what subsequent sync runs grep for to
enforce idempotency — preserve it verbatim. Capture the new
comment URL from the post for the Step 6 recap.

Before posting, **scrub the resolved body** for the same bare-
name → `@`-handle replacements documented for the rollup PATCH
above, so the `RM_HANDLE` substitution actually notifies the
release manager.
- **Publication-ready notification comment:** same recipe as the
hand-off comment above, but loading
`tools/<cve-tool>/release-manager-publication-comment.md`. The
marker is `<!-- apache-steward: release-manager-publication-ready v1 -->`.
Apply right after the *Public advisory URL* body-field update has
landed and the CVE JSON has been regenerated (Step 5) — that way
the comment's *"the JSON has been regenerated to include the
archive URL"* claim is true at the moment the RM reads it.
- **Close / reopen:** `gh issue close <N> --repo <tracker> --reason completed` (or `not planned`).
When this is a GitHub-backed tracker that uses a project board,
**always** follow a successful close with the **archive-from-board**
mutation per the *Archive a board item* recipe in
[`tools/github/project-board.md`](../../../tools/github/project-board.md#archive-a-board-item--terminal-state-cleanup).
Closed issues leave the active board view automatically, but an
explicit archive (`archiveProjectV2Item`) is what moves the item
to the board's *"Archived items"* view permanently — without it,
reopening a tracker resurfaces it on whatever column its `Status`
field still points at, and historical board sweeps still see the
item. Apply the archive for every close, regardless of the close
reason (terminal-Step-15 or non-terminal disposition like
`invalid` / `duplicate` / `not CVE worthy` / `wontfix`); the
mutation is idempotent and a no-op on already-archived items.
- **Project-board column:** apply via the `updateProjectV2ItemFieldValue`
GraphQL recipe in
[`tools/github/project-board.md`](../../../tools/github/project-board.md#write--move-a-tracker-to-a-different-column).
Expand Down
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,18 @@ this ownership hand-off implicit; splitting them makes it explicit and
surfaces a `fix released` backlog the release manager can drive from the
board.

**Hand-off comment.** The same `sync-security-issue` pass that proposes
the `pr merged` → `fix released` swap also proposes posting an explicit
**release-manager hand-off comment** on the tracker — a self-contained,
numbered checklist (steps 13–15 from the RM's perspective) that
@`-mentions the release manager and links to the paste-ready CVE JSON,
the Vulnogram `#source` and `#email` tabs, and the canned-response
templates. The comment is a one-shot, posted exactly once per tracker;
subsequent sync runs detect it via an HTML marker and skip the post.
This is a separate first-class comment, not a status-rollup entry —
the rollup is for the security team's audit trail, the hand-off is the
RM's call-to-action surface.

### Step 13 — Send the advisory

During releases, the release manager looks through `fix released`
Expand Down Expand Up @@ -958,6 +970,19 @@ Until the *Public advisory URL* field is populated, the
record's public `vendor-advisory` reference will point at, and publishing
a CVE with an empty reference leaks a broken record into `cve.org`.

**Publication-ready notification comment.** The same sync pass that
populates the *Public advisory URL* body field also proposes posting a
**publication-ready notification comment** on the tracker — a separate
first-class comment that `@`-mentions the release manager, summarises
the deterministic updates that just landed (URL captured, JSON
regenerated, `announced` label added), and gives the explicit go-ahead
for the final paste + `READY` → `PUBLIC` move and tracker close. Like
the Step 12 hand-off comment, it is a one-shot posted exactly once
per tracker (idempotent on subsequent runs via an HTML marker).
Together the two comments form a two-part narrative the release
manager can drive from the tracker page without consulting the rollup
or external docs.

### Step 15 — Publish the CVE record and close the issue

**Push the final CVE record and close the issue.** For every issue
Expand All @@ -969,11 +994,14 @@ same person who sent the advisory in Step 13):
* copies the latest CVE JSON attachment from the tracking issue (the one
regenerated in Step 14, now carrying the `vendor-advisory` URL) and
pastes it into the `#source` form;
* saves and moves the record from `REVIEW` to `PUBLIC` in the ASF CVE
* saves and moves the record from `READY` to `PUBLIC` in the ASF CVE
tool — **this is the final action** that propagates the record to
[`cve.org`](https://cve.org);
* **closes the issue** — do not update any labels. That closes
the lifecycle.
the lifecycle. The `sync-security-issue` skill follows the close
with an explicit `archiveProjectV2Item` mutation so the closed
tracker leaves the active board permanently
(see [`tools/github/project-board.md` — *Archive a board item*](tools/github/project-board.md#archive-a-board-item--terminal-state-cleanup)).

This two-step hand-off (sync captures the URL → RM publishes the record)
means nobody has to remember both halves: the sync skill's responsibility
Expand Down
69 changes: 69 additions & 0 deletions tools/github/project-board.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
- [Introspection — re-fetch the option IDs](#introspection--re-fetch-the-option-ids)
- [Write — move a tracker to a different column](#write--move-a-tracker-to-a-different-column)
- [Orphan-issue path](#orphan-issue-path)
- [Archive a board item — terminal-state cleanup](#archive-a-board-item--terminal-state-cleanup)
- [Archive recipe](#archive-recipe)
- [Idempotency](#idempotency)
- [Inverse — unarchive](#inverse--unarchive)
- [When the board is a no-op](#when-the-board-is-a-no-op)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand Down Expand Up @@ -216,6 +220,71 @@ gh api graphql -f query='
# Step 3: move the newly-added item to the target column (see the write recipe above).
```

## Archive a board item — terminal-state cleanup

When a tracker reaches the terminal `closed` state of the lifecycle
(Step 15 of [`../../README.md`](../../README.md) — CVE record moved
to `PUBLIC`, tracker closed), the project-board item is no longer
useful as an active-work signal. Archive it from the board so the
active columns reflect only in-flight trackers.

Archiving is an explicit Projects V2 mutation (`archiveProjectV2Item`)
that hides the item from the default board view. The item is *not*
deleted — it stays on the board's "Archived items" view and continues
to belong to the project.

### Archive recipe

```bash
gh api graphql -f query='
mutation($pid:ID!,$iid:ID!) {
archiveProjectV2Item(input: { projectId: $pid, itemId: $iid }) {
item { id }
}
}' \
-F pid=<project-node-id> \
-F iid=<item-id>
```

The `pid` is the same `<project-node-id>` used by the column-move
mutation in [*Write — move a tracker to a different column*](#write--move-a-tracker-to-a-different-column);
the `iid` is the same `<item-id>` returned by the introspection
query (or freshly captured at apply time after `addProjectV2ItemById`
on a previously-orphan issue).

### Idempotency

Archiving an already-archived item returns success without changing
state — the mutation is idempotent on the second call. Skills that
re-run on closed trackers (for example, a backfill sweep on
historically-closed issues) can call `archiveProjectV2Item` without
detecting prior-archived state first.

To detect whether an item is already archived (e.g. before a sync run
to avoid emitting a no-op log line), include `isArchived` in the
introspection query — when an item carries `isArchived: true`, skip
the archive call.

### Inverse — unarchive

If a tracker is reopened (rare; usually only when a closing
disposition is reverted), restore the item to the active board with:

```bash
gh api graphql -f query='
mutation($pid:ID!,$iid:ID!) {
unarchiveProjectV2Item(input: { projectId: $pid, itemId: $iid }) {
item { id }
}
}' \
-F pid=<project-node-id> \
-F iid=<item-id>
```

The unarchived item lands back on whatever column its `Status` field
points at; if the column needs to change too, follow with a regular
column-move mutation.

## When the board is a no-op

Not every GitHub-backed project runs a Projects V2 board. A project
Expand Down
Loading
Loading