Skip to content

Commit eb02167

Browse files
Merge branch 'main' into feat/status-telemetry
2 parents ed0c3dd + 6043caa commit eb02167

205 files changed

Lines changed: 12751 additions & 1702 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/e2e-tests-full.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ jobs:
5757
parse-json-secrets: true
5858
- run: npm ci
5959
- run: npm run build
60+
- name: Generate GitHub App Token
61+
if: matrix.cdk-source == 'main'
62+
id: app-token
63+
uses: actions/create-github-app-token@v1
64+
with:
65+
app-id: ${{ vars.APP_ID }}
66+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
67+
owner: aws
6068
- name: Build CDK package from main
6169
if: matrix.cdk-source == 'main'
6270
run: |
@@ -67,7 +75,7 @@ jobs:
6775
TARBALL=$(npm pack --pack-destination "$RUNNER_TEMP" | tail -1)
6876
echo "CDK_TARBALL=$RUNNER_TEMP/$TARBALL" >> "$GITHUB_ENV"
6977
env:
70-
CDK_REPO_TOKEN: ${{ secrets.CDK_REPO_TOKEN }}
78+
CDK_REPO_TOKEN: ${{ steps.app-token.outputs.token }}
7179
CDK_REPO: ${{ secrets.CDK_REPO_NAME }}
7280
- name: Install CLI globally
7381
run: npm install -g "$(npm pack | tail -1)"

.github/workflows/e2e-tests.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,15 @@ jobs:
7979
E2E,${{ secrets.E2E_SECRET_ARN }}
8080
parse-json-secrets: true
8181

82+
- name: Generate GitHub App Token
83+
id: app-token
84+
uses: actions/create-github-app-token@v1
85+
with:
86+
app-id: ${{ vars.APP_ID }}
87+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
88+
owner: aws
8289
# Build @aws/agentcore-cdk from source for cross-package testing.
83-
# Requires secrets: CDK_REPO_NAME (org/repo), CDK_REPO_TOKEN (fine-grained PAT)
90+
# Requires secret: CDK_REPO_NAME (org/repo). Token is generated by the App above.
8491
- name: Build CDK package
8592
run: |
8693
CDK_BRANCH="${{ inputs.cdk_branch || 'main' }}"
@@ -92,7 +99,7 @@ jobs:
9299
TARBALL=$(npm pack --pack-destination "$RUNNER_TEMP" | tail -1)
93100
echo "CDK_TARBALL=$RUNNER_TEMP/$TARBALL" >> "$GITHUB_ENV"
94101
env:
95-
CDK_REPO_TOKEN: ${{ secrets.CDK_REPO_TOKEN }}
102+
CDK_REPO_TOKEN: ${{ steps.app-token.outputs.token }}
96103
CDK_REPO: ${{ secrets.CDK_REPO_NAME }}
97104

98105
- run: npm ci

.github/workflows/lint.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
uses: actions/cache/save@v5
3030
with:
3131
path: node_modules
32-
key: node-modules-${{ hashFiles('package-lock.json') }}
32+
key: node-modules-${{ hashFiles('npm-shrinkwrap.json') }}
3333

3434
format:
3535
needs: setup
@@ -42,7 +42,7 @@ jobs:
4242
- uses: actions/cache/restore@v5
4343
with:
4444
path: node_modules
45-
key: node-modules-${{ hashFiles('package-lock.json') }}
45+
key: node-modules-${{ hashFiles('npm-shrinkwrap.json') }}
4646
- run: npm run format:check
4747

4848
lint:
@@ -56,7 +56,7 @@ jobs:
5656
- uses: actions/cache/restore@v5
5757
with:
5858
path: node_modules
59-
key: node-modules-${{ hashFiles('package-lock.json') }}
59+
key: node-modules-${{ hashFiles('npm-shrinkwrap.json') }}
6060
- run: npm run lint
6161

6262
security:
@@ -70,7 +70,7 @@ jobs:
7070
- uses: actions/cache/restore@v5
7171
with:
7272
path: node_modules
73-
key: node-modules-${{ hashFiles('package-lock.json') }}
73+
key: node-modules-${{ hashFiles('npm-shrinkwrap.json') }}
7474
- run: npm run security:audit
7575

7676
secrets:
@@ -84,7 +84,7 @@ jobs:
8484
- uses: actions/cache/restore@v5
8585
with:
8686
path: node_modules
87-
key: node-modules-${{ hashFiles('package-lock.json') }}
87+
key: node-modules-${{ hashFiles('npm-shrinkwrap.json') }}
8888
- run: npm run secrets:check
8989

9090
typecheck:
@@ -98,7 +98,7 @@ jobs:
9898
- uses: actions/cache/restore@v5
9999
with:
100100
path: node_modules
101-
key: node-modules-${{ hashFiles('package-lock.json') }}
101+
key: node-modules-${{ hashFiles('npm-shrinkwrap.json') }}
102102
- run: npm run typecheck
103103

104104
schema-check:
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
name: Claude Security Review
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, reopened, synchronize, labeled]
6+
workflow_dispatch:
7+
inputs:
8+
pr_number:
9+
description:
10+
'PR number to review (note: workflow_dispatch will NOT post inline comments — the action only attaches the
11+
inline-comment MCP server on PR-context events. Use this only for end-to-end smoke-testing the prompt
12+
plumbing.)'
13+
required: true
14+
type: string
15+
16+
permissions:
17+
id-token: write
18+
pull-requests: write
19+
issues: write
20+
contents: read
21+
22+
concurrency:
23+
# Don't cancel-in-progress: a cancelled run that has already started its labels/checkout
24+
# but not the actual review still triggers always() steps and ends up posting a misleading
25+
# "no findings" summary (since the inline-comment buffer is empty when the analysis step
26+
# was skipped due to cancellation). Letting both runs complete is the safer default.
27+
group: pr-security-review-${{ github.event.pull_request.number || inputs.pr_number }}
28+
cancel-in-progress: false
29+
30+
jobs:
31+
authorize:
32+
runs-on: ubuntu-latest
33+
# On 'labeled' events, only proceed when the label is exactly 'safe-to-review'.
34+
# Other labels (e.g. size/m) are filtered out so we don't spawn API calls.
35+
if: |
36+
github.event_name != 'pull_request_target' ||
37+
github.event.action != 'labeled' ||
38+
github.event.label.name == 'safe-to-review'
39+
outputs:
40+
authorized: ${{ steps.auth.outputs.authorized || steps.dispatch-auth.outputs.authorized }}
41+
steps:
42+
- name: Check authorization
43+
id: auth
44+
if: github.event_name == 'pull_request_target'
45+
uses: actions/github-script@v9
46+
with:
47+
script: |
48+
// pull_request_target opened/reopened/synchronize: gate on the PR author
49+
// (auto-runs on maintainer-authored PRs; community PRs need the label path below).
50+
// pull_request_target labeled (safe-to-review): gate on the labeler (sender)
51+
// so a maintainer applying the label authorizes the run on a community PR.
52+
const isLabel = context.payload.action === 'labeled';
53+
const user = isLabel
54+
? context.payload.sender.login
55+
: context.payload.pull_request.user.login;
56+
const reason = isLabel ? `labeler ${user}` : `PR author ${user}`;
57+
try {
58+
await github.rest.teams.getMembershipForUserInOrg({
59+
org: context.repo.owner,
60+
team_slug: 'agentcore-cli-devs',
61+
username: user,
62+
});
63+
console.log(`${reason} is a member of agentcore-cli-devs`);
64+
core.setOutput('authorized', 'true');
65+
} catch (teamError) {
66+
try {
67+
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
68+
owner: context.repo.owner,
69+
repo: context.repo.repo,
70+
username: user,
71+
});
72+
const hasWriteAccess = ['write', 'admin'].includes(data.permission);
73+
if (hasWriteAccess) {
74+
console.log(`${reason} has write access (${data.permission})`);
75+
core.setOutput('authorized', 'true');
76+
} else {
77+
console.log(`${reason} does not have write access (${data.permission}) — skipping review`);
78+
core.setOutput('authorized', 'false');
79+
}
80+
} catch (collabError) {
81+
console.log(`${reason} authorization check failed (${collabError.status}) — skipping review`);
82+
core.setOutput('authorized', 'false');
83+
}
84+
}
85+
86+
- name: Auto-authorize workflow_dispatch
87+
id: dispatch-auth
88+
if: github.event_name == 'workflow_dispatch'
89+
run: echo "authorized=true" >> "$GITHUB_OUTPUT"
90+
91+
security-review:
92+
needs: authorize
93+
if: needs.authorize.outputs.authorized == 'true'
94+
runs-on: ubuntu-latest
95+
timeout-minutes: 30
96+
env:
97+
AWS_REGION: us-west-2
98+
steps:
99+
# Generate the GitHub App token first so every subsequent github-script step can
100+
# use it. The default GITHUB_TOKEN is read-only on pull_request_target /
101+
# pull_request_review events from forks, which makes label/comment writes 403.
102+
- name: Generate GitHub App token
103+
id: app-token
104+
uses: actions/create-github-app-token@v1
105+
with:
106+
app-id: ${{ vars.APP_ID }}
107+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
108+
109+
- name: Resolve PR number
110+
id: pr
111+
uses: actions/github-script@v9
112+
env:
113+
PR_NUMBER_INPUT: ${{ inputs.pr_number }}
114+
with:
115+
github-token: ${{ steps.app-token.outputs.token }}
116+
script: |
117+
const num =
118+
context.eventName === 'workflow_dispatch'
119+
? parseInt(process.env.PR_NUMBER_INPUT, 10)
120+
: context.payload.pull_request.number;
121+
const { data: pr } = await github.rest.pulls.get({
122+
owner: context.repo.owner,
123+
repo: context.repo.repo,
124+
pull_number: num,
125+
});
126+
core.setOutput('number', num);
127+
core.setOutput('head_sha', pr.head.sha);
128+
core.setOutput('base_ref', pr.base.ref);
129+
130+
- name: Add claude-security-reviewing label
131+
uses: actions/github-script@v9
132+
env:
133+
PR_NUMBER: ${{ steps.pr.outputs.number }}
134+
with:
135+
github-token: ${{ steps.app-token.outputs.token }}
136+
script: |
137+
const prNumber = parseInt(process.env.PR_NUMBER, 10);
138+
try {
139+
await github.rest.issues.getLabel({
140+
owner: context.repo.owner,
141+
repo: context.repo.repo,
142+
name: 'claude-security-reviewing',
143+
});
144+
} catch (e) {
145+
if (e.status === 404) {
146+
await github.rest.issues.createLabel({
147+
owner: context.repo.owner,
148+
repo: context.repo.repo,
149+
name: 'claude-security-reviewing',
150+
color: 'D73A4A',
151+
description: 'Claude Code /security-review in progress',
152+
});
153+
}
154+
}
155+
await github.rest.issues.addLabels({
156+
owner: context.repo.owner,
157+
repo: context.repo.repo,
158+
issue_number: prNumber,
159+
labels: ['claude-security-reviewing'],
160+
});
161+
162+
- name: Checkout PR head
163+
uses: actions/checkout@v6
164+
with:
165+
ref: ${{ steps.pr.outputs.head_sha }}
166+
# The bundled /security-review skill runs `git diff origin/HEAD...` so we need
167+
# the base branch locally too. fetch-depth: 0 grabs the full history.
168+
fetch-depth: 0
169+
170+
- name: Set origin/HEAD for /security-review skill
171+
env:
172+
BASE_REF: ${{ steps.pr.outputs.base_ref }}
173+
run: |
174+
set -euo pipefail
175+
# actions/checkout doesn't set up the remote's symbolic HEAD ref, so
176+
# `git diff origin/HEAD...` (the first command the bundled
177+
# /security-review skill runs) fails with "ambiguous argument
178+
# 'origin/HEAD...': unknown revision". Point origin/HEAD at the PR's
179+
# base branch so the skill resolves the diff against the right ref.
180+
git remote set-head origin "$BASE_REF"
181+
git symbolic-ref refs/remotes/origin/HEAD
182+
183+
- name: Configure AWS credentials (OIDC)
184+
uses: aws-actions/configure-aws-credentials@v6
185+
with:
186+
role-to-assume: ${{ secrets.BEDROCK_SECURITY_REVIEW_ROLE_ARN }}
187+
aws-region: us-west-2
188+
189+
- name: Run Claude Code security review
190+
id: review
191+
uses: anthropics/claude-code-action@v1
192+
with:
193+
github_token: ${{ steps.app-token.outputs.token }}
194+
use_bedrock: 'true'
195+
# The Claude Code SDK that ships with the action has /security-review bundled
196+
# as a slash command. Invoking it directly lets the skill drive its own
197+
# `git diff origin/HEAD...`, sub-task fan-out, and false-positive filtering
198+
# without us re-implementing any of that. We append a short tail telling the
199+
# action to use the inline-comment MCP tool for findings.
200+
prompt: |
201+
/security-review
202+
203+
For each finding, call mcp__github_inline_comment__create_inline_comment with
204+
{ path, line, body } pointing at the exact file and line in the diff. Do NOT
205+
post a single summary comment listing all findings — the workflow handles a
206+
top-level summary after this run completes. If there are no findings, exit
207+
without calling any tool.
208+
show_full_output: 'true'
209+
# Allow-listing this MCP tool name is what tells the action to register the
210+
# github_inline_comment MCP server. See anthropics/claude-code-action
211+
# src/mcp/install-mcp-server.ts.
212+
claude_args: >-
213+
--model us.anthropic.claude-opus-4-7 --max-turns 30 --allowedTools
214+
mcp__github_inline_comment__create_inline_comment
215+
216+
- name: Count buffered findings
217+
id: findings
218+
# Only count if the review step actually ran (success or failure - both produce
219+
# a meaningful buffer state). Skip on cancellation/skip so we don't lie about
220+
# "no findings" when Bedrock was never invoked.
221+
if: steps.review.conclusion == 'success' || steps.review.conclusion == 'failure'
222+
run: |
223+
set -euo pipefail
224+
BUFFER=/tmp/inline-comments-buffer.jsonl
225+
if [ -s "$BUFFER" ]; then
226+
COUNT=$(wc -l < "$BUFFER" | tr -d ' ')
227+
else
228+
COUNT=0
229+
fi
230+
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
231+
echo "Buffered findings: $COUNT"
232+
233+
- name: Post security review summary comment
234+
# Always post some kind of summary so the PR shows the run happened, but branch on
235+
# the review step's conclusion so a cancelled/skipped run doesn't get reported as
236+
# "no findings".
237+
if: always()
238+
uses: actions/github-script@v9
239+
env:
240+
PR_NUMBER: ${{ steps.pr.outputs.number }}
241+
FINDING_COUNT: ${{ steps.findings.outputs.count }}
242+
REVIEW_CONCLUSION: ${{ steps.review.conclusion }}
243+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
244+
with:
245+
github-token: ${{ steps.app-token.outputs.token }}
246+
script: |
247+
const prNumber = parseInt(process.env.PR_NUMBER, 10);
248+
const count = parseInt(process.env.FINDING_COUNT || '0', 10);
249+
const conclusion = process.env.REVIEW_CONCLUSION || 'skipped';
250+
const runUrl = process.env.RUN_URL;
251+
252+
let body;
253+
if (conclusion === 'success') {
254+
body =
255+
count > 0
256+
? `**Claude Security Review:** posted ${count} inline finding${count === 1 ? '' : 's'} on this PR. ([run](${runUrl}))`
257+
: `**Claude Security Review:** no high-confidence findings. ([run](${runUrl}))`;
258+
} else if (conclusion === 'failure') {
259+
body = `**Claude Security Review:** the review run failed before completing. See the [run](${runUrl}) for details.`;
260+
} else {
261+
// cancelled / skipped — analysis didn't run, do NOT claim "no findings"
262+
body = `**Claude Security Review:** the review run was ${conclusion} before the analysis could complete (likely superseded or interrupted). See the [run](${runUrl}); a later run on this PR will replace this status.`;
263+
}
264+
265+
await github.rest.issues.createComment({
266+
owner: context.repo.owner,
267+
repo: context.repo.repo,
268+
issue_number: prNumber,
269+
body,
270+
});
271+
272+
- name: Remove claude-security-reviewing label
273+
if: always()
274+
uses: actions/github-script@v9
275+
env:
276+
PR_NUMBER: ${{ steps.pr.outputs.number }}
277+
with:
278+
github-token: ${{ steps.app-token.outputs.token }}
279+
script: |
280+
const prNumber = parseInt(process.env.PR_NUMBER, 10);
281+
try {
282+
await github.rest.issues.removeLabel({
283+
owner: context.repo.owner,
284+
repo: context.repo.repo,
285+
issue_number: prNumber,
286+
name: 'claude-security-reviewing',
287+
});
288+
} catch (error) {
289+
console.log('Label removal failed (may not exist):', error.message);
290+
}

0 commit comments

Comments
 (0)