| name | import-security-issue-from-pr |
| description | Open a tracking issue in <tracker> for a security-relevant fix that
has already been opened (or merged) as a public PR in <upstream>,
in the case where there is no inbound `<security-list>`
report. The tracker lands in the `Assessed` board column (the
team-deliberate import implies the security assessment has already
happened) with the scope label applied, `pr created` / `pr merged`
reflecting the PR's state, and `Remediation developer` / `PR with
the fix` body fields populated from the PR — ready for
`allocate-cve` to take over.
|
| when_to_use | Invoke when a security team member says "import a tracker from
PR <N>", "open a tracker for <upstream>#NNN", "we need a CVE
for this PR", or similar — typically when a contributor opens or
merges a public fix that the team agrees is security-relevant but
that never went through `security@`. Use only when the PR's
security relevance has already been agreed informally; this skill
does not host a validity discussion. For reports that arrive on
`<security-list>`, use `import-security-issue`.
|
import-security-issue-from-pr
This skill is an alternative on-ramp of the security-issue handling
process for the case where the report never arrived on
<security-list>. A contributor opened a public fix
in <upstream>; somebody on the security team noticed it is
security-relevant; the team decided informally that the fix
warrants a CVE. This skill turns that public PR into an
<tracker> tracking issue so the rest of the workflow
(allocate-cve → sync-security-issue → fix-security-issue →
public advisory) can run.
It is the smaller sibling of import-security-issue:
| import-security-issue | import-security-issue-from-pr |
|---|
| Source | <security-list> Gmail / PonyMail thread | <upstream> PR URL or number |
| Reporter present | Yes (external researcher) | No (PR author = remediation developer = de-facto finder) |
| Receipt-of-confirmation reply | Drafted on the inbound thread | Skipped — no reporter to reply to |
| Inbound confidentiality | Report content is private; never leaks to public | PR is already public; no new private info to protect |
| Validity discussion | Hosted on the tracker after import (Step 3 of README.md) | Already done informally before invocation; tracker lands Assessed |
| Initial board column | Needs triage | Assessed |
Golden rule — Assessed, not Needs triage. When the team
deliberately imports from a public PR, they have already concluded
that the report is a security issue. The tracker therefore skips
the Needs triage column and the validity discussion that
column implies; it lands in Assessed with the scope label
applied, ready for CVE allocation. Only invoke this skill once
that informal assessment has happened — if the report's security
relevance is genuinely unclear, route it through the normal
process (a brief discussion in security team chat, then either
import via security@ if a reporter is involved, or open a
Needs triage tracker manually).
Golden rule — never reveal the security framing in <upstream>.
The PR exists in public. The security team's interpretation of it
(severity, exploit path, CVE intent) does not until the
advisory ships. After this skill runs, do not characterise the
public PR as a security fix, do not comment on it with the CVE
plan, and do not paste tracker discussion content into it. The
tracker URL itself is a public-safe identifier per the
Confidentiality of <tracker>
rule and may appear in the public PR description as a
cross-reference, so long as the surrounding text does not frame
the change as a security fix. The
fix-security-issue public-PR
guardrails apply in full from the moment the tracker exists:
neutral bug-fix language, no CVE-, no "vulnerability" or
"security fix" phrasing.
Prerequisites
Before running, the skill needs:
gh CLI authenticated (gh auth status returns OK) with
collaborator access to <tracker> and read access to
<upstream>. The skill calls gh pr view, gh search issues,
gh api repos/<tracker>/issues, and gh issue edit.
- Project-board write access. Setting the
Assessed column
uses the addProjectV2ItemById /
updateProjectV2ItemFieldValue GraphQL mutations from
tools/github/project-board.md.
No Gmail, no PonyMail. There is no inbound thread to read and no
reporter to draft a reply to.
See Prerequisites for running the agent skills
in README.md for overall setup.
Step 0 — Pre-flight check
Before fetching the PR, verify:
-
gh is authenticated and has access to both repos. Run
gh api repos/<tracker> --jq .name and
gh api repos/<upstream> --jq .name. If either errors (401,
403, 404), stop and tell the user to log in or get added.
-
The PR identifier is parseable. Accept any of:
| User input form | Resolved PR number |
|---|
65703 | 65703 |
<upstream>#65703 | 65703 (require repo == <upstream>) |
https://github.com/<upstream>/pull/65703 | 65703 (require repo == <upstream>) |
https://github.com/<upstream>/pull/65703/files | 65703 (trailing path stripped) |
If the input names a different repo than <upstream>, stop —
the security team only allocates CVEs for <upstream> PRs.
If either check fails, do not proceed; the skill would fail
mid-flow leaving half-built state.
Step 1 — Fetch PR metadata
Pull everything needed in one gh pr view:
gh pr view <N> --repo <upstream> --json \
number,title,body,author,state,mergedAt,url,files,labels,milestone,baseRefName \
> /tmp/pr-<N>.json
Record into the observed-state bag:
pr.number, pr.url, pr.title, pr.state
(OPEN / CLOSED / MERGED), pr.mergedAt (null when not
merged), pr.baseRefName, pr.body.
pr.author.login, pr.author.name — used for Remediation
developer and the proposed Reporter credited as.
pr.files[].path — drives scope detection in Step 2.
pr.labels[].name — informational only; tracker labels are
derived from scope, not copied.
pr.milestone.title — used for milestone detection in Step 3.
Reject CLOSED (not merged) PRs with a one-line ask: confirm the
user wants a tracker for an abandoned fix. The normal case is
OPEN (in-flight) or MERGED (already shipped).
Step 2 — Detect scope from changed files
The scope label is the load-bearing tracker field — it pins the
release train, the milestone format, the CVE container, and the
Affected versions shape (see
<project-config>/scope-labels.md).
Map pr.files[].path to scope:
| Path prefix | Scope | Notes |
|---|
providers/<name>/… | providers | Capture <name> (e.g. amazon, cncf-kubernetes, smtp) — used for the package name and the Affected versions field. |
chart/… or helm-chart/… | chart | Helm-chart-only changes. |
Anything else (e.g. airflow-core/…, top-level airflow/…, task-sdk/… through 3.2.x) | airflow | Core / shared. |
Mixed-scope guard. If pr.files[] touches more than one
scope (e.g. one file in providers/amazon/ and one in
airflow-core/), stop and surface a blocker:
PR changes files across more than one scope (<scope-A>,
<scope-B>). One tracker maps to one CVE container. Either
split the report into per-scope trackers manually, or
re-confirm with the team which scope the CVE should be
allocated against, and re-invoke with that decision noted.
The same convention exists in
scope-labels.md:
"if a report affects more than one scope, the security team
splits the report into per-scope trackers before allocation."
Multiple providers. A PR that touches
providers/amazon/ only is a single-package providers
tracker. A PR that touches providers/amazon/ and
providers/google/ is still a single providers-scope tracker
(scope is one), but the Affected versions body field carries
one line per affected package — propose both lines in
Step 5.
Test-only changes (*/tests/**) do not count toward
scope detection — they ride wherever the production code rides.
Strip them before applying the scope mapping.
Step 3 — Propose milestone
Milestone shape is scope-dependent (see
milestones.md):
airflow / chart scope — propose the PR's own
milestone (e.g. Airflow 3.2.2). If the PR has no milestone,
ask the user to pick the next core release; do not invent
one.
providers scope — propose the next Providers YYYY-MM-DD wave from
release-trains.md.
The PR's own milestone (if any) is the wrong signal for
providers — the providers wave ships on a separate cadence.
If the PR is already merged and the next wave's date is
unclear, surface the question and let the user pick.
Validate the proposed milestone exists on <tracker>:
gh api repos/<tracker>/milestones --jq '.[].title' | grep -F '<milestone>'
If it does not exist, surface as a blocker — milestone creation
is a manual project-board action, not part of this skill.
Step 4 — Duplicate-tracker guard
Before proposing a new tracker, check that one does not already
exist for this PR. The PR URL and number are both reliable
discriminators because the PR with the fix body field on
existing trackers contains the URL once sync-security-issue
has run on them.
gh search issues --repo <tracker> "in:body \"pull/<N>\"" \
--json number,title,state \
| jq '.'
Also search for the bare number to catch trackers where the
field has been hand-edited:
gh search issues --repo <tracker> "in:body <N>" --json number,title,state | jq '.'
If either search returns a hit:
- Surface the existing tracker(s) to the user with a clickable
<tracker>#NNN reference.
- Stop — do not create a duplicate tracker. The user either
re-invokes
sync-security-issue NNN to refresh the existing
tracker's PR-state labels, or (if the existing tracker is
closed and the fix needs re-tracking) invokes the skill again
with an explicit force argument.
Step 5 — Build proposed tracker contents
Assemble the proposal and surface it to the user before any
write. The proposal must include every field the user might want
to override.
5a — Title
Start from pr.title. Strip:
- Conventional-commit prefixes (
fix:, feat:, security:,
chore:, etc.) and their parenthesised scope (fix(secrets):).
[skip ci], [ci-skip], [skip-ci] markers.
- Trailing
(#NNNN) and [#NNNN].
Do not add <vendor>: <product>: (e.g. Apache Airflow:) prefix — that lives in the CVE
title, not the tracker title (the
allocate-cve skill normalises for
the CVE record). Tracker titles in <tracker> are
plain-language summaries.
If the cleaned title is shorter than ~25 characters or vague
(e.g. just fix bug in secrets backend), propose a longer
title that names the affected component, and surface the
proposed swap to the user.
5b — Issue body
The <tracker> issue template (see
tools/github/issue-template.md)
has nine fields. Fill them as follows:
| Field | Value |
|---|
| The issue description | Two paragraphs: (1) a one-line note > **Imported from public PR <upstream>#<N>** — there is no inbound \security@` report; the PR description below is the public statement of the vulnerability.` (2) the PR body verbatim, fenced if it is heavily templated. |
| Short public summary for publish | _No response_ (the team writes this when drafting the advisory; not derivable from the PR). |
| Affected versions | Per the scope's Affected versions convention from scope-labels.md. For providers: one line per affected package, <package-name> < NEXT VERSION. For airflow / chart: < X.Y.Z from the milestone. |
| Security mailing list thread | Sentinel: N/A — opened from public PR <upstream>#<N>; no security@ thread. The field is required: true in the form — the skill creates the issue via gh api (Step 7), which bypasses form-required-field enforcement, but the sentinel is still set so future sync-security-issue runs do not flag the field as missing. |
| Public advisory URL | _No response_. |
| Reporter credited as | _No response_. The PR author is not credited as the CVE reporter for this kind of import. A public PR is not a responsible disclosure — the contributor went straight to the public fix without giving the security team a chance to coordinate the announcement, so the security team neither owes a finder credit nor wants to incentivise the practice. The user can populate the field manually if there is a project-specific reason to credit a different individual (e.g. an internal reviewer who privately flagged the issue on the PR before it landed). See Reporter credit policy for public-PR imports below. |
| PR with the fix | pr.url (e.g. https://github.com/<upstream>/pull/65703). |
|
The body is written to a temp file in Step 7; in the proposal,
show it inline so the user can scan-and-redirect before any
write.
Reporter credit policy for public-PR imports
Trackers imported via this skill do not credit the PR author as
the CVE reporter. The reasoning:
- No responsible disclosure. The contributor opened a public fix
PR without giving the security team a chance to coordinate. The
CVE-finder credit is the project's recognition of someone who
followed the disclosure process; it is not appropriate to award it
retroactively to a public-PR submitter.
- Incentive alignment. Treating public-PR submitters as CVE
reporters trains the next contributor to skip
<security-list> and go straight to the public fix.
The credit asymmetry (no reporter credit for public-PR imports,
full credit for security@ reports) makes the disclosure path the
more attractive one.
- Remediation developer is different. The PR commit already
attributes the code change to the contributor publicly; crediting
them as
Remediation developer (which appears in the CVE record's
credits[] with type: "remediation developer") just acknowledges
what the public commit history already says. No new information is
exposed.
If a triager has a project-specific reason to credit a different
individual — for example, a security-team member who privately
spotted the issue on review of a routine-looking PR and asked the
author to land the fix — they override Reporter credited as
manually during Step 6 confirmation. The default is always blank.
Golden rule — no outreach to the PR author about the CVE. The
public PR stays unaware of the CVE plan until the advisory ships.
Do not comment on the PR characterising it as a security fix, do
not email or DM the PR author about the CVE allocation or the
advisory schedule, and do not paste tracker discussion content
into the PR description, commit messages, or review threads. The
tracker URL itself is a public-safe identifier (per the
Confidentiality of <tracker>
rule) and may appear as a cross-reference, but the security
framing and any tracker-content quotes must not. The PR author
learns about the CVE — if at all — when the public advisory ships.
5c — Labels
Apply at creation:
- Scope label:
<scope> (airflow, providers, or chart).
- PR-state label:
pr created if pr.state == OPEN, pr merged if pr.state == MERGED.
security issue — required for the <tracker> Auto-add to project workflow filter (is:issue label:"security issue"); without it the issue will not appear on the board.
Do not apply needs triage — this skill's deliberate-import
contract is that the validity assessment has already happened.
5d — Project board
Target column: Assessed (option ID
ce6377ce — see
<project-config>/project.md).
Validates the Label + body state → Status mapping:
Scope label applied, no CVE yet → Assessed.
5e — Status-rollup comment
The first entry on the tracker's status rollup. Shape per
tools/github/status-rollup.md:
<!-- <tracker> status rollup v1 — all bot-authored status updates fold into this single comment. -->
<details><summary><YYYY-MM-DD> · @<author-handle> · Import from PR (<scope>, <upstream>#<N>)</summary>
**Imported from public PR `<upstream>#<N>` on <YYYY-MM-DD>** (scope: `<scope>`, PR state: `<state>`).
This tracker was deliberately opened by the security team for a public fix that did **not** arrive on `<security-list>`. The validity assessment was made informally before invocation; the tracker landed in the `Assessed` column accordingly.
**Next:** Step 6 — allocate the CVE via the [`allocate-cve`](https://github.com/<tracker>/blob/<default-branch>/.claude/skills/allocate-cve/SKILL.md) skill.
Provenance: public PR <pr.url>, author `@<pr.author.login>`.
Extracted fields: scope=`<scope>`, *PR with the fix*=<pr.url>, *Remediation developer*=, =, Severity=.
intentionally left blank — public-PR imports do not credit the PR author as the CVE reporter (no responsible disclosure). See the []() section of the skill for the rationale.
Zero-whitespace rules from
status-rollup.md
apply: no leading spaces on any line inside the <details>
block, exactly one blank line after <summary>…</summary>,
exactly one blank line before </details>.
Step 6 — User confirmation
Surface the full proposal:
- PR identification (number, title, author, state, merged-at).
- Detected scope and reasoning (which file paths drove it).
- Proposed milestone.
- Title (original → cleaned).
- Body (each of the nine fields, inline).
- Labels.
- Target board column (
Assessed).
- Rollup comment text.
Confirmation forms:
go / proceed / yes / OK — apply as proposed.
title: <new title> — override the title only; everything
else as proposed.
reporter: <name> — populate Reporter credited as (default is
blank per Reporter credit policy).
Use only when there is a project-specific reason to credit a
different individual; this override does not add the PR
author back as the reporter.
severity: <level> — override the proposed Unknown.
- Multiple overrides comma-separated:
reporter: Anonymous, severity: Important.
cancel / none / hold off — bail; no tracker created.
Do not auto-default to import the way import-security-issue
does. This skill is invoked deliberately on a single PR;
spending one round-trip on explicit confirmation is the right
trade. The proposal-to-confirmation pause also lets the user
catch a bad scope detection (e.g. a task-sdk change
mis-classified as airflow) before any tracker write.
Step 7 — Apply
Sequenced. Each step depends on the previous one's output.
7a — Create the tracker via gh api
Bypasses the form so the Security mailing list thread
required-field check does not fire. Equivalent to
import-security-issue's Step 7.
Write the body to a temp file:
cat > /tmp/import-pr-<N>-body.md <<'EOF'
> **Imported from public PR <upstream>#<N>** — there is no inbound `security@` report; the PR description below is the public statement of the vulnerability.
<verbatim PR body>
_No response_
<per-scope shape>
N/A — opened from public PR <pr.url>; no security@ thread
_No response_
<proposed reporter>
<pr.url>
<proposed remediation developer>
_No response_
<proposed severity>
_No response_
EOF
Create:
gh api repos/<tracker>/issues \
-f title='<cleaned title>' \
-F body=@/tmp/import-pr-<N>-body.md \
--jq '.number, .node_id, .html_url'
Capture number, node_id, html_url from the response.
7b — Apply labels
gh issue edit <new-issue-number> \
--repo <tracker> \
--add-label '<scope>' \
--add-label '<pr-state-label>' \
--add-label 'security issue'
<scope> is one of airflow, providers, chart.
<pr-state-label> is pr created or pr merged per Step 5c.
7c — Set milestone
gh issue edit <new-issue-number> --repo <tracker> --milestone '<milestone>'
Skip if the user explicitly chose to leave it unset.
7d — Pin to the Assessed board column
Run the orphan-issue path from
tools/github/project-board.md
— addProjectV2ItemById followed by
updateProjectV2ItemFieldValue. The Auto-add to project
workflow may have already added the issue (filter:
is:issue label:"security issue"); both branches converge
because addProjectV2ItemById is idempotent.
gh api graphql -f query='
mutation($pid:ID!,$nid:ID!) {
addProjectV2ItemById(input: { projectId: $pid, contentId: $nid }) {
item { id }
}
}' \
-F pid=PVT_kwDOCAwKzs4BUzbt \
-F nid=<issue-node-id> \
--jq '.data.addProjectV2ItemById.item.id'
Capture the returned item ID, then set Status to Assessed:
gh api graphql -f query='
mutation($pid:ID!,$iid:ID!,$fid:ID!,$oid:String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $pid,
itemId: $iid,
fieldId: $fid,
value: { singleSelectOptionId: $oid }
}) { projectV2Item { id } }
}' \
-F pid=PVT_kwDOCAwKzs4BUzbt \
-F iid=<item-id> \
-F fid=PVTSSF_lADOCAwKzs4BUzbtzhD08bw \
-f oid=ce6377ce
The pid / fid / oid values come from
project.md;
re-fetch them via the introspection query in
project-board.md if
either mutation returns not found.
7e — Post the status-rollup comment
gh issue comment <new-issue-number> \
--repo <tracker> \
--body-file /tmp/import-pr-<N>-rollup.md
The rollup body is the one drafted in Step 5e with placeholders
filled.
7f — Cleanup
Delete /tmp/import-pr-<N>-body.md and
/tmp/import-pr-<N>-rollup.md. They served their purpose for
this run and would otherwise accumulate.
Step 8 — Recap and hand-off
Print a one-screen recap:
- The new tracker number and clickable
<tracker>#NNN link.
- The PR URL it was imported from.
- The board column (
Assessed).
- The labels applied.
- The milestone (if set).
- The status-rollup comment ID (clickable).
Then a one-line hand-off:
Next: allocate the CVE for this tracker. Run
allocate-cve on <tracker>#NNN.
Do not auto-invoke allocate-cve — CVE allocation is
PMC-gated (a non-PMC triager must relay the allocation request
to a PMC member), and the user may want to batch the allocation
with other trackers.
What this skill does not do
- Does not run a validity discussion. The skill's contract is
that the assessment has already happened; the tracker lands
Assessed. If you want a validity discussion, do not use this
skill — open the tracker manually with Needs triage instead.
- Does not draft a reporter reply. There is no reporter; the
PR author is the de-facto finder, and any communication with
them happens on the public PR (which already exists).
- Does not create the GHSA. GHSA creation, advisory drafting,
and the
<upstream> private-repo coordination all happen
later in the process — see
README.md.
- Does not characterise the public PR as a security fix until
the advisory ships. The tracker URL itself is a public-safe
identifier and may appear in the PR description as a
cross-reference; what does not appear is the CVE ID, the words
"vulnerability" / "security fix" / "advisory", and any
verbatim quote from the tracker discussion. See the
Confidentiality of
<tracker>
rule.
- Does not run
sync-security-issue on the new tracker. The
initial body is already coherent; sync's job (reconciling PR
state, milestone, assignee against current reality) is not
needed on a tracker that is being created from those exact
signals. Run sync only when the PR or thread state evolves
later.
Failure modes
| Symptom | Likely cause | Fix |
|---|
gh api repos/<upstream> returns 404 | Repo placeholder not substituted | Re-read <project-config>/project.md for the upstream_repo: value. |
PR is CLOSED (not merged) | Fix abandoned upstream | Stop and confirm with the user that a tracker is still wanted; otherwise abandon. |
gh api repos/<tracker>/issues returns 422 | Missing or invalid title / body field shape | Re-check the body against the issue template's nine fields; the ### <field> headings must match exactly (case-sensitive). |
addProjectV2ItemById returns not found for the project | Project-board node ID changed | Re-run the introspection query in project-board.md and update project.md. |
| Multiple existing trackers match the duplicate-guard search | Earlier closed-as-duplicate trackers reference the PR number in passing | Surface all hits to the user; let them confirm force to proceed anyway. |
Mixed-scope PR (e.g. providers/ + airflow-core/) | The fix lives in more than one product | Stop; surface the per-scope split decision to the user before re-invoking. |
Examples
Example 1 — providers scope, already merged
import from pr 65703
PR <upstream>#65703 (Prevent unauthorized access to
team-scoped secrets in SM and SSM), state MERGED, author
justinpakzad. Files: 6 paths under
providers/amazon/.../secrets/. Scope detection: providers
(amazon). Milestone: next Providers YYYY-MM-DD wave (the PR
itself has no milestone). Labels: providers, pr merged,
security issue. Board column: Assessed. Affected versions:
apache-airflow-providers-amazon < NEXT VERSION. Remediation
developer: Justin Pakzad (PR commit attributes the change
publicly). Reporter credited as: blank — public-PR imports do
not credit the PR author as the CVE reporter (no responsible
disclosure; see Reporter credit policy).
Example 2 — airflow scope, in-flight
import from pr https://github.com/<upstream>/pull/65999
PR state OPEN, milestone Airflow 3.2.3. Files all under
airflow-core/src/airflow/api_fastapi/. Scope: airflow.
Milestone: Airflow 3.2.3. Labels: airflow, pr created,
security issue. Affected versions: < 3.2.3. The skill
proposes everything; on user confirmation, the tracker lands
Assessed, ready for allocate-cve.
Example 3 — Mixed-scope PR (blocker)
import from pr 66042
PR touches airflow-core/src/.../serialization.py and
providers/standard/src/.../python_operator.py. The skill
stops and surfaces:
PR 66042 changes files across airflow and providers
scopes. Split the report into two trackers (one per scope)
manually, or re-confirm which scope the CVE should be
allocated against.