diff --git a/.github/workflows/primer-api-review.lock.yml b/.github/workflows/primer-api-review.lock.yml
index 6bd7a406874..309dae9b211 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":"cc63dce1a77a407e7e3568c748edc9570742f51b2f3070dfd76951a1d3b100b4","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":"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
#
# ___ _ _
@@ -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\":\"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');
@@ -582,7 +582,30 @@ 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": "Publish one batch of up to 100 component finding comments and resolve their checklist links",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "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": {
+ "description": "Review issue number, or aw_review for the issue created in this run",
+ "type": "string"
+ }
+ },
+ "required": [
+ "comments",
+ "issue_number"
+ ],
+ "type": "object"
+ },
+ "name": "publish_component_findings"
+ }
+ ]
}
GH_AW_VALIDATION_JSON: |
{
@@ -1819,6 +1842,91 @@ 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_75faeab9d5765245_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 { comments, issue_number } = config;
+ return async function handlePublishComponentFindings(item, resolvedTemporaryIds, temporaryIdMap) {
+ const {matchesWorkflowId, generateWorkflowIdMarker} = require('./generate_footer.cjs')
+ 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 ((!pendingStagedIssue && (!Number.isSafeInteger(issueNumber) || issueNumber <= 0)) ||
+ 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 ${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 comments = await github.paginate(github.rest.issues.listComments, {
+ ...repo, issue_number: issueNumber, per_page: 100,
+ })
+ 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})`,
+ )
+ }
+ if (updatedBody !== issue.body) {
+ await github.rest.issues.update({...repo, issue_number: issueNumber, body: updatedBody})
+ }
+ return {success: true, url: issue.html_url}
+
+ };
+ }
+ module.exports = { main };
+
+ 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
@@ -1828,7 +1936,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 43b86fa2e84..ddc396425e0 100644
--- a/.github/workflows/primer-api-review.md
+++ b/.github/workflows/primer-api-review.md
@@ -49,12 +49,92 @@ 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: 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
+ comments:
+ type: string
+ required: true
+ 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')
+ 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 ((!pendingStagedIssue && (!Number.isSafeInteger(issueNumber) || issueNumber <= 0)) ||
+ 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 ${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 comments = await github.paginate(github.rest.issues.listComments, {
+ ...repo, issue_number: issueNumber, per_page: 100,
+ })
+ 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})`,
+ )
+ }
+ if (updatedBody !== issue.body) {
+ await github.rest.issues.update({...repo, issue_number: issueNumber, body: updatedBody})
+ }
+ return {success: true, url: issue.html_url}
---
# Primer API Review
@@ -65,70 +145,208 @@ 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`; 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`. 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: 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;
+ 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.
+ 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
- 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
- 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.
-5. Merge duplicate findings and discard anything speculative, stylistic but not
+ 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
+ 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
-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 number of components reviewed.
-- 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.
-- Include the workflow run as
+- 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, 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.
+- 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.
+- 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 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.
+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.
-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.
+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.
+
+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
+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: small
+model: claude-sonnet-5
---
-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 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`
-Return compact structured findings. Each finding must contain:
+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 +354,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.
diff --git a/script/primer-api-review.test.mjs b/script/primer-api-review.test.mjs
new file mode 100644
index 00000000000..f056b74c4ff
--- /dev/null
+++ b/script/primer-api-review.test.mjs
@@ -0,0 +1,277 @@
+/* eslint camelcase: ["error", {allow: ["pull_request", "html_url", "issue_number", "aw_review"]}] */
+import assert from 'node:assert/strict'
+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')
+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'
+const encodeComments = entries => Buffer.from(JSON.stringify(entries), 'utf8').toString('base64')
+
+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', comments: encodeComments([{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'},
+ {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/)
+ 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 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)
+ 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('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)'
+ 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 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})
+ }
+ },
+)