From 6aad1a7996936960b5135ebea99835b1afb97c07 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 8 Sep 2026 21:06:10 +0000
Subject: [PATCH 1/5] Require complete API audit coverage with bounded
delegation recovery
Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com>
---
.github/workflows/primer-api-review.lock.yml | 2 +-
.github/workflows/primer-api-review.md | 87 ++++++++++++++++----
2 files changed, 72 insertions(+), 17 deletions(-)
diff --git a/.github/workflows/primer-api-review.lock.yml b/.github/workflows/primer-api-review.lock.yml
index 6bd7a406874..dab213da18a 100644
--- a/.github/workflows/primer-api-review.lock.yml
+++ b/.github/workflows/primer-api-review.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"52efa6af926e1af676d737798ec49d1d15da528f2613da1467779d0974b37fe4","body_hash":"cc63dce1a77a407e7e3568c748edc9570742f51b2f3070dfd76951a1d3b100b4","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"52efa6af926e1af676d737798ec49d1d15da528f2613da1467779d0974b37fe4","body_hash":"eba959b2605574154f7e180cddad15cea4c5e31080f63db33aa27aac8bfac943","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}}
# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"skills":[".github/skills/style-guide"],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.12","digest":"sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.12@sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"safeoutputs","tools":["create_issue","missing_data","missing_tool","noop","update_issue"]}]}
# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
diff --git a/.github/workflows/primer-api-review.md b/.github/workflows/primer-api-review.md
index 43b86fa2e84..ce82eabeb62 100644
--- a/.github/workflows/primer-api-review.md
+++ b/.github/workflows/primer-api-review.md
@@ -65,32 +65,73 @@ unresolved component API deviations from the Primer React style guide.
## Review process
1. Read and apply the installed `style-guide` skill, including
- `contributor-docs/style.md` and its component prop-naming guidance.
+ `contributor-docs/style.md` and its component prop-naming guidance. Build a
+ checklist of every principle from both documents, using their headings as
+ stable identifiers. Include rest-parameter placement, intentional shared-prop
+ merging (`mergeProps`), hooks accepting instead of returning refs, hide/show
+ naming and durable defaults, and mutually exclusive booleans, as well as
+ callback signatures, boolean state names, and variant/size semantics. These
+ examples are not the complete checklist.
2. Read `/tmp/gh-aw/data/components.json`. Review every listed component
directory. Cross-check the package's public exports and add any exported
- component that is missing from the inventory. Partition the complete list
- into batches of no more than 10 directories and delegate each batch to the
- `component-api-auditor` agent. Do not skip a component because it is
+ component that is missing from the inventory. Include compound subcomponents,
+ re-exported types, and relevant hooks, even when their implementation lives
+ outside the component directory. Do not skip a component because it is
deprecated, experimental, or complex.
-3. Require evidence for every finding:
+3. Track a coverage matrix for every component and checklist principle. Each cell
+ starts as `not-reviewed` and must end as `pass`, `finding`, or `not-applicable`.
+ Record inspected file paths and line ranges for passes and findings, and a
+ source-backed reason for each not-applicable judgment. A component is fully
+ reviewed only when no cell remains `not-reviewed`.
+4. Partition the inventory into batches of no more than five directories.
+ Delegate one pilot batch to `component-api-auditor` with the exact component
+ list, principle checklist, and coverage contract below. Validate its returned
+ coverage before dispatching the remaining batches. Keep delegation one level
+ deep and at most two batches in flight.
+ - Treat empty, malformed, errored, or findings-only responses as missing
+ coverage, not as evidence of no deviations. A bare `none` is insufficient.
+ - Accept only source-backed cells for assigned components and principles;
+ retain valid partial results and leave omitted or unsupported cells
+ `not-reviewed`.
+ - Retry missing coverage once per batch with a smaller assignment (one
+ component). Inspect the rest of that batch directly. After two consecutive
+ unusable responses, stop dispatching sub-agents for this run and switch to
+ direct review of all remaining cells. Do not repeat the same failed fan-out.
+ - On any unresolved delegation failure, inspect the missing coverage directly
+ using the same checklist and evidence requirements. Read the relevant
+ source, types, render paths, and hooks for every component/principle pair.
+ Grep-based sweeps are navigation aids, not proof of full coverage or of the
+ absence of a deviation.
+5. Require evidence for every finding:
- identify the component and public API
- cite the exact style-guide principle
- cite repository file paths and line numbers
- describe the smallest consumer-facing API change that would resolve it
-4. Read `/tmp/gh-aw/data/existing-review.json`. When a prior finding appears in
+6. Read `/tmp/gh-aw/data/existing-review.json`. When a prior finding appears in
the existing issue, inspect the issue comments for a clear, substantive
explanation of why that API intentionally exists. If a comment is tied to
that finding and provides a reason, omit the finding entirely. Do not treat
an acknowledgement, question, or unrelated comment as a reason.
-5. Merge duplicate findings and discard anything speculative, stylistic but not
+7. Merge duplicate findings and discard anything speculative, stylistic but not
covered by the guide, or unsupported by source evidence.
+8. Reconcile the coverage matrix against the complete inventory and checklist
+ before writing the issue. If any cell is still `not-reviewed`, use `noop` and
+ identify the missing components/principles and delegation failures in its
+ reason. Do not replace the existing issue with a partial audit, remove prior
+ findings merely because they were not rechecked, or claim full coverage based
+ on the number of dispatched batches.
## Issue output
Build a complete replacement body using GitHub-flavored Markdown:
- Start sections at `###`.
-- Include a short summary with the review date and number of components reviewed.
+- Include a short summary with the review date and fully reviewed component count
+ out of the total inventory.
+- Include a compact coverage table by principle with counts of `pass`, `finding`,
+ and `not-applicable` components. Each row must account for the entire inventory.
+ State whether sub-agents, direct inspection, or both supplied the evidence,
+ including any failed batches and recovery performed.
- Group findings by style-guide principle.
- For each finding, include the component/API, evidence, impact, and recommended
change.
@@ -120,15 +161,29 @@ the review cannot be completed well enough to produce a trustworthy issue body.
---
description: Audits a bounded batch of Primer React component APIs against the style guide
-model: small
+model: inherited
---
-Review only the assigned component directories. Read the installed `style-guide`
-skill, `contributor-docs/style.md`, and relevant public types, exports, tests,
-stories, and documentation for each component.
+Review only the assigned components and principles, following their relevant
+hooks, compound subcomponents, and re-exported types outside the assigned
+directories as needed. Read the installed `style-guide` skill,
+`contributor-docs/style.md`, its component prop-naming guidance, and relevant
+source, public types, exports, tests, stories, and documentation for each
+component. Do not delegate further.
-Return compact structured findings. Each finding must contain:
+Return a compact structured report with `coverage` and `findings` sections, even
+when no deviations are found. For every assigned component/principle pair,
+include a coverage entry with:
+
+- component and principle heading
+- status: `pass`, `finding`, `not-applicable`, or `not-reviewed`
+- inspected file paths and line ranges, plus a brief explanation supporting the
+ status (including why a principle is not applicable)
+- a blocker when the status is `not-reviewed`
+
+Mark a pair `not-reviewed` when source inspection is incomplete; never infer a
+pass from a search with no matches. Each finding must contain:
- component and public API
- violated style-guide principle
@@ -136,6 +191,6 @@ Return compact structured findings. Each finding must contain:
- consumer impact
- smallest recommended API change
-Report `none` for a component when no evidence-backed deviation exists. Do not
-infer requirements that are absent from the style guide, and do not propose code
-changes.
+Return an empty `findings` list when no evidence-backed deviation exists, but
+still return the coverage entries. Do not infer requirements that are absent
+from the style guide, and do not propose code changes.
From 1898d342e1d00f87daf9af5f69e7c6e66dc4a1c1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 8 Sep 2026 21:47:31 +0000
Subject: [PATCH 2/5] Publish verified partial API audits and fix auditor model
selection
Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com>
---
.github/workflows/primer-api-review.lock.yml | 2 +-
.github/workflows/primer-api-review.md | 52 ++++++++++++++------
2 files changed, 38 insertions(+), 16 deletions(-)
diff --git a/.github/workflows/primer-api-review.lock.yml b/.github/workflows/primer-api-review.lock.yml
index dab213da18a..a9eb3643867 100644
--- a/.github/workflows/primer-api-review.lock.yml
+++ b/.github/workflows/primer-api-review.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"52efa6af926e1af676d737798ec49d1d15da528f2613da1467779d0974b37fe4","body_hash":"eba959b2605574154f7e180cddad15cea4c5e31080f63db33aa27aac8bfac943","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"52efa6af926e1af676d737798ec49d1d15da528f2613da1467779d0974b37fe4","body_hash":"a0a1d80481e0af4b55ee1bbd853afc11a8a5129dea81c323735568447f40243b","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}}
# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"skills":[".github/skills/style-guide"],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.12","digest":"sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.12@sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"safeoutputs","tools":["create_issue","missing_data","missing_tool","noop","update_issue"]}]}
# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
diff --git a/.github/workflows/primer-api-review.md b/.github/workflows/primer-api-review.md
index ce82eabeb62..dbc26205ef9 100644
--- a/.github/workflows/primer-api-review.md
+++ b/.github/workflows/primer-api-review.md
@@ -79,15 +79,20 @@ unresolved component API deviations from the Primer React style guide.
outside the component directory. Do not skip a component because it is
deprecated, experimental, or complex.
3. Track a coverage matrix for every component and checklist principle. Each cell
- starts as `not-reviewed` and must end as `pass`, `finding`, or `not-applicable`.
+ starts as `not-reviewed`; change it to `pass`, `finding`, or `not-applicable`
+ only after inspection.
Record inspected file paths and line ranges for passes and findings, and a
source-backed reason for each not-applicable judgment. A component is fully
- reviewed only when no cell remains `not-reviewed`.
+ reviewed only when no cell remains `not-reviewed`. Read the existing issue's
+ remaining-coverage list and prioritize those pairs so successive runs do not
+ repeat only the same callback, boolean-name, and variant/size checks. Prior
+ coverage is historical context, not proof of a pass against current source.
4. Partition the inventory into batches of no more than five directories.
Delegate one pilot batch to `component-api-auditor` with the exact component
list, principle checklist, and coverage contract below. Validate its returned
- coverage before dispatching the remaining batches. Keep delegation one level
- deep and at most two batches in flight.
+ coverage before dispatching the remaining batches: wait for the pilot result,
+ not just an agent ID or idle status. Keep delegation one level deep and at
+ most two batches in flight.
- Treat empty, malformed, errored, or findings-only responses as missing
coverage, not as evidence of no deviations. A bare `none` is insufficient.
- Accept only source-backed cells for assigned components and principles;
@@ -97,11 +102,16 @@ unresolved component API deviations from the Primer React style guide.
component). Inspect the rest of that batch directly. After two consecutive
unusable responses, stop dispatching sub-agents for this run and switch to
direct review of all remaining cells. Do not repeat the same failed fan-out.
+ For a model/pricing or authentication error, skip the retry and switch to
+ direct inspection immediately; a smaller assignment cannot fix that error.
- On any unresolved delegation failure, inspect the missing coverage directly
using the same checklist and evidence requirements. Read the relevant
source, types, render paths, and hooks for every component/principle pair.
Grep-based sweeps are navigation aids, not proof of full coverage or of the
absence of a deviation.
+ - Continue direct inspection in bounded batches while time and context allow,
+ reserving time to publish the verified results and remaining coverage. Do
+ not stop solely because the entire inventory is too large for one run.
5. Require evidence for every finding:
- identify the component and public API
- cite the exact style-guide principle
@@ -115,11 +125,13 @@ unresolved component API deviations from the Primer React style guide.
7. Merge duplicate findings and discard anything speculative, stylistic but not
covered by the guide, or unsupported by source evidence.
8. Reconcile the coverage matrix against the complete inventory and checklist
- before writing the issue. If any cell is still `not-reviewed`, use `noop` and
- identify the missing components/principles and delegation failures in its
- reason. Do not replace the existing issue with a partial audit, remove prior
- findings merely because they were not rechecked, or claim full coverage based
- on the number of dispatched batches.
+ before writing the issue. Publish useful, evidence-backed progress even if
+ some cells remain `not-reviewed`; label the audit partial and list the
+ remaining component/principle pairs. Merge verified findings with the existing
+ issue, retaining prior findings not rechecked and labeling them as such. Remove
+ a prior finding only when current source disproves it or an issue comment
+ supplies the documented rationale described above. Never claim full coverage
+ based on the number of dispatched batches.
## Issue output
@@ -127,16 +139,23 @@ Build a complete replacement body using GitHub-flavored Markdown:
- Start sections at `###`.
- Include a short summary with the review date and fully reviewed component count
- out of the total inventory.
+ out of the total inventory. Distinguish a full audit from a partial audit.
- Include a compact coverage table by principle with counts of `pass`, `finding`,
- and `not-applicable` components. Each row must account for the entire inventory.
+ `not-applicable`, and `not-reviewed` components for this run. Each row must
+ account for the entire inventory; retained historical findings do not count
+ as reviewed cells.
State whether sub-agents, direct inspection, or both supplied the evidence,
including any failed batches and recovery performed.
+- For a partial audit, include a remaining-coverage list grouped by principle
+ with component names and the next bounded batch to inspect. Prioritize gaps
+ from the previous run that remain unreviewed before newly introduced gaps.
- Group findings by style-guide principle.
- For each finding, include the component/API, evidence, impact, and recommended
change.
-- If there are no unresolved findings, state that the full review found no
- unexplained deviations.
+- State that the full review found no unexplained deviations only when coverage
+ is complete and there are no unresolved findings. When a partial audit has no
+ new findings, state that no new deviations were found in the inspected subset
+ without implying the unreviewed APIs passed.
- Include the workflow run as
`[§${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})`.
- Do not include findings that have a documented rationale in issue comments.
@@ -154,14 +173,17 @@ Otherwise, create one issue with `create_issue`, the exact title
Perform exactly one visible issue action per run. Never create a second review
issue when an exact-title issue exists. Use `noop` with a short reason only when
-the review cannot be completed well enough to produce a trustworthy issue body.
+no trustworthy progress can be published (for example, source or prior issue
+data is unavailable and no safe update is possible). Incomplete coverage or
+failed delegation alone is not a reason to use `noop`: verified existing
+findings, new findings, or newly completed coverage can support a partial update.
## agent: `component-api-auditor`
---
description: Audits a bounded batch of Primer React component APIs against the style guide
-model: inherited
+model: claude-sonnet-5
---
From c1df664fa240b4b3440e66317fbe2e76df503155 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 9 Sep 2026 16:54:25 +0000
Subject: [PATCH 3/5] Organize API reviews into linked component finding
comments
Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com>
---
.github/workflows/primer-api-review.lock.yml | 108 +++++++++++-
.github/workflows/primer-api-review.md | 156 +++++++++++++++---
script/primer-api-review.test.mjs | 164 +++++++++++++++++++
3 files changed, 403 insertions(+), 25 deletions(-)
create mode 100644 script/primer-api-review.test.mjs
diff --git a/.github/workflows/primer-api-review.lock.yml b/.github/workflows/primer-api-review.lock.yml
index a9eb3643867..cc0dc38dd43 100644
--- a/.github/workflows/primer-api-review.lock.yml
+++ b/.github/workflows/primer-api-review.lock.yml
@@ -1,5 +1,5 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"52efa6af926e1af676d737798ec49d1d15da528f2613da1467779d0974b37fe4","body_hash":"a0a1d80481e0af4b55ee1bbd853afc11a8a5129dea81c323735568447f40243b","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}}
-# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"skills":[".github/skills/style-guide"],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.12","digest":"sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.12@sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"safeoutputs","tools":["create_issue","missing_data","missing_tool","noop","update_issue"]}]}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c01551fcc9ed737db2b8f9e8ffc782c6e00ffca3f4c83441c034382002a91341","body_hash":"15a99df84d881d25a2fbfb75d09e056c9d9409bab6b3c9d5e2f02d551eb28891","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}}
+# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"skills":[".github/skills/style-guide"],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.12","digest":"sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.12@sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"safeoutputs","tools":["create_issue","missing_data","missing_tool","noop","publish_component_findings","update_issue"]}]}
# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
# ___ _ _
@@ -298,7 +298,7 @@ jobs:
GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
GH_AW_PROMPT_CONTENT_0000: "\n"
- GH_AW_PROMPT_CONTENT_0001: "\nTools: create_issue, update_issue, missing_tool, missing_data, noop\n"
+ GH_AW_PROMPT_CONTENT_0001: "\nTools: create_issue, update_issue, missing_tool, missing_data, noop, publish_component_findings\n"
GH_AW_PROMPT_CONTENT_0002: "\n"
GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n"
GH_AW_PROMPT_CONTENT_0004: "\n"
@@ -564,7 +564,7 @@ jobs:
env:
GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw"
GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}"
- GH_AW_SAFE_OUTPUTS_CONFIG: "{\"create_issue\":{\"deduplicate_by_title\":true,\"max\":1},\"create_report_incomplete_issue\":{},\"mentions\":{\"enabled\":false},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"required_title_prefix\":\"Primer API Review\",\"target\":\"*\"}}"
+ GH_AW_SAFE_OUTPUTS_CONFIG: "{\"create_issue\":{\"deduplicate_by_title\":true,\"footer\":false,\"max\":1},\"create_report_incomplete_issue\":{},\"mentions\":{\"enabled\":false},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"publish_component_findings\":{\"description\":\"Upsert one component finding comment and resolve its checklist links on the Primer API Review issue\",\"inputs\":{\"body\":{\"default\":null,\"description\":\"Complete component comment with findings grouped under headings and evidence tables\",\"required\":true,\"type\":\"string\"},\"component\":{\"default\":null,\"description\":\"Component name used in the api-review-comment-COMPONENT checklist link\",\"required\":true,\"type\":\"string\"},\"issue_number\":{\"default\":null,\"description\":\"Review issue number, or aw_review for the issue created in this run\",\"required\":true,\"type\":\"string\"}}},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"footer\":false,\"max\":1,\"required_title_prefix\":\"Primer API Review\",\"target\":\"*\"}}"
with:
script: |
const path = require('path');
@@ -582,7 +582,35 @@ jobs:
"update_issue": " CONSTRAINTS: Maximum 1 issue(s) can be updated. Target: *. The target issue title must start with \"Primer API Review\"."
},
"repo_params": {},
- "dynamic_tools": []
+ "dynamic_tools": [
+ {
+ "description": "Upsert one component finding comment and resolve its checklist links on the Primer API Review issue",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "body": {
+ "description": "Complete component comment with findings grouped under headings and evidence tables",
+ "type": "string"
+ },
+ "component": {
+ "description": "Component name used in the api-review-comment-COMPONENT checklist link",
+ "type": "string"
+ },
+ "issue_number": {
+ "description": "Review issue number, or aw_review for the issue created in this run",
+ "type": "string"
+ }
+ },
+ "required": [
+ "body",
+ "component",
+ "issue_number"
+ ],
+ "type": "object"
+ },
+ "name": "publish_component_findings"
+ }
+ ]
}
GH_AW_VALIDATION_JSON: |
{
@@ -1819,6 +1847,73 @@ jobs:
GH_HOST="${GITHUB_SERVER_URL#https://}"
GH_HOST="${GH_HOST#http://}"
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
+ - name: Configure Safe Output Scripts
+ run: |
+ cat > "${RUNNER_TEMP}/gh-aw/actions/safe_output_script_publish_component_findings.cjs" << 'GH_AW_SAFE_OUTPUT_SCRIPT_PUBLISH_COMPONENT_FINDINGS_3ee6c007f66fb2b9_EOF'
+ // @ts-check
+ ///
+ // Auto-generated safe-output script handler: publish-component-findings
+
+ const { sanitizeContent } = require("./sanitize_content.cjs");
+
+ /** @type {import('./types/safe-output-script').SafeOutputScriptMain} */
+ async function main(config = {}) {
+ const { body, component, issue_number } = config;
+ return async function handlePublishComponentFindings(item, resolvedTemporaryIds, temporaryIdMap) {
+ const {matchesWorkflowId, generateWorkflowIdMarker} = require('./generate_footer.cjs')
+ config.publishCount = (config.publishCount || 0) + 1
+ if (config.publishCount > 100) {
+ throw new Error('At most 100 component comments may be published per run')
+ }
+ const repo = context.repo
+ const resolved = resolvedTemporaryIds[item.issue_number]
+ const issueNumber = Number(resolved ? resolved.number : item.issue_number)
+ if (resolved && resolved.repo !== `${repo.owner}/${repo.repo}`) {
+ throw new Error('Cross-repository review targets are not allowed')
+ }
+ if (!Number.isSafeInteger(issueNumber) || issueNumber <= 0 ||
+ typeof item.component !== 'string' || !/^[A-Za-z][A-Za-z0-9.]*$/.test(item.component) ||
+ typeof item.body !== 'string' || item.body.length < 20 || item.body.length > 60000) {
+ throw new Error('Invalid component review payload')
+ }
+ if (process.env.GH_AW_SAFE_OUTPUTS_STAGED === 'true') {
+ core.info(`Would publish findings for ${item.component} on issue ${issueNumber}`)
+ return {success: true}
+ }
+ const {data: issue} = await github.rest.issues.get({...repo, issue_number: issueNumber})
+ if (issue.pull_request || issue.title !== 'Primer API Review') {
+ throw new Error('Target must be the Primer API Review issue')
+ }
+ const marker = ``
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ ...repo, issue_number: issueNumber, per_page: 100,
+ })
+ const previous = comments.find(comment =>
+ comment.user?.login === 'github-actions[bot]' &&
+ matchesWorkflowId(comment.body || '', 'primer-api-review') &&
+ (comment.body || '').includes(marker),
+ )
+ const body = `${sanitizeContent(item.body)}\n\n${marker}\n${generateWorkflowIdMarker('primer-api-review')}`
+ let comment = previous
+ if (!previous || previous.body !== body) {
+ const result = previous
+ ? await github.rest.issues.updateComment({...repo, comment_id: previous.id, body})
+ : await github.rest.issues.createComment({...repo, issue_number: issueNumber, body})
+ comment = result.data
+ }
+ const updatedBody = (issue.body || '').replaceAll(
+ `(#api-review-comment-${item.component})`, `(${comment.html_url})`,
+ )
+ if (updatedBody !== issue.body) {
+ await github.rest.issues.update({...repo, issue_number: issueNumber, body: updatedBody})
+ }
+ return {success: true, url: comment.html_url}
+
+ };
+ }
+ module.exports = { main };
+
+ GH_AW_SAFE_OUTPUT_SCRIPT_PUBLISH_COMPONENT_FINDINGS_3ee6c007f66fb2b9_EOF
- name: Process Safe Outputs
id: process_safe_outputs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -1828,7 +1923,8 @@ jobs:
GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_API_URL: ${{ github.api_url }}
- GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"deduplicate_by_title\":true,\"max\":1},\"create_report_incomplete_issue\":{},\"mentions\":{\"enabled\":false},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"required_title_prefix\":\"Primer API Review\",\"target\":\"*\"}}"
+ GH_AW_SAFE_OUTPUT_SCRIPTS: "{\"publish_component_findings\":\"safe_output_script_publish_component_findings.cjs\"}"
+ GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"deduplicate_by_title\":true,\"footer\":false,\"max\":1},\"create_report_incomplete_issue\":{},\"mentions\":{\"enabled\":false},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"footer\":false,\"max\":1,\"required_title_prefix\":\"Primer API Review\",\"target\":\"*\"}}"
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
diff --git a/.github/workflows/primer-api-review.md b/.github/workflows/primer-api-review.md
index dbc26205ef9..b2604a55fc4 100644
--- a/.github/workflows/primer-api-review.md
+++ b/.github/workflows/primer-api-review.md
@@ -49,12 +49,78 @@ skills:
- .github/skills/style-guide
safe-outputs:
mentions: false
+ footer: false
create-issue:
deduplicate-by-title: true
max: 1
update-issue:
target: '*'
required-title-prefix: 'Primer API Review'
+ scripts:
+ publish-component-findings:
+ description: Upsert one component finding comment and resolve its checklist links on the Primer API Review issue
+ inputs:
+ issue_number:
+ type: string
+ required: true
+ description: Review issue number, or aw_review for the issue created in this run
+ component:
+ type: string
+ required: true
+ description: Component name used in the api-review-comment-COMPONENT checklist link
+ body:
+ type: string
+ required: true
+ description: Complete component comment with findings grouped under headings and evidence tables
+ script: |
+ const {matchesWorkflowId, generateWorkflowIdMarker} = require('./generate_footer.cjs')
+ config.publishCount = (config.publishCount || 0) + 1
+ if (config.publishCount > 100) {
+ throw new Error('At most 100 component comments may be published per run')
+ }
+ const repo = context.repo
+ const resolved = resolvedTemporaryIds[item.issue_number]
+ const issueNumber = Number(resolved ? resolved.number : item.issue_number)
+ if (resolved && resolved.repo !== `${repo.owner}/${repo.repo}`) {
+ throw new Error('Cross-repository review targets are not allowed')
+ }
+ if (!Number.isSafeInteger(issueNumber) || issueNumber <= 0 ||
+ typeof item.component !== 'string' || !/^[A-Za-z][A-Za-z0-9.]*$/.test(item.component) ||
+ typeof item.body !== 'string' || item.body.length < 20 || item.body.length > 60000) {
+ throw new Error('Invalid component review payload')
+ }
+ if (process.env.GH_AW_SAFE_OUTPUTS_STAGED === 'true') {
+ core.info(`Would publish findings for ${item.component} on issue ${issueNumber}`)
+ return {success: true}
+ }
+ const {data: issue} = await github.rest.issues.get({...repo, issue_number: issueNumber})
+ if (issue.pull_request || issue.title !== 'Primer API Review') {
+ throw new Error('Target must be the Primer API Review issue')
+ }
+ const marker = ``
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ ...repo, issue_number: issueNumber, per_page: 100,
+ })
+ const previous = comments.find(comment =>
+ comment.user?.login === 'github-actions[bot]' &&
+ matchesWorkflowId(comment.body || '', 'primer-api-review') &&
+ (comment.body || '').includes(marker),
+ )
+ const body = `${sanitizeContent(item.body)}\n\n${marker}\n${generateWorkflowIdMarker('primer-api-review')}`
+ let comment = previous
+ if (!previous || previous.body !== body) {
+ const result = previous
+ ? await github.rest.issues.updateComment({...repo, comment_id: previous.id, body})
+ : await github.rest.issues.createComment({...repo, issue_number: issueNumber, body})
+ comment = result.data
+ }
+ const updatedBody = (issue.body || '').replaceAll(
+ `(#api-review-comment-${item.component})`, `(${comment.html_url})`,
+ )
+ if (updatedBody !== issue.body) {
+ await github.rest.issues.update({...repo, issue_number: issueNumber, body: updatedBody})
+ }
+ return {success: true, url: comment.html_url}
---
# Primer API Review
@@ -117,11 +183,14 @@ unresolved component API deviations from the Primer React style guide.
- cite the exact style-guide principle
- cite repository file paths and line numbers
- describe the smallest consumer-facing API change that would resolve it
-6. Read `/tmp/gh-aw/data/existing-review.json`. When a prior finding appears in
- the existing issue, inspect the issue comments for a clear, substantive
+6. Read `/tmp/gh-aw/data/existing-review.json` and all comments on that issue,
+ paginating if needed. Findings may be in the legacy issue body or in managed
+ component comments. When a prior finding appears, inspect the other issue
+ comments for a clear, substantive
explanation of why that API intentionally exists. If a comment is tied to
that finding and provides a reason, omit the finding entirely. Do not treat
- an acknowledgement, question, or unrelated comment as a reason.
+ an acknowledgement, question, unrelated comment, or the workflow's own
+ finding/recommendation text as a reason.
7. Merge duplicate findings and discard anything speculative, stylistic but not
covered by the guide, or unsupported by source evidence.
8. Reconcile the coverage matrix against the complete inventory and checklist
@@ -135,44 +204,93 @@ unresolved component API deviations from the Primer React style guide.
## Issue output
-Build a complete replacement body using GitHub-flavored Markdown:
+Build the issue body as an overview, not a dump of findings. Use GitHub-flavored
+Markdown and start sections at `###`:
-- Start sections at `###`.
-- Include a short summary with the review date and fully reviewed component count
- out of the total inventory. Distinguish a full audit from a partial audit.
-- Include a compact coverage table by principle with counts of `pass`, `finding`,
+- Limit the summary to at most two sentences, including the review date, fully
+ reviewed component count out of the inventory, and whether the audit is partial
+ or complete.
+- Under `### Proposed API changes`, include an unchecked task-list item for each
+ proposed API change, naming the component/API and linking to its finding
+ comment. Use the finding heading as the link text when a component has multiple
+ findings. Do not check a proposal merely because it was reviewed.
+- Keep evidence, impact, and recommendation details in the component comments,
+ not in the overview checklist.
+- Put run details and the coverage table inside
+ `Run details and coverage
`. Include counts of `pass`, `finding`,
`not-applicable`, and `not-reviewed` components for this run. Each row must
account for the entire inventory; retained historical findings do not count
as reviewed cells.
State whether sub-agents, direct inspection, or both supplied the evidence,
including any failed batches and recovery performed.
-- For a partial audit, include a remaining-coverage list grouped by principle
+- For a partial audit, put the remaining-coverage list inside
+ `Remaining coverage
`, grouped by principle
with component names and the next bounded batch to inspect. Prioritize gaps
from the previous run that remain unreviewed before newly introduced gaps.
-- Group findings by style-guide principle.
-- For each finding, include the component/API, evidence, impact, and recommended
- change.
+- Put retained findings not rechecked in this run behind
+ `Past findings not rechecked
`, preserving their
+ comment links and unresolved status rather than silently dropping them.
- State that the full review found no unexplained deviations only when coverage
is complete and there are no unresolved findings. When a partial audit has no
new findings, state that no new deviations were found in the inspected subset
without implying the unreviewed APIs passed.
-- Include the workflow run as
+- Inside the run-details disclosure, include the workflow run as
`[§${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})`.
- Do not include findings that have a documented rationale in issue comments.
+- Close every disclosure with ` ` and leave blank lines around its
+ Markdown content so GitHub renders tables and lists correctly.
- Do not append an unbounded run history or copy comment discussions into the
- issue body.
+ issue body or component comments.
+
+### Component finding comments
+
+Maintain one managed comment per component with findings, grouping its findings
+under separate `####` headings below `### ComponentName`. Every finding must
+belong to that component, identify its public API and style-guide principle, and
+use a table with `Evidence`, `Impact`, and `Recommendation` columns. Evidence
+must link to the source file and line range and the exact style-guide principle;
+recommendations must describe the proposed consumer-facing API change.
+
+Keep active findings visible. Place retained findings not rechecked this run in
+a `Past findings not rechecked
` block within the
+component comment. When source inspection resolves a previously published
+finding, move a brief status and its evidence into
+`Resolved findings
` rather than presenting it as an
+active recommendation. Omit findings with a documented rationale as described
+above. Never modify human comments or publish comments for components with no
+current or previously published findings.
+
+Use `publish_component_findings` with the component name and its complete comment
+body; the safe-output handler maintains the component marker, reuses the managed
+comment, skips unchanged content, and inserts its real URL into the checklist.
+Do not supply comment IDs or invent comment URLs. For a new or updated component
+comment, use `(#api-review-comment-ComponentName)` as the checklist link target.
+Use actual existing comment URLs for unchanged comments. Multiple findings for
+one component may link to the same comment, with distinct finding labels.
If `/tmp/gh-aw/data/existing-review.json` contains an issue:
-- update that issue's body with `update_issue`
+- first queue that issue's complete replacement overview body with `update_issue`
+ and `operation: replace` (never append a second checklist)
- keep the title exactly `Primer API Review`
- reopen it if it is closed
-Otherwise, create one issue with `create_issue`, the exact title
-`Primer API Review`, and the generated body.
+Otherwise, first queue one issue with `create_issue`, the exact title
+`Primer API Review`, `temporary_id: aw_review`, and the overview body.
+
+Then queue `publish_component_findings` for each new or changed component comment
+(at most one per component and 100 per run), using the existing issue number as
+a string or `aw_review` for the newly created issue. Also queue it for any
+component whose checklist links use placeholders, even if its comment is
+unchanged. Safe outputs execute after the agent finishes, so do not try to read
+back newly queued comments during this run. Queue the overview before the
+component publishers; do not queue another overview update afterward that would
+overwrite resolved links. If the publication cap is reached, retain existing
+links and list unpublished components in the collapsed remaining-coverage block
+without emitting dangling placeholders.
-Perform exactly one visible issue action per run. Never create a second review
-issue when an exact-title issue exists. Use `noop` with a short reason only when
+Never create a second review issue when an exact-title issue exists. Use `noop`
+with a short reason only when
no trustworthy progress can be published (for example, source or prior issue
data is unavailable and no safe update is possible). Incomplete coverage or
failed delegation alone is not a reason to use `noop`: verified existing
diff --git a/script/primer-api-review.test.mjs b/script/primer-api-review.test.mjs
new file mode 100644
index 00000000000..a6c1d431c74
--- /dev/null
+++ b/script/primer-api-review.test.mjs
@@ -0,0 +1,164 @@
+/* eslint camelcase: ["error", {allow: ["pull_request", "html_url", "issue_number", "aw_review"]}] */
+import assert from 'node:assert/strict'
+import {readFileSync} from 'node:fs'
+import {test} from 'node:test'
+
+const workflow = readFileSync(new URL('../.github/workflows/primer-api-review.md', import.meta.url), 'utf8')
+const script = workflow
+ .match(/ {6}script: \|\n([\s\S]*?)\n---/)[1]
+ .split('\n')
+ .map(line => line.slice(8))
+ .join('\n')
+const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
+const publish = new AsyncFunction(
+ 'item',
+ 'resolvedTemporaryIds',
+ 'config',
+ 'context',
+ 'github',
+ 'core',
+ 'process',
+ 'require',
+ 'sanitizeContent',
+ script,
+)
+const workflowMarker = ''
+const componentMarker = ''
+const commentUrl = 'https://github.com/primer/react/issues/8384#issuecomment-123'
+const commentBody = '### Button\n\nEvidence-backed API findings'
+
+function setup({comments = [], title = 'Primer API Review', pullRequest, staged = false} = {}) {
+ const calls = []
+ const issue = {
+ title,
+ pull_request: pullRequest,
+ body: '- [ ] [Button API change](#api-review-comment-Button)',
+ }
+ const config = {}
+ const issues = {
+ get: async () => ({data: issue}),
+ listComments: () => {},
+ createComment: async args => {
+ calls.push(['createComment', args])
+ const comment = {
+ id: 123 + comments.length,
+ html_url: commentUrl.replace('123', String(123 + comments.length)),
+ user: {login: 'github-actions[bot]'},
+ body: args.body,
+ }
+ comments.push(comment)
+ return {data: comment}
+ },
+ updateComment: async args => {
+ calls.push(['updateComment', args])
+ const comment = comments.find(value => value.id === args.comment_id)
+ comment.body = args.body
+ return {data: comment}
+ },
+ update: async args => {
+ calls.push(['update', args])
+ issue.body = args.body
+ },
+ }
+ return {
+ calls,
+ issue,
+ config,
+ run: (item = {}, resolved = {}) =>
+ publish(
+ {issue_number: '8384', component: 'Button', body: commentBody, ...item},
+ resolved,
+ config,
+ {repo: {owner: 'primer', repo: 'react'}},
+ {rest: {issues}, paginate: async () => comments},
+ {info: () => {}},
+ {env: {GH_AW_SAFE_OUTPUTS_STAGED: String(staged)}},
+ () => ({
+ matchesWorkflowId: body => body.includes(workflowMarker),
+ generateWorkflowIdMarker: () => workflowMarker,
+ }),
+ body => `sanitized:${body}`,
+ ),
+ }
+}
+
+test('creates a sanitized component comment and replaces every matching checklist link', async () => {
+ const fixture = setup()
+ fixture.issue.body += '\n- [ ] [Another finding](#api-review-comment-Button)'
+ await fixture.run()
+ assert.deepEqual(
+ fixture.calls.map(([name]) => name),
+ ['createComment', 'update'],
+ )
+ assert.equal(fixture.calls[0][1].body, `sanitized:${commentBody}\n\n${componentMarker}\n${workflowMarker}`)
+ assert.equal(fixture.issue.body.split(commentUrl).length - 1, 2)
+ await fixture.run()
+ assert.equal(fixture.calls.length, 2, 'unchanged reruns must not create comments or rewrite the issue')
+})
+
+test('updates only its own managed component comment', async () => {
+ const comments = [
+ {id: 1, user: {login: 'maintainer'}, body: `${componentMarker}\n${workflowMarker}`},
+ {id: 2, user: {login: 'github-actions[bot]'}, body: componentMarker},
+ {id: 3, user: {login: 'github-actions[bot]'}, body: `${componentMarker}\n${workflowMarker}`, html_url: commentUrl},
+ ]
+ const fixture = setup({comments})
+ await fixture.run()
+ assert.equal(fixture.calls[0][0], 'updateComment')
+ assert.equal(fixture.calls[0][1].comment_id, 3)
+ assert.equal(comments[0].body, `${componentMarker}\n${workflowMarker}`)
+ assert.equal(comments[1].body, componentMarker)
+})
+
+test('resolves a newly created issue temporary ID in this repository', async () => {
+ const fixture = setup()
+ await fixture.run({issue_number: 'aw_review'}, {aw_review: {repo: 'primer/react', number: 8384}})
+ assert.equal(fixture.calls[0][1].issue_number, 8384)
+})
+
+test('rejects invalid targets and payloads without writing', async () => {
+ for (const options of [{title: 'Another issue'}, {pullRequest: {url: 'pr'}}]) {
+ const fixture = setup(options)
+ await assert.rejects(fixture.run(), /Target must be/)
+ assert.equal(fixture.calls.length, 0)
+ }
+ for (const item of [{issue_number: '-1'}, {component: '../Button'}, {component: undefined}, {body: ''}]) {
+ const fixture = setup()
+ await assert.rejects(fixture.run(item), /Invalid component review payload/)
+ assert.equal(fixture.calls.length, 0)
+ }
+ const fixture = setup()
+ await assert.rejects(
+ fixture.run({issue_number: 'aw_review'}, {aw_review: {repo: 'primer/other', number: 8384}}),
+ /Cross-repository/,
+ )
+ assert.equal(fixture.calls.length, 0)
+})
+
+test('respects staged mode and the per-run publication cap', async () => {
+ const fixture = setup({staged: true})
+ await fixture.run()
+ assert.equal(fixture.calls.length, 0)
+ fixture.config.publishCount = 100
+ await assert.rejects(fixture.run(), /At most 100/)
+ assert.equal(fixture.calls.length, 0)
+})
+
+test('successive component publications preserve other checklist links', async () => {
+ const fixture = setup()
+ fixture.issue.body += '\n- [ ] [Dialog change](#api-review-comment-Dialog)'
+ await fixture.run()
+ await fixture.run({component: 'Dialog'})
+ assert.equal(fixture.issue.body.includes('#api-review-comment-'), false)
+ assert.equal(
+ fixture.issue.body,
+ `- [ ] [Button API change](${commentUrl})\n- [ ] [Dialog change](${commentUrl.replace('123', '124')})`,
+ )
+ assert.equal(fixture.calls.filter(([name]) => name === 'createComment').length, 2)
+})
+
+test('overview instructions replace instead of append and precede comment publication', () => {
+ assert.match(workflow, /`update_issue`\n\s+and `operation: replace`/)
+ assert.match(workflow, /Queue the overview before the\ncomponent publishers/)
+ assert.match(workflow, /at most two sentences/)
+})
From 6b63004124b8b5c0658dfa7ea6c8ce2d7e526200 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 9 Sep 2026 16:57:30 +0000
Subject: [PATCH 4/5] Support staged component findings for newly created
review issues
Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com>
---
.github/workflows/primer-api-review.lock.yml | 16 +++++++++-------
.github/workflows/primer-api-review.md | 10 ++++++----
script/primer-api-review.test.mjs | 1 +
3 files changed, 16 insertions(+), 11 deletions(-)
diff --git a/.github/workflows/primer-api-review.lock.yml b/.github/workflows/primer-api-review.lock.yml
index cc0dc38dd43..f5f2040256b 100644
--- a/.github/workflows/primer-api-review.lock.yml
+++ b/.github/workflows/primer-api-review.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c01551fcc9ed737db2b8f9e8ffc782c6e00ffca3f4c83441c034382002a91341","body_hash":"15a99df84d881d25a2fbfb75d09e056c9d9409bab6b3c9d5e2f02d551eb28891","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"0f78433e1193601a540e10a90add617d942958e4da7100307ed6c002cdd219be","body_hash":"15a99df84d881d25a2fbfb75d09e056c9d9409bab6b3c9d5e2f02d551eb28891","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}}
# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"skills":[".github/skills/style-guide"],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.12","digest":"sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.12@sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"safeoutputs","tools":["create_issue","missing_data","missing_tool","noop","publish_component_findings","update_issue"]}]}
# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
@@ -1849,7 +1849,7 @@ jobs:
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
- name: Configure Safe Output Scripts
run: |
- cat > "${RUNNER_TEMP}/gh-aw/actions/safe_output_script_publish_component_findings.cjs" << 'GH_AW_SAFE_OUTPUT_SCRIPT_PUBLISH_COMPONENT_FINDINGS_3ee6c007f66fb2b9_EOF'
+ cat > "${RUNNER_TEMP}/gh-aw/actions/safe_output_script_publish_component_findings.cjs" << 'GH_AW_SAFE_OUTPUT_SCRIPT_PUBLISH_COMPONENT_FINDINGS_42efc8e50632612b_EOF'
// @ts-check
///
// Auto-generated safe-output script handler: publish-component-findings
@@ -1868,17 +1868,19 @@ jobs:
const repo = context.repo
const resolved = resolvedTemporaryIds[item.issue_number]
const issueNumber = Number(resolved ? resolved.number : item.issue_number)
+ const staged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === 'true'
+ const pendingStagedIssue = staged && !resolved && item.issue_number === 'aw_review'
if (resolved && resolved.repo !== `${repo.owner}/${repo.repo}`) {
throw new Error('Cross-repository review targets are not allowed')
}
- if (!Number.isSafeInteger(issueNumber) || issueNumber <= 0 ||
+ if ((!pendingStagedIssue && (!Number.isSafeInteger(issueNumber) || issueNumber <= 0)) ||
typeof item.component !== 'string' || !/^[A-Za-z][A-Za-z0-9.]*$/.test(item.component) ||
typeof item.body !== 'string' || item.body.length < 20 || item.body.length > 60000) {
throw new Error('Invalid component review payload')
}
- if (process.env.GH_AW_SAFE_OUTPUTS_STAGED === 'true') {
- core.info(`Would publish findings for ${item.component} on issue ${issueNumber}`)
- return {success: true}
+ if (staged) {
+ core.info(`Would publish findings for ${item.component} on issue ${item.issue_number}`)
+ return {success: true, staged: true}
}
const {data: issue} = await github.rest.issues.get({...repo, issue_number: issueNumber})
if (issue.pull_request || issue.title !== 'Primer API Review') {
@@ -1913,7 +1915,7 @@ jobs:
}
module.exports = { main };
- GH_AW_SAFE_OUTPUT_SCRIPT_PUBLISH_COMPONENT_FINDINGS_3ee6c007f66fb2b9_EOF
+ GH_AW_SAFE_OUTPUT_SCRIPT_PUBLISH_COMPONENT_FINDINGS_42efc8e50632612b_EOF
- name: Process Safe Outputs
id: process_safe_outputs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
diff --git a/.github/workflows/primer-api-review.md b/.github/workflows/primer-api-review.md
index b2604a55fc4..9c74f2d9367 100644
--- a/.github/workflows/primer-api-review.md
+++ b/.github/workflows/primer-api-review.md
@@ -81,17 +81,19 @@ safe-outputs:
const repo = context.repo
const resolved = resolvedTemporaryIds[item.issue_number]
const issueNumber = Number(resolved ? resolved.number : item.issue_number)
+ const staged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === 'true'
+ const pendingStagedIssue = staged && !resolved && item.issue_number === 'aw_review'
if (resolved && resolved.repo !== `${repo.owner}/${repo.repo}`) {
throw new Error('Cross-repository review targets are not allowed')
}
- if (!Number.isSafeInteger(issueNumber) || issueNumber <= 0 ||
+ if ((!pendingStagedIssue && (!Number.isSafeInteger(issueNumber) || issueNumber <= 0)) ||
typeof item.component !== 'string' || !/^[A-Za-z][A-Za-z0-9.]*$/.test(item.component) ||
typeof item.body !== 'string' || item.body.length < 20 || item.body.length > 60000) {
throw new Error('Invalid component review payload')
}
- if (process.env.GH_AW_SAFE_OUTPUTS_STAGED === 'true') {
- core.info(`Would publish findings for ${item.component} on issue ${issueNumber}`)
- return {success: true}
+ if (staged) {
+ core.info(`Would publish findings for ${item.component} on issue ${item.issue_number}`)
+ return {success: true, staged: true}
}
const {data: issue} = await github.rest.issues.get({...repo, issue_number: issueNumber})
if (issue.pull_request || issue.title !== 'Primer API Review') {
diff --git a/script/primer-api-review.test.mjs b/script/primer-api-review.test.mjs
index a6c1d431c74..8972919b93d 100644
--- a/script/primer-api-review.test.mjs
+++ b/script/primer-api-review.test.mjs
@@ -138,6 +138,7 @@ test('rejects invalid targets and payloads without writing', async () => {
test('respects staged mode and the per-run publication cap', async () => {
const fixture = setup({staged: true})
await fixture.run()
+ await fixture.run({issue_number: 'aw_review'})
assert.equal(fixture.calls.length, 0)
fixture.config.publishCount = 100
await assert.rejects(fixture.run(), /At most 100/)
From b73a244f051a66a85140cbde78c52373645bf4a0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 9 Sep 2026 22:03:15 +0000
Subject: [PATCH 5/5] Publish component findings in one ingestion-safe bounded
batch
Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com>
---
.github/workflows/primer-api-review.lock.yml | 87 ++++++------
.github/workflows/primer-api-review.md | 101 ++++++++------
script/primer-api-review.test.mjs | 134 +++++++++++++++++--
3 files changed, 233 insertions(+), 89 deletions(-)
diff --git a/.github/workflows/primer-api-review.lock.yml b/.github/workflows/primer-api-review.lock.yml
index f5f2040256b..309dae9b211 100644
--- a/.github/workflows/primer-api-review.lock.yml
+++ b/.github/workflows/primer-api-review.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"0f78433e1193601a540e10a90add617d942958e4da7100307ed6c002cdd219be","body_hash":"15a99df84d881d25a2fbfb75d09e056c9d9409bab6b3c9d5e2f02d551eb28891","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"149c1b91ae1410c0e9cd931a0773a390482432f4a0b17bac7d1e4cf7b4353a5f","body_hash":"d851a45338e8ee3368c9dfec3e1d7b5d9c7ac3c41008817b4b7b040858c53907","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}}
# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"skills":[".github/skills/style-guide"],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.12","digest":"sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.12@sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"safeoutputs","tools":["create_issue","missing_data","missing_tool","noop","publish_component_findings","update_issue"]}]}
# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
@@ -564,7 +564,7 @@ jobs:
env:
GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw"
GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}"
- GH_AW_SAFE_OUTPUTS_CONFIG: "{\"create_issue\":{\"deduplicate_by_title\":true,\"footer\":false,\"max\":1},\"create_report_incomplete_issue\":{},\"mentions\":{\"enabled\":false},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"publish_component_findings\":{\"description\":\"Upsert one component finding comment and resolve its checklist links on the Primer API Review issue\",\"inputs\":{\"body\":{\"default\":null,\"description\":\"Complete component comment with findings grouped under headings and evidence tables\",\"required\":true,\"type\":\"string\"},\"component\":{\"default\":null,\"description\":\"Component name used in the api-review-comment-COMPONENT checklist link\",\"required\":true,\"type\":\"string\"},\"issue_number\":{\"default\":null,\"description\":\"Review issue number, or aw_review for the issue created in this run\",\"required\":true,\"type\":\"string\"}}},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"footer\":false,\"max\":1,\"required_title_prefix\":\"Primer API Review\",\"target\":\"*\"}}"
+ GH_AW_SAFE_OUTPUTS_CONFIG: "{\"create_issue\":{\"deduplicate_by_title\":true,\"footer\":false,\"max\":1},\"create_report_incomplete_issue\":{},\"mentions\":{\"enabled\":false},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"publish_component_findings\":{\"description\":\"Publish one batch of up to 100 component finding comments and resolve their checklist links\",\"inputs\":{\"comments\":{\"default\":null,\"description\":\"Base64-encoded UTF-8 JSON array of objects with component (name) and body (complete finding comment), at most 100 unique components\",\"required\":true,\"type\":\"string\"},\"issue_number\":{\"default\":null,\"description\":\"Review issue number, or aw_review for the issue created in this run\",\"required\":true,\"type\":\"string\"}}},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"footer\":false,\"max\":1,\"required_title_prefix\":\"Primer API Review\",\"target\":\"*\"}}"
with:
script: |
const path = require('path');
@@ -584,16 +584,12 @@ jobs:
"repo_params": {},
"dynamic_tools": [
{
- "description": "Upsert one component finding comment and resolve its checklist links on the Primer API Review issue",
+ "description": "Publish one batch of up to 100 component finding comments and resolve their checklist links",
"inputSchema": {
"additionalProperties": false,
"properties": {
- "body": {
- "description": "Complete component comment with findings grouped under headings and evidence tables",
- "type": "string"
- },
- "component": {
- "description": "Component name used in the api-review-comment-COMPONENT checklist link",
+ "comments": {
+ "description": "Base64-encoded UTF-8 JSON array of objects with component (name) and body (complete finding comment), at most 100 unique components",
"type": "string"
},
"issue_number": {
@@ -602,8 +598,7 @@ jobs:
}
},
"required": [
- "body",
- "component",
+ "comments",
"issue_number"
],
"type": "object"
@@ -1849,7 +1844,7 @@ jobs:
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
- name: Configure Safe Output Scripts
run: |
- cat > "${RUNNER_TEMP}/gh-aw/actions/safe_output_script_publish_component_findings.cjs" << 'GH_AW_SAFE_OUTPUT_SCRIPT_PUBLISH_COMPONENT_FINDINGS_42efc8e50632612b_EOF'
+ cat > "${RUNNER_TEMP}/gh-aw/actions/safe_output_script_publish_component_findings.cjs" << 'GH_AW_SAFE_OUTPUT_SCRIPT_PUBLISH_COMPONENT_FINDINGS_75faeab9d5765245_EOF'
// @ts-check
///
// Auto-generated safe-output script handler: publish-component-findings
@@ -1858,13 +1853,9 @@ jobs:
/** @type {import('./types/safe-output-script').SafeOutputScriptMain} */
async function main(config = {}) {
- const { body, component, issue_number } = config;
+ const { comments, issue_number } = config;
return async function handlePublishComponentFindings(item, resolvedTemporaryIds, temporaryIdMap) {
const {matchesWorkflowId, generateWorkflowIdMarker} = require('./generate_footer.cjs')
- config.publishCount = (config.publishCount || 0) + 1
- if (config.publishCount > 100) {
- throw new Error('At most 100 component comments may be published per run')
- }
const repo = context.repo
const resolved = resolvedTemporaryIds[item.issue_number]
const issueNumber = Number(resolved ? resolved.number : item.issue_number)
@@ -1874,48 +1865,68 @@ jobs:
throw new Error('Cross-repository review targets are not allowed')
}
if ((!pendingStagedIssue && (!Number.isSafeInteger(issueNumber) || issueNumber <= 0)) ||
- typeof item.component !== 'string' || !/^[A-Za-z][A-Za-z0-9.]*$/.test(item.component) ||
- typeof item.body !== 'string' || item.body.length < 20 || item.body.length > 60000) {
+ typeof item.comments !== 'string' || item.comments.length > 500000) {
throw new Error('Invalid component review payload')
}
+ const decoded = Buffer.from(item.comments, 'base64')
+ if (decoded.toString('base64') !== item.comments) {
+ throw new Error('Invalid base64 component review payload')
+ }
+ const entries = JSON.parse(decoded.toString('utf8'))
+ if (!Array.isArray(entries) || entries.length === 0 || entries.length > 100) {
+ throw new Error('Batch must contain between 1 and 100 component comments')
+ }
+ const components = new Set()
+ const batch = entries.map(entry => {
+ if (!entry || typeof entry.component !== 'string' ||
+ !/^[A-Za-z][A-Za-z0-9.]*$/.test(entry.component) || components.has(entry.component) ||
+ typeof entry.body !== 'string' || entry.body.length < 20 || entry.body.length > 60000) {
+ throw new Error('Invalid or duplicate component review entry')
+ }
+ components.add(entry.component)
+ return {component: entry.component, body: sanitizeContent(entry.body)}
+ })
if (staged) {
- core.info(`Would publish findings for ${item.component} on issue ${item.issue_number}`)
+ core.info(`Would publish ${batch.length} component comments on issue ${item.issue_number}`)
return {success: true, staged: true}
}
const {data: issue} = await github.rest.issues.get({...repo, issue_number: issueNumber})
if (issue.pull_request || issue.title !== 'Primer API Review') {
throw new Error('Target must be the Primer API Review issue')
}
- const marker = ``
const comments = await github.paginate(github.rest.issues.listComments, {
...repo, issue_number: issueNumber, per_page: 100,
})
- const previous = comments.find(comment =>
- comment.user?.login === 'github-actions[bot]' &&
- matchesWorkflowId(comment.body || '', 'primer-api-review') &&
- (comment.body || '').includes(marker),
- )
- const body = `${sanitizeContent(item.body)}\n\n${marker}\n${generateWorkflowIdMarker('primer-api-review')}`
- let comment = previous
- if (!previous || previous.body !== body) {
- const result = previous
- ? await github.rest.issues.updateComment({...repo, comment_id: previous.id, body})
- : await github.rest.issues.createComment({...repo, issue_number: issueNumber, body})
- comment = result.data
+ let updatedBody = issue.body || ''
+ for (const entry of batch) {
+ const marker = ``
+ const previous = comments.find(comment =>
+ comment.user?.login === 'github-actions[bot]' &&
+ matchesWorkflowId(comment.body || '', 'primer-api-review') &&
+ (comment.body || '').includes(marker),
+ )
+ const body = `${entry.body}\n\n${marker}\n${generateWorkflowIdMarker('primer-api-review')}`
+ let comment = previous
+ if (!previous || previous.body !== body) {
+ const result = previous
+ ? await github.rest.issues.updateComment({...repo, comment_id: previous.id, body})
+ : await github.rest.issues.createComment({...repo, issue_number: issueNumber, body})
+ comment = result.data
+ }
+ updatedBody = updatedBody.replaceAll(
+ `(#api-review-comment-${entry.component})`, `(${comment.html_url})`,
+ )
}
- const updatedBody = (issue.body || '').replaceAll(
- `(#api-review-comment-${item.component})`, `(${comment.html_url})`,
- )
if (updatedBody !== issue.body) {
await github.rest.issues.update({...repo, issue_number: issueNumber, body: updatedBody})
}
- return {success: true, url: comment.html_url}
+ return {success: true, url: issue.html_url}
};
}
module.exports = { main };
- GH_AW_SAFE_OUTPUT_SCRIPT_PUBLISH_COMPONENT_FINDINGS_42efc8e50632612b_EOF
+ GH_AW_SAFE_OUTPUT_SCRIPT_PUBLISH_COMPONENT_FINDINGS_75faeab9d5765245_EOF
- name: Process Safe Outputs
id: process_safe_outputs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
diff --git a/.github/workflows/primer-api-review.md b/.github/workflows/primer-api-review.md
index 9c74f2d9367..ddc396425e0 100644
--- a/.github/workflows/primer-api-review.md
+++ b/.github/workflows/primer-api-review.md
@@ -58,26 +58,18 @@ safe-outputs:
required-title-prefix: 'Primer API Review'
scripts:
publish-component-findings:
- description: Upsert one component finding comment and resolve its checklist links on the Primer API Review issue
+ description: Publish one batch of up to 100 component finding comments and resolve their checklist links
inputs:
issue_number:
type: string
required: true
description: Review issue number, or aw_review for the issue created in this run
- component:
+ comments:
type: string
required: true
- description: Component name used in the api-review-comment-COMPONENT checklist link
- body:
- type: string
- required: true
- description: Complete component comment with findings grouped under headings and evidence tables
+ description: Base64-encoded UTF-8 JSON array of objects with component (name) and body (complete finding comment), at most 100 unique components
script: |
const {matchesWorkflowId, generateWorkflowIdMarker} = require('./generate_footer.cjs')
- config.publishCount = (config.publishCount || 0) + 1
- if (config.publishCount > 100) {
- throw new Error('At most 100 component comments may be published per run')
- }
const repo = context.repo
const resolved = resolvedTemporaryIds[item.issue_number]
const issueNumber = Number(resolved ? resolved.number : item.issue_number)
@@ -87,42 +79,62 @@ safe-outputs:
throw new Error('Cross-repository review targets are not allowed')
}
if ((!pendingStagedIssue && (!Number.isSafeInteger(issueNumber) || issueNumber <= 0)) ||
- typeof item.component !== 'string' || !/^[A-Za-z][A-Za-z0-9.]*$/.test(item.component) ||
- typeof item.body !== 'string' || item.body.length < 20 || item.body.length > 60000) {
+ typeof item.comments !== 'string' || item.comments.length > 500000) {
throw new Error('Invalid component review payload')
}
+ const decoded = Buffer.from(item.comments, 'base64')
+ if (decoded.toString('base64') !== item.comments) {
+ throw new Error('Invalid base64 component review payload')
+ }
+ const entries = JSON.parse(decoded.toString('utf8'))
+ if (!Array.isArray(entries) || entries.length === 0 || entries.length > 100) {
+ throw new Error('Batch must contain between 1 and 100 component comments')
+ }
+ const components = new Set()
+ const batch = entries.map(entry => {
+ if (!entry || typeof entry.component !== 'string' ||
+ !/^[A-Za-z][A-Za-z0-9.]*$/.test(entry.component) || components.has(entry.component) ||
+ typeof entry.body !== 'string' || entry.body.length < 20 || entry.body.length > 60000) {
+ throw new Error('Invalid or duplicate component review entry')
+ }
+ components.add(entry.component)
+ return {component: entry.component, body: sanitizeContent(entry.body)}
+ })
if (staged) {
- core.info(`Would publish findings for ${item.component} on issue ${item.issue_number}`)
+ core.info(`Would publish ${batch.length} component comments on issue ${item.issue_number}`)
return {success: true, staged: true}
}
const {data: issue} = await github.rest.issues.get({...repo, issue_number: issueNumber})
if (issue.pull_request || issue.title !== 'Primer API Review') {
throw new Error('Target must be the Primer API Review issue')
}
- const marker = ``
const comments = await github.paginate(github.rest.issues.listComments, {
...repo, issue_number: issueNumber, per_page: 100,
})
- const previous = comments.find(comment =>
- comment.user?.login === 'github-actions[bot]' &&
- matchesWorkflowId(comment.body || '', 'primer-api-review') &&
- (comment.body || '').includes(marker),
- )
- const body = `${sanitizeContent(item.body)}\n\n${marker}\n${generateWorkflowIdMarker('primer-api-review')}`
- let comment = previous
- if (!previous || previous.body !== body) {
- const result = previous
- ? await github.rest.issues.updateComment({...repo, comment_id: previous.id, body})
- : await github.rest.issues.createComment({...repo, issue_number: issueNumber, body})
- comment = result.data
+ let updatedBody = issue.body || ''
+ for (const entry of batch) {
+ const marker = ``
+ const previous = comments.find(comment =>
+ comment.user?.login === 'github-actions[bot]' &&
+ matchesWorkflowId(comment.body || '', 'primer-api-review') &&
+ (comment.body || '').includes(marker),
+ )
+ const body = `${entry.body}\n\n${marker}\n${generateWorkflowIdMarker('primer-api-review')}`
+ let comment = previous
+ if (!previous || previous.body !== body) {
+ const result = previous
+ ? await github.rest.issues.updateComment({...repo, comment_id: previous.id, body})
+ : await github.rest.issues.createComment({...repo, issue_number: issueNumber, body})
+ comment = result.data
+ }
+ updatedBody = updatedBody.replaceAll(
+ `(#api-review-comment-${entry.component})`, `(${comment.html_url})`,
+ )
}
- const updatedBody = (issue.body || '').replaceAll(
- `(#api-review-comment-${item.component})`, `(${comment.html_url})`,
- )
if (updatedBody !== issue.body) {
await github.rest.issues.update({...repo, issue_number: issueNumber, body: updatedBody})
}
- return {success: true, url: comment.html_url}
+ return {success: true, url: issue.html_url}
---
# Primer API Review
@@ -262,8 +274,15 @@ active recommendation. Omit findings with a documented rationale as described
above. Never modify human comments or publish comments for components with no
current or previously published findings.
-Use `publish_component_findings` with the component name and its complete comment
-body; the safe-output handler maintains the component marker, reuses the managed
+Use one `publish_component_findings` call with `comments` containing a
+base64-encoded UTF-8 JSON array of objects, each containing `component` and its
+complete comment `body`. Use
+`Buffer.from(JSON.stringify(comments), 'utf8').toString('base64')` or an equivalent
+encoder; do not hand-escape Markdown. This keeps ingestion's Markdown sanitizer
+from corrupting the JSON transport; each decoded comment is still sanitized
+before publication. Keep the encoded batch under 500,000 characters.
+The safe-output
+handler maintains the component marker, reuses the managed
comment, skips unchanged content, and inserts its real URL into the checklist.
Do not supply comment IDs or invent comment URLs. For a new or updated component
comment, use `(#api-review-comment-ComponentName)` as the checklist link target.
@@ -280,13 +299,15 @@ If `/tmp/gh-aw/data/existing-review.json` contains an issue:
Otherwise, first queue one issue with `create_issue`, the exact title
`Primer API Review`, `temporary_id: aw_review`, and the overview body.
-Then queue `publish_component_findings` for each new or changed component comment
-(at most one per component and 100 per run), using the existing issue number as
-a string or `aw_review` for the newly created issue. Also queue it for any
-component whose checklist links use placeholders, even if its comment is
-unchanged. Safe outputs execute after the agent finishes, so do not try to read
-back newly queued comments during this run. Queue the overview before the
-component publishers; do not queue another overview update afterward that would
+Then queue exactly one `publish_component_findings` call containing all new or
+changed component comments (at most one entry per component and 100 per batch),
+using the existing issue number as a string or `aw_review` for the newly created
+issue. Include any component whose checklist links use placeholders, even if
+its comment is unchanged. Skip the call when there are no comments to publish.
+The ingestion layer allows only one publishing output per run: never emit one
+call per component or split the array across calls. Safe outputs execute after
+the agent finishes, so do not try to read back newly queued comments during this
+run. Queue the overview before the batch publisher; do not queue another overview update afterward that would
overwrite resolved links. If the publication cap is reached, retain existing
links and list unpublished components in the collapsed remaining-coverage block
without emitting dangling placeholders.
diff --git a/script/primer-api-review.test.mjs b/script/primer-api-review.test.mjs
index 8972919b93d..f056b74c4ff 100644
--- a/script/primer-api-review.test.mjs
+++ b/script/primer-api-review.test.mjs
@@ -1,6 +1,9 @@
/* eslint camelcase: ["error", {allow: ["pull_request", "html_url", "issue_number", "aw_review"]}] */
import assert from 'node:assert/strict'
-import {readFileSync} from 'node:fs'
+import {mkdtempSync, readFileSync, rmSync, writeFileSync} from 'node:fs'
+import {execFileSync} from 'node:child_process'
+import {tmpdir} from 'node:os'
+import {join} from 'node:path'
import {test} from 'node:test'
const workflow = readFileSync(new URL('../.github/workflows/primer-api-review.md', import.meta.url), 'utf8')
@@ -26,6 +29,7 @@ const workflowMarker = ''
const componentMarker = ''
const commentUrl = 'https://github.com/primer/react/issues/8384#issuecomment-123'
const commentBody = '### Button\n\nEvidence-backed API findings'
+const encodeComments = entries => Buffer.from(JSON.stringify(entries), 'utf8').toString('base64')
function setup({comments = [], title = 'Primer API Review', pullRequest, staged = false} = {}) {
const calls = []
@@ -66,7 +70,7 @@ function setup({comments = [], title = 'Primer API Review', pullRequest, staged
config,
run: (item = {}, resolved = {}) =>
publish(
- {issue_number: '8384', component: 'Button', body: commentBody, ...item},
+ {issue_number: '8384', comments: encodeComments([{component: 'Button', body: commentBody}]), ...item},
resolved,
config,
{repo: {owner: 'primer', repo: 'react'}},
@@ -122,9 +126,16 @@ test('rejects invalid targets and payloads without writing', async () => {
await assert.rejects(fixture.run(), /Target must be/)
assert.equal(fixture.calls.length, 0)
}
- for (const item of [{issue_number: '-1'}, {component: '../Button'}, {component: undefined}, {body: ''}]) {
+ for (const item of [
+ {issue_number: '-1'},
+ {comments: undefined},
+ {comments: 'x'.repeat(500001)},
+ ...[{component: '../Button'}, {component: undefined}, {body: ''}, null].map(entry => ({
+ comments: encodeComments([entry]),
+ })),
+ ]) {
const fixture = setup()
- await assert.rejects(fixture.run(item), /Invalid component review payload/)
+ await assert.rejects(fixture.run(item), /Invalid.*component review/)
assert.equal(fixture.calls.length, 0)
}
const fixture = setup()
@@ -135,31 +146,132 @@ test('rejects invalid targets and payloads without writing', async () => {
assert.equal(fixture.calls.length, 0)
})
-test('respects staged mode and the per-run publication cap', async () => {
+test('respects staged mode and validates the whole bounded batch before writing', async () => {
const fixture = setup({staged: true})
await fixture.run()
await fixture.run({issue_number: 'aw_review'})
assert.equal(fixture.calls.length, 0)
- fixture.config.publishCount = 100
- await assert.rejects(fixture.run(), /At most 100/)
assert.equal(fixture.calls.length, 0)
+ for (const entries of [
+ [],
+ {},
+ Array.from({length: 101}, (_, index) => ({component: `Component${index}`, body: commentBody})),
+ [
+ {component: 'Button', body: commentBody},
+ {component: 'Button', body: commentBody},
+ ],
+ [
+ {component: 'Button', body: commentBody},
+ {component: 'Dialog', body: ''},
+ ],
+ ]) {
+ const invalid = setup()
+ await assert.rejects(invalid.run({comments: encodeComments(entries)}))
+ assert.equal(invalid.calls.length, 0)
+ }
+ await assert.rejects(fixture.run({comments: '[invalid base64'}), /Invalid base64/)
+ await assert.rejects(fixture.run({comments: Buffer.from('[invalid JSON').toString('base64')}), SyntaxError)
})
-test('successive component publications preserve other checklist links', async () => {
+test('one batch publishes all component comments and preserves their checklist links', async () => {
const fixture = setup()
fixture.issue.body += '\n- [ ] [Dialog change](#api-review-comment-Dialog)'
- await fixture.run()
- await fixture.run({component: 'Dialog'})
+ const item = {
+ comments: encodeComments([
+ {component: 'Button', body: commentBody},
+ {component: 'Dialog', body: '### Dialog\n\nEvidence-backed API findings'},
+ ]),
+ }
+ await fixture.run(item)
assert.equal(fixture.issue.body.includes('#api-review-comment-'), false)
assert.equal(
fixture.issue.body,
`- [ ] [Button API change](${commentUrl})\n- [ ] [Dialog change](${commentUrl.replace('123', '124')})`,
)
assert.equal(fixture.calls.filter(([name]) => name === 'createComment').length, 2)
+ assert.equal(fixture.calls.filter(([name]) => name === 'update').length, 1)
+ await fixture.run(item)
+ assert.equal(fixture.calls.length, 3, 'reruns reuse every comment in the batch')
})
test('overview instructions replace instead of append and precede comment publication', () => {
assert.match(workflow, /`update_issue`\n\s+and `operation: replace`/)
- assert.match(workflow, /Queue the overview before the\ncomponent publishers/)
+ assert.match(workflow, /Queue the overview before the batch publisher/)
+ assert.match(workflow, /exactly one `publish_component_findings` call/)
assert.match(workflow, /at most two sentences/)
})
+
+// Set GH_AW_ACTIONS_DIR to actions/setup/js from the lock file's pinned gh-aw release.
+test(
+ 'pinned gh-aw ingestion accepts all 19 comments in one output, not 19 outputs',
+ {
+ skip: !process.env.GH_AW_ACTIONS_DIR,
+ },
+ async () => {
+ const directory = mkdtempSync(join(tmpdir(), 'api-review-ingestion-'))
+ try {
+ const lock = readFileSync(new URL('../.github/workflows/primer-api-review.lock.yml', import.meta.url), 'utf8')
+ const config = JSON.parse(JSON.parse(lock.match(/GH_AW_SAFE_OUTPUTS_CONFIG: (.+)/)[1]))
+ writeFileSync(join(directory, 'config.json'), JSON.stringify(config))
+ const entries = Array.from({length: 19}, (_, index) => ({
+ component: `Component${index}`,
+ body: `### Component${index}\n\n| Evidence | Impact | Recommendation |\n| --- | --- | --- |\n| [Source](https://github.com/primer/react/blob/main/file.ts#L1) | "quoted" and "fullwidth" text | Use \`size\` |\n\nPast findings
\n\nSee https://example.com/documentation for more context. No past findings.\n\n `,
+ }))
+ const message = {type: 'publish_component_findings', issue_number: '8384', comments: encodeComments(entries)}
+ const ingest = messages => {
+ writeFileSync(join(directory, 'outputs.jsonl'), messages.map(value => JSON.stringify(value)).join('\n'))
+ return JSON.parse(
+ execFileSync(
+ process.execPath,
+ [
+ '-e',
+ `
+ const path = require('node:path');
+ const runtime = process.env.GH_AW_ACTIONS_DIR;
+ require(path.join(runtime, 'constants.cjs')).TMP_GH_AW_PATH = process.env.TEST_DIRECTORY;
+ global.core = {
+ info() {}, warning() {}, error() {}, debug() {}, exportVariable() {},
+ setFailed(message) { throw new Error(message); },
+ setOutput(name, value) { if (name === 'output') process.stdout.write(value); },
+ };
+ global.context = {repo: {owner: 'primer', repo: 'react'}, payload: {}};
+ global.github = {};
+ require(path.join(runtime, 'collect_ndjson_output.cjs')).main().catch(error => {
+ console.error(error);
+ process.exitCode = 1;
+ });
+ `,
+ ],
+ {
+ encoding: 'utf8',
+ env: {
+ ...process.env,
+ TEST_DIRECTORY: directory,
+ RUNNER_TEMP: directory,
+ GH_AW_SAFE_OUTPUTS: join(directory, 'outputs.jsonl'),
+ GH_AW_SAFE_OUTPUTS_CONFIG_PATH: join(directory, 'config.json'),
+ GH_AW_VALIDATION_CONFIG_PATH: join(directory, 'missing-validation.json'),
+ GH_AW_VALIDATION_CONFIG: '',
+ },
+ },
+ ),
+ )
+ }
+ const rejected = ingest(entries.map(entry => ({...message, comments: encodeComments([entry])})))
+ assert.equal(rejected.items.length, 1)
+ assert.equal(rejected.errors.length, 18)
+ assert.match(rejected.errors[0], /Maximum allowed: 1/)
+ const accepted = ingest([message])
+ assert.deepEqual(accepted.errors, [])
+ assert.equal(accepted.items.length, 1)
+ assert.deepEqual(JSON.parse(Buffer.from(accepted.items[0].comments, 'base64').toString('utf8')), entries)
+ const fixture = setup()
+ fixture.issue.body = entries.map(entry => `- [ ] [Finding](#api-review-comment-${entry.component})`).join('\n')
+ await fixture.run(accepted.items[0])
+ assert.equal(fixture.calls.filter(([name]) => name === 'createComment').length, 19)
+ assert.equal(fixture.issue.body.includes('#api-review-comment-'), false)
+ } finally {
+ rmSync(directory, {recursive: true, force: true})
+ }
+ },
+)