From cb4a1ecdb9da8772e06bcef9182e9cd7b3d97b92 Mon Sep 17 00:00:00 2001 From: Eric He Date: Thu, 23 Jul 2026 16:59:14 -0400 Subject: [PATCH 1/4] ci: wire stage 1 reusable workflow tests --- .github/workflows/dependabot-auto-merge.yml | 82 +--- .../workflows/github-slack-notifications.yml | 220 --------- .github/workflows/pr-automerge.yml | 65 +-- .github/workflows/pr-security-review.yml | 461 +----------------- .github/workflows/pr-size-title.yml | 21 + .../workflows/slack-issue-notification.yml | 23 + .../workflows/slack-open-prs-notification.yml | 52 +- .github/workflows/sync-from-public.yml | 19 + 8 files changed, 100 insertions(+), 843 deletions(-) delete mode 100644 .github/workflows/github-slack-notifications.yml create mode 100644 .github/workflows/pr-size-title.yml create mode 100644 .github/workflows/slack-issue-notification.yml create mode 100644 .github/workflows/sync-from-public.yml diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 9ce99174..ff8aaf8c 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -1,83 +1,17 @@ -# .github/workflows/dependabot-auto-merge.yml -# -# Automatically enables auto-merge on Dependabot PRs for minor and patch -# version bumps. GitHub holds the actual merge until all required status -# checks in the 'main-status-checks' Ruleset pass. -# -# Major version bumps are explicitly skipped and require human review -# from the aws/bedrock-agentcore-maintainers team. -# -# PREREQUISITES (already completed): -# ✅ Ruleset 'main-status-checks' — CI must pass, no bypass for anyone -# ✅ Ruleset 'main' — approval requirement, Dependabot bypass added -# ✅ Settings → General → Allow auto-merge enabled - name: Dependabot Auto-merge on: + push: + branches: + - feat/caller-workflows pull_request: types: [opened, synchronize, reopened, ready_for_review] permissions: - contents: write # required to execute the squash merge - pull-requests: write # required to enable auto-merge + contents: write + pull-requests: write jobs: - dependabot-auto-merge: - name: Auto-merge minor/patch PRs - runs-on: ubuntu-latest - - # Only act on PRs opened by the Dependabot bot. - # The correct login is 'dependabot[bot]' — the bare string 'dependabot' - # never matches and would silently skip all runs. - if: github.actor == 'dependabot[bot]' - - steps: - - name: Generate GitHub App Token - id: app-token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - name: Fetch Dependabot metadata - id: metadata - uses: dependabot/fetch-metadata@v2 - with: - github-token: ${{ steps.app-token.outputs.token }} - - # patch and minor bumps: enable auto-merge. - # GitHub holds the merge until all required status checks pass. - # If CI fails the PR stays open — no merge happens. - - name: Enable auto-merge for minor/patch bumps - if: | - steps.metadata.outputs.update-type == 'version-update:semver-patch' || - steps.metadata.outputs.update-type == 'version-update:semver-minor' - env: - PR_URL: ${{ github.event.pull_request.html_url }} - GH_TOKEN: ${{ steps.app-token.outputs.token }} - DEP_NAMES: ${{ steps.metadata.outputs.dependency-names }} - PREV_VERSION: ${{ steps.metadata.outputs.previous-version }} - NEW_VERSION: ${{ steps.metadata.outputs.new-version }} - UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }} - run: | - gh pr merge --auto --squash "$PR_URL" - echo "✓ Auto-merge enabled: $DEP_NAMES \ - ($PREV_VERSION → \ - $NEW_VERSION, \ - $UPDATE_TYPE)" - - # major bumps: log clearly and do nothing. - # The PR stays open and is assigned to aws/bedrock-agentcore-maintainers - # via dependabot.yml for human review. - - name: Skip major bumps — human review required - if: steps.metadata.outputs.update-type == 'version-update:semver-major' - env: - DEP_NAMES: ${{ steps.metadata.outputs.dependency-names }} - PREV_VERSION: ${{ steps.metadata.outputs.previous-version }} - NEW_VERSION: ${{ steps.metadata.outputs.new-version }} - run: | - echo "⏭ Skipped: $DEP_NAMES \ - is a major bump ($PREV_VERSION → \ - $NEW_VERSION). \ - Requires review from aws/bedrock-agentcore-maintainers." + call: + uses: aws/agentcore-devx-devtools/.github/workflows/test-dependabot-auto-merge.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab + secrets: inherit diff --git a/.github/workflows/github-slack-notifications.yml b/.github/workflows/github-slack-notifications.yml deleted file mode 100644 index 31e069b3..00000000 --- a/.github/workflows/github-slack-notifications.yml +++ /dev/null @@ -1,220 +0,0 @@ -name: GitHub Slack Notifications - -# Central GitHub -> Slack integration point. Two triggers feed one shared -# Slack workflow (via SLACK_WEBHOOK_URL), which branches on event_type: -# - issues opened -> notify oncall of a new issue -# - comments on closed PRs -> redirect the commenter to open an issue, -# and notify oncall (closed-PR comments are -# otherwise easy to miss) -# Every payload sends the same key set so the Slack workflow can branch -# reliably; fields that don't apply to an event are sent empty. - -on: - issues: - types: [opened] - issue_comment: - types: [created] - -jobs: - notify-issue-opened: - if: github.event_name == 'issues' - runs-on: ubuntu-latest - permissions: {} - steps: - - name: Send issue details to Slack - # Attacker-controlled fields are passed through env: rather than - # interpolated into the YAML payload, to prevent workflow injection. - # For issue_opened, the issue_* fields carry the data and the - # pr_*/comment_* fields are empty. - env: - REPOSITORY: ${{ github.repository }} - CREATED_AT: ${{ github.event.issue.created_at }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - ISSUE_TITLE: ${{ github.event.issue.title }} - ISSUE_URL: ${{ github.event.issue.html_url }} - ISSUE_AUTHOR: ${{ github.event.issue.user.login }} - ISSUE_BODY: ${{ github.event.issue.body }} - LABELS: ${{ join(github.event.issue.labels.*.name, ', ') }} - uses: slackapi/slack-github-action@v3.0.1 - with: - webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - webhook-type: webhook-trigger - payload: | - event_type: "issue_opened" - repository: "${{ env.REPOSITORY }}" - created_at: "${{ env.CREATED_AT }}" - issue_number: "${{ env.ISSUE_NUMBER }}" - issue_title: ${{ toJSON(env.ISSUE_TITLE) }} - issue_url: "${{ env.ISSUE_URL }}" - issue_author: "${{ env.ISSUE_AUTHOR }}" - issue_body: ${{ toJSON(env.ISSUE_BODY) }} - labels: ${{ toJSON(env.LABELS) }} - pr_number: "" - pr_title: "" - pr_url: "" - pr_author: "" - pr_state: "" - pr_closed_at: "" - pr_merged_at: "" - comment_id: "" - comment_url: "" - comment_author: "" - comment_body: "" - - closed-pr-comment-redirect: - # Only fire on comments left on PRs (issue_comment fires for issues too) - # that are already closed, and skip comments left by bots. - if: >- - github.event_name == 'issue_comment' && github.event.issue.pull_request != null && github.event.issue.state == - 'closed' && github.event.comment.user.type != 'Bot' - runs-on: ubuntu-latest - permissions: - pull-requests: write - issues: read - # Serialize per-PR so the marker-comment dedup is race-free (otherwise two - # rapid-fire comments could both see "no marker" and both post a redirect). - # cancel-in-progress is false so the second run still executes after the - # first finishes -- we want to evaluate dedup against the just-posted marker. - concurrency: - group: closed-pr-comment-${{ github.event.issue.number }} - cancel-in-progress: false - steps: - - name: Check commenter permission - id: perm - uses: actions/github-script@v9 - with: - script: | - // External users on private repos can 404 here; treat any - // failure as "not a maintainer" so the redirect still fires. - let permission = 'none'; - try { - const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: context.payload.comment.user.login, - }); - permission = data.permission; - } catch (err) { - core.info(`Permission lookup failed for ${context.payload.comment.user.login}: ${err.message}. Treating as non-maintainer.`); - } - const skip = ['admin', 'maintain', 'write'].includes(permission); - core.setOutput('skip', String(skip)); - core.info(`Commenter ${context.payload.comment.user.login} permission=${permission} skip=${skip}`); - - - name: Check for existing redirect comment - id: existing - if: steps.perm.outputs.skip != 'true' - uses: actions/github-script@v9 - with: - script: | - // Marker we embed in our reply so we don't double-post on the same PR. - const marker = ''; - const comments = await github.paginate( - github.rest.issues.listComments, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.issue.number, - per_page: 100, - } - ); - const alreadyPosted = comments.some(c => c.body && c.body.includes(marker)); - core.setOutput('already_posted', String(alreadyPosted)); - - - name: Post redirect comment - if: steps.perm.outputs.skip != 'true' && steps.existing.outputs.already_posted != 'true' - # The Slack notification is the load-bearing part of this job. If - # posting the bot reply fails (rate limit, transient error), don't - # block the Slack notification. - continue-on-error: true - uses: actions/github-script@v9 - env: - # Repos with issue templates use /issues/new/choose; repos without - # templates should change this to /issues/new. - ISSUES_NEW_URL: https://github.com/${{ github.repository }}/issues/new/choose - with: - script: | - const commenter = context.payload.comment.user.login; - const issuesNewUrl = process.env.ISSUES_NEW_URL; - const body = [ - '', - '', - `Thanks for the report, @${commenter} — feedback like this is exactly`, - "how we catch the things we missed. Because this PR is already", - "closed, the team won't see follow-up comments here.", - '', - 'Would you mind opening a new issue so we can track it properly?', - issuesNewUrl, - '', - 'If this is a security issue, please report it privately via', - 'https://aws.amazon.com/security/vulnerability-reporting/ instead', - 'of a public issue.', - ].join('\n'); - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.issue.number, - body, - }); - - - name: Compute PR state - id: pr_state - if: steps.perm.outputs.skip != 'true' - uses: actions/github-script@v9 - with: - script: | - // PRs surface as `issue` events; merged_at is null when closed-not-merged. - const mergedAt = context.payload.issue.pull_request && - context.payload.issue.pull_request.merged_at; - core.setOutput('state', mergedAt ? 'merged' : 'closed'); - - - name: Notify Slack - # Notify oncall only on the FIRST external comment per PR (gated by - # already_posted). Subsequent comments on the same PR don't notify -- - # the redirect comment has already directed the commenter to open an - # issue, and issues notify oncall via the issue path. This bounds - # notification volume regardless of how chatty a thread becomes. - if: steps.perm.outputs.skip != 'true' && steps.existing.outputs.already_posted != 'true' - # Attacker-controlled fields are passed through env: rather than - # interpolated into the YAML payload, to prevent workflow injection. - # For closed-PR comments, the issue_* fields are empty (this isn't - # an issue) and the pr_*/comment_* fields carry the real data. - env: - REPOSITORY: ${{ github.repository }} - CREATED_AT: ${{ github.event.comment.created_at }} - PR_NUMBER: ${{ github.event.issue.number }} - PR_TITLE: ${{ github.event.issue.title }} - PR_URL: ${{ github.event.issue.html_url }} - PR_AUTHOR: ${{ github.event.issue.user.login }} - PR_CLOSED_AT: ${{ github.event.issue.closed_at }} - PR_MERGED_AT: ${{ github.event.issue.pull_request.merged_at }} - COMMENT_ID: ${{ github.event.comment.id }} - COMMENT_URL: ${{ github.event.comment.html_url }} - COMMENT_AUTHOR: ${{ github.event.comment.user.login }} - COMMENT_BODY: ${{ github.event.comment.body }} - uses: slackapi/slack-github-action@v3.0.1 - with: - webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - webhook-type: webhook-trigger - payload: | - event_type: "closed_pr_comment" - repository: "${{ env.REPOSITORY }}" - created_at: "${{ env.CREATED_AT }}" - issue_number: "" - issue_title: "" - issue_url: "" - issue_author: "" - issue_body: "" - labels: "" - pr_number: "${{ env.PR_NUMBER }}" - pr_title: ${{ toJSON(env.PR_TITLE) }} - pr_url: "${{ env.PR_URL }}" - pr_author: "${{ env.PR_AUTHOR }}" - pr_state: "${{ steps.pr_state.outputs.state }}" - pr_closed_at: "${{ env.PR_CLOSED_AT }}" - pr_merged_at: "${{ env.PR_MERGED_AT }}" - comment_id: "${{ env.COMMENT_ID }}" - comment_url: "${{ env.COMMENT_URL }}" - comment_author: "${{ env.COMMENT_AUTHOR }}" - comment_body: ${{ toJSON(env.COMMENT_BODY) }} diff --git a/.github/workflows/pr-automerge.yml b/.github/workflows/pr-automerge.yml index 8aea1d9b..9fa77265 100644 --- a/.github/workflows/pr-automerge.yml +++ b/.github/workflows/pr-automerge.yml @@ -1,66 +1,17 @@ name: PR Auto-merge on: + push: + branches: + - feat/caller-workflows pull_request_review: types: [submitted] permissions: - contents: write - pull-requests: write + contents: read + pull-requests: read jobs: - auto-merge: - name: Auto-merge Release PRs - runs-on: ubuntu-latest - # Only run when PR is approved and it's a release PR - if: | - github.event.review.state == 'approved' && - github.event.pull_request.user.login == 'github-actions[bot]' && - startsWith(github.event.pull_request.head.ref, 'release/') && - github.event.pull_request.base.ref == 'main' - - steps: - - name: Check CI status - id: ci-status - uses: actions/github-script@v8 - with: - script: | - const { data: checkRuns } = await github.rest.checks.listForRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: context.payload.pull_request.head.sha - }); - - // Include ALL required checks - const requiredChecks = [ - 'Lint', - 'Test Python 3.10', - 'Test Python 3.11', - 'Test Python 3.12', - 'Test Python 3.13', - 'Build Package' - ]; - - const allPassed = requiredChecks.every(checkName => { - const check = checkRuns.check_runs.find(run => run.name === checkName); - return check && check.conclusion === 'success'; - }); - - console.log(`All required checks passed: ${allPassed}`); - return allPassed; - - - name: Auto-merge PR - if: steps.ci-status.outputs.result == 'true' - uses: actions/github-script@v8 - with: - script: | - await github.rest.pulls.merge({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.payload.pull_request.number, - merge_method: 'squash', - commit_title: `Release v${context.payload.pull_request.head.ref.split('/')[1]}`, - commit_message: 'Auto-merged by release workflow' - }); - - console.log('✓ PR auto-merged successfully'); + call: + uses: aws/agentcore-devx-devtools/.github/workflows/test-pr-automerge.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab + secrets: inherit diff --git a/.github/workflows/pr-security-review.yml b/.github/workflows/pr-security-review.yml index 42e32d98..9cd30938 100644 --- a/.github/workflows/pr-security-review.yml +++ b/.github/workflows/pr-security-review.yml @@ -1,26 +1,17 @@ name: Claude Security Review -# This workflow inlines the security-review prompt rather than calling the -# bundled /security-review slash command. The bundled skill silently bombs -# whenever the runner's clone gets shallowed mid-run (claude-code-action's -# restoreConfigFromBase does this on every PR by design — see -# https://github.com/anthropics/claude-code-action/blob/v1/src/github/operations/restore-config.ts), -# because its first action is `git diff origin/HEAD...` and a shallow clone -# has no merge base. Computing the diff ourselves before the action starts -# eliminates that whole class of failure. - on: + push: + branches: + - feat/caller-workflows pull_request_target: types: [opened, reopened, synchronize, labeled] - # Only review PRs targeting our long-lived release branch. PRs into - # short-lived feature branches don't need a security gate — they get - # reviewed when those features are merged into main. branches: - main workflow_dispatch: inputs: pr_number: - description: PR number to review (workflow_dispatch will NOT post inline comments — use only for prompt smoke tests) + description: PR number to review (workflow_dispatch will NOT post inline comments - use only for prompt smoke tests) required: true type: string @@ -30,442 +21,10 @@ permissions: issues: write contents: read -concurrency: - # Don't cancel-in-progress: a cancelled run that has already started its labels/checkout - # but not the actual review still triggers always() steps and ends up posting a misleading - # "no findings" summary (since the inline-comment buffer is empty when the analysis step - # was skipped due to cancellation). Letting both runs complete is the safer default. - group: pr-security-review-${{ github.event.pull_request.number || inputs.pr_number }} - cancel-in-progress: false - jobs: - authorize: - runs-on: ubuntu-latest - # On 'labeled' events, only proceed when the label is exactly 'safe-to-review'. - # Other labels (e.g. size/m) are filtered out so we don't spawn API calls. - if: | - github.event_name != 'pull_request_target' || - github.event.action != 'labeled' || - github.event.label.name == 'safe-to-review' - outputs: - authorized: ${{ steps.auth.outputs.authorized || steps.dispatch-auth.outputs.authorized }} - steps: - - name: Check authorization - id: auth - if: github.event_name == 'pull_request_target' - uses: actions/github-script@v9 - with: - script: | - const isLabel = context.payload.action === 'labeled'; - const user = isLabel - ? context.payload.sender.login - : context.payload.pull_request.user.login; - const reason = isLabel ? `labeler ${user}` : `PR author ${user}`; - try { - await github.rest.teams.getMembershipForUserInOrg({ - org: context.repo.owner, - team_slug: 'agentcore-cli-devs', - username: user, - }); - core.setOutput('authorized', 'true'); - } catch { - try { - const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: user, - }); - core.setOutput('authorized', ['write', 'admin'].includes(data.permission) ? 'true' : 'false'); - } catch { - core.setOutput('authorized', 'false'); - } - } - - name: Auto-authorize workflow_dispatch - id: dispatch-auth - if: github.event_name == 'workflow_dispatch' - run: echo "authorized=true" >> "$GITHUB_OUTPUT" - - review: - needs: authorize - if: needs.authorize.outputs.authorized == 'true' - runs-on: ubuntu-latest - timeout-minutes: 30 - env: - AWS_REGION: us-west-2 - steps: - - name: Generate GitHub App token - id: app-token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - name: Resolve PR - id: pr - uses: actions/github-script@v9 - env: - PR_NUMBER_INPUT: ${{ inputs.pr_number }} - with: - github-token: ${{ steps.app-token.outputs.token }} - script: | - const num = context.eventName === 'workflow_dispatch' - ? parseInt(process.env.PR_NUMBER_INPUT, 10) - : context.payload.pull_request.number; - const { data: pr } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: num, - }); - core.setOutput('number', num); - core.setOutput('head_sha', pr.head.sha); - core.setOutput('base_ref', pr.base.ref); - - - name: Add reviewing label - uses: actions/github-script@v9 - env: - PR_NUMBER: ${{ steps.pr.outputs.number }} - with: - github-token: ${{ steps.app-token.outputs.token }} - script: | - const prNumber = parseInt(process.env.PR_NUMBER, 10); - try { - await github.rest.issues.getLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: 'claude-security-reviewing', - }); - } catch (e) { - if (e.status === 404) { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: 'claude-security-reviewing', - color: 'D73A4A', - description: 'Claude Code security review in progress', - }); - } - } - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - labels: ['claude-security-reviewing'], - }); - - - name: Checkout PR head - uses: actions/checkout@v6 - with: - ref: ${{ steps.pr.outputs.head_sha }} - fetch-depth: 0 - - - name: Compute diff - id: diff - env: - BASE_REF: ${{ steps.pr.outputs.base_ref }} - run: | - set -euo pipefail - # Compute the diff *before* claude-code-action shallows the repo. - # The action's restoreConfigFromBase() runs `git fetch --depth=1` - # against the base branch on startup, which strips history and - # would break any base-vs-head diff after that point. Doing it - # here means the model gets a frozen artifact that can't be - # invalidated by anything the action does later. - git fetch --no-tags origin "+refs/heads/$BASE_REF:refs/remotes/origin/$BASE_REF" - git diff "origin/$BASE_REF...HEAD" > /tmp/pr.diff - BYTES=$(wc -c < /tmp/pr.diff) - FILES=$(git diff --name-only "origin/$BASE_REF...HEAD" | wc -l | tr -d ' ') - echo "bytes=$BYTES" >> "$GITHUB_OUTPUT" - echo "files=$FILES" >> "$GITHUB_OUTPUT" - echo "Diff: $BYTES bytes across $FILES files" - - - name: Configure AWS credentials - if: steps.diff.outputs.bytes != '0' - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ secrets.BEDROCK_SECURITY_REVIEW_ROLE_ARN }} - aws-region: us-west-2 - - - name: Run Claude Code - id: review - if: steps.diff.outputs.bytes != '0' - uses: anthropics/claude-code-action@v1 - with: - github_token: ${{ steps.app-token.outputs.token }} - use_bedrock: "true" - prompt: | - You are performing a HIGH-CONFIDENCE security code review of a pull - request. The complete diff is at `/tmp/pr.diff` — read it first - using the Read tool. That file is the ground truth for what the - PR changes; do not run `git diff` or any other git commands. To - understand context — callers of a changed function, existing - sanitization patterns, the project's threat model — use Grep, - Glob, and Read against the repository working tree. Do not use - Bash. - - # OBJECTIVE - - Identify HIGH-CONFIDENCE security vulnerabilities newly introduced - by this PR that have real exploitation potential. This is NOT a - general code review. Focus ONLY on security implications added by - the PR. Do not comment on pre-existing issues. - - # CRITICAL INSTRUCTIONS - - 1. MINIMIZE FALSE POSITIVES: Only flag issues where you are >80% - confident of actual exploitability. - 2. AVOID NOISE: Skip theoretical issues, style concerns, or - low-impact findings. - 3. FOCUS ON IMPACT: Prioritize vulnerabilities that lead to - unauthorized access, data breach, or system compromise. - 4. DO NOT report any of: - - Denial of service / resource exhaustion / rate limiting - - Secrets at rest on disk (handled by other tooling) - - Memory consumption or CPU exhaustion - - # CATEGORIES TO EXAMINE - - **Input validation**: SQL injection, command injection, XXE, - template injection, NoSQL injection, path traversal. - **AuthN/AuthZ**: authentication bypass, privilege escalation, - session/JWT flaws, authorization-logic bypasses. - **Crypto & secrets**: hardcoded keys/passwords/tokens, weak - algorithms, improper key storage, weak randomness, certificate - validation bypass. - **Code execution**: deserialization RCE (pickle, YAML, etc.), - eval injection, XSS (reflected/stored/DOM) — only in unsafe paths - (see precedents). - **Data exposure**: sensitive logging, PII handling violations, - API leakage, debug-info exposure. - - A finding can still be HIGH severity if only exploitable from the - local network. - - # METHODOLOGY - - Phase 1 — Repository context: identify existing security - libraries/frameworks, sanitization patterns, the project's threat - model. Use search tools. - - Phase 2 — Comparative analysis: compare new changes against - established patterns; flag deviations and net-new attack surface. - - Phase 3 — Vulnerability assessment: for each modified file, - trace user input → sensitive operations, look for unsafe privilege - boundary crossings, identify injection points. - - # FALSE-POSITIVE FILTER (apply hard) - - Read the code (Read/Grep/Glob); do not run commands to reproduce - or write files. - - HARD EXCLUSIONS — drop any finding matching: - 1. DoS / resource exhaustion. - 2. Secrets/credentials on disk if otherwise secured. - 3. Rate limiting or service overload. - 4. Memory/CPU exhaustion. - 5. Missing input validation on non-security-critical fields. - 6. Input sanitization in GitHub Actions workflows unless clearly - triggerable via untrusted input. - 7. Lack of hardening; only flag concrete vulns. - 8. Theoretical race conditions or timing attacks. - 9. Outdated third-party libraries (handled separately). - 10. Memory-safety issues in memory-safe languages (Rust, Go, - JS/TS, Python). - 11. Files that are unit tests or test-only. - 12. Log spoofing — un-sanitized user input to logs is not a vuln. - 13. SSRF that only controls the path (host/protocol control is - required). - 14. User-controlled content in AI system prompts is not a vuln. - 15. Regex injection. - 16. Regex DoS. - 17. Insecure documentation (.md and similar). - 18. Lack of audit logs. - - PRECEDENTS: - 1. Plaintext-logging high-value secrets IS a vuln; logging URLs - is assumed safe. - 2. UUIDs are unguessable and need no validation. - 3. Env vars and CLI flags are trusted inputs. - 4. Resource leaks (memory, fd) are not vulns. - 5. Tabnabbing, XS-Leaks, prototype pollution, open redirects: - only with extremely high confidence. - 6. React / Angular: do not report XSS in components or .tsx files - unless using `dangerouslySetInnerHTML`, - `bypassSecurityTrustHtml`, or equivalents. - 7. GitHub Actions workflow vulns: only when a concrete attack - path through untrusted input exists. - 8. Missing AuthN/AuthZ in client-side code is not a vuln — - validation is the server's job. - 9. MEDIUM findings only when obvious and concrete. - 10. .ipynb notebook vulns: only with a concrete attack path - through untrusted input. - 11. Logging non-PII data is not a vuln. Only flag when the data - is secrets, passwords, or PII. - 12. Command injection in shell scripts: only when there is a - concrete attack path through untrusted input. - - For each surviving finding, score confidence 1–10: - - 1–3: low / likely noise — drop - - 4–6: medium — drop unless obvious and concrete - - 7–10: high — keep - - # PROCESS - - Run this in three steps, exactly: - - 1. Spawn a Task sub-agent to identify candidate vulnerabilities. - Pass the full instructions above (objective, categories, - methodology, hard exclusions, precedents). Have it return a - structured list of candidates with file/line/category/ - description/exploit/fix. - - 2. For EACH candidate from step 1, spawn an independent Task - sub-agent IN PARALLEL to adversarially verify it. Each - verifier gets the full FALSE-POSITIVE FILTER above and is - told to default to "drop" if uncertain. Each returns a - confidence score 1–10. - - 3. Drop any finding with confidence < 8. For every finding that - survives, call: - - mcp__github_inline_comment__create_inline_comment - - with `{ path, line, body }` pointing at the exact file and - line in the diff. The body should follow: - - **: ** - - **Recommendation:** - - Do NOT post a single summary comment listing all findings — - the workflow handles a top-level summary after this run - completes. If zero findings survive Phase 3, exit without - calling any tool. - - Begin. - show_full_output: "true" - # Read/Grep/Glob let the model explore the repo for context - # (existing sanitization patterns, threat model, callers of a - # changed function). Task is needed for the parallel verifier - # sub-agents in Phase 2. The github_inline_comment MCP tool is - # the output channel; allow-listing it is also what tells the - # action to attach the inline-comment MCP server. Bash is - # intentionally NOT allowed: the prompt forbids running - # commands, and keeping Bash off the list makes the diff at - # /tmp/pr.diff the only ground truth (no `git diff` re-runs - # against a possibly-shallow clone). - claude_args: >- - --model us.anthropic.claude-opus-4-7 --max-turns 60 --allowedTools "Read Grep Glob Task - mcp__github_inline_comment__create_inline_comment" - - - name: Verify model ran productively - id: model-ran - if: - steps.diff.outputs.bytes != '0' && (steps.review.conclusion == 'success' || steps.review.conclusion == - 'failure') - env: - OUTPUT_JSON: ${{ steps.review.outputs.execution_file || format('{0}/claude-execution-output.json', runner.temp) }} - run: | - set -euo pipefail - if [ ! -s "$OUTPUT_JSON" ]; then - echo "::warning::No execution transcript at $OUTPUT_JSON — cannot verify" - echo "ran=unknown" >> "$GITHUB_OUTPUT" - echo "num_turns=0" >> "$GITHUB_OUTPUT" - exit 0 - fi - NUM_TURNS=$(jq -r '.[-1].num_turns // 0' "$OUTPUT_JSON") - IS_ERROR=$(jq -r '.[-1].is_error // false' "$OUTPUT_JSON") - OUTPUT_TOKENS=$(jq -r '.[-1].usage.output_tokens // 0' "$OUTPUT_JSON") - echo "num_turns=$NUM_TURNS, is_error=$IS_ERROR, output_tokens=$OUTPUT_TOKENS" - echo "num_turns=$NUM_TURNS" >> "$GITHUB_OUTPUT" - if [ "$IS_ERROR" = "true" ] || [ "$NUM_TURNS" = "0" ] || [ "$OUTPUT_TOKENS" = "0" ]; then - echo "::group::Last messages from SDK transcript" - jq -r '.[] | select(.type == "user" or .type == "system") | .message.content // .subtype' "$OUTPUT_JSON" | tail -40 - echo "::endgroup::" - echo "::error::Model did not run productively (turns=$NUM_TURNS, output_tokens=$OUTPUT_TOKENS, is_error=$IS_ERROR)" - echo "ran=false" >> "$GITHUB_OUTPUT" - exit 1 - fi - echo "ran=true" >> "$GITHUB_OUTPUT" - - - name: Count findings - id: findings - if: - steps.diff.outputs.bytes != '0' && (steps.review.conclusion == 'success' || steps.review.conclusion == - 'failure') - run: | - set -euo pipefail - BUFFER=/tmp/inline-comments-buffer.jsonl - if [ -s "$BUFFER" ]; then - COUNT=$(wc -l < "$BUFFER" | tr -d ' ') - else - COUNT=0 - fi - echo "count=$COUNT" >> "$GITHUB_OUTPUT" - echo "Buffered findings: $COUNT" - - - name: Post summary - if: always() - uses: actions/github-script@v9 - env: - PR_NUMBER: ${{ steps.pr.outputs.number }} - FINDING_COUNT: ${{ steps.findings.outputs.count }} - REVIEW_CONCLUSION: ${{ steps.review.conclusion }} - MODEL_RAN: ${{ steps.model-ran.outputs.ran }} - NUM_TURNS: ${{ steps.model-ran.outputs.num_turns }} - DIFF_BYTES: ${{ steps.diff.outputs.bytes }} - DIFF_FILES: ${{ steps.diff.outputs.files }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - with: - github-token: ${{ steps.app-token.outputs.token }} - script: | - const prNumber = parseInt(process.env.PR_NUMBER, 10); - const count = parseInt(process.env.FINDING_COUNT || '0', 10); - const conclusion = process.env.REVIEW_CONCLUSION || 'skipped'; - const modelRan = process.env.MODEL_RAN || 'unknown'; - const numTurns = process.env.NUM_TURNS || '0'; - const runUrl = process.env.RUN_URL; - const diffBytes = parseInt(process.env.DIFF_BYTES || '0', 10); - - let body; - if (diffBytes === 0) { - body = `**Claude Security Review:** PR has an empty diff against base — nothing to review. ([run](${runUrl}))`; - } else if (modelRan !== 'true') { - body = `**Claude Security Review:** the review did not analyze this PR (model took ${numTurns} turn${numTurns === '1' ? '' : 's'}). See the [run](${runUrl}) for details; a later push or re-run is needed.`; - } else if (conclusion === 'success') { - body = count > 0 - ? `**Claude Security Review:** posted ${count} inline finding${count === 1 ? '' : 's'} on this PR. ([run](${runUrl}))` - : `**Claude Security Review:** no high-confidence findings. ([run](${runUrl}))`; - } else if (conclusion === 'failure') { - body = `**Claude Security Review:** the review run failed before completing. See the [run](${runUrl}) for details.`; - } else { - body = `**Claude Security Review:** the review run was ${conclusion} before analysis could complete. See the [run](${runUrl}); a later run on this PR will replace this status.`; - } - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body, - }); - - - name: Remove reviewing label - if: always() - uses: actions/github-script@v9 - env: - PR_NUMBER: ${{ steps.pr.outputs.number }} - with: - github-token: ${{ steps.app-token.outputs.token }} - script: | - const prNumber = parseInt(process.env.PR_NUMBER, 10); - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - name: 'claude-security-reviewing', - }); - } catch (error) { - console.log('Label removal failed (may not exist):', error.message); - } + call: + uses: aws/agentcore-devx-devtools/.github/workflows/test-pr-security-review.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab + with: + runner: ubuntu + pr_number: ${{ inputs.pr_number || format('{0}', github.event.pull_request.number) }} + secrets: inherit diff --git a/.github/workflows/pr-size-title.yml b/.github/workflows/pr-size-title.yml new file mode 100644 index 00000000..00d97691 --- /dev/null +++ b/.github/workflows/pr-size-title.yml @@ -0,0 +1,21 @@ +name: PR Size and Title + +on: + push: + branches: + - feat/caller-workflows + pull_request_target: + branches: [main, feat/**] + types: [opened, edited, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + statuses: write + +jobs: + call: + uses: aws/agentcore-devx-devtools/.github/workflows/test-pr-size-title.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab + with: + runner: ubuntu + secrets: inherit diff --git a/.github/workflows/slack-issue-notification.yml b/.github/workflows/slack-issue-notification.yml new file mode 100644 index 00000000..adecedd7 --- /dev/null +++ b/.github/workflows/slack-issue-notification.yml @@ -0,0 +1,23 @@ +name: Slack Issue Notification + +on: + push: + branches: + - feat/caller-workflows + issues: + types: [opened] + issue_comment: + types: [created] + +permissions: + contents: read + id-token: write + pull-requests: write + issues: read + +jobs: + call: + uses: aws/agentcore-devx-devtools/.github/workflows/test-slack-issue-notification.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab + with: + runner: ubuntu + secrets: inherit diff --git a/.github/workflows/slack-open-prs-notification.yml b/.github/workflows/slack-open-prs-notification.yml index f59402bc..28fbbd7e 100644 --- a/.github/workflows/slack-open-prs-notification.yml +++ b/.github/workflows/slack-open-prs-notification.yml @@ -1,51 +1,21 @@ name: Slack Open PRs Notification on: + push: + branches: + - feat/caller-workflows schedule: - - cron: '0 13 * * *' # 8:00 AM EST (13:00 UTC) + - cron: '0 13 * * *' workflow_dispatch: permissions: + contents: read pull-requests: read + id-token: write jobs: - notify-slack: - runs-on: ubuntu-latest - steps: - - name: Get open PRs - id: open-prs - uses: actions/github-script@v8 - with: - script: | - const { data: prs } = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - }); - - const count = prs.length; - - // Format each PR with plain text and bare URL (Slack auto-links URLs) - const prList = prs.map(pr => - `• #${pr.number} - ${pr.title} (by ${pr.user.login})\n ${pr.html_url}` - ).join('\n'); - - core.setOutput('count', count); - - // Use GITHUB_OUTPUT delimiter for multiline support - const fs = require('fs'); - fs.appendFileSync( - process.env.GITHUB_OUTPUT, - `pr_list< Date: Fri, 24 Jul 2026 14:44:40 -0400 Subject: [PATCH 2/4] ci: switch callers to production reusables --- .github/workflows/dependabot-auto-merge.yml | 5 +---- .github/workflows/pr-automerge.yml | 9 +++------ .github/workflows/pr-security-review.yml | 5 +---- .github/workflows/pr-size-title.yml | 5 +---- .github/workflows/slack-issue-notification.yml | 5 +---- .github/workflows/slack-open-prs-notification.yml | 5 +---- .github/workflows/sync-from-public.yml | 5 +---- 7 files changed, 9 insertions(+), 30 deletions(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index ff8aaf8c..6bb962c4 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -1,9 +1,6 @@ name: Dependabot Auto-merge on: - push: - branches: - - feat/caller-workflows pull_request: types: [opened, synchronize, reopened, ready_for_review] @@ -13,5 +10,5 @@ permissions: jobs: call: - uses: aws/agentcore-devx-devtools/.github/workflows/test-dependabot-auto-merge.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab + uses: aws/agentcore-devx-devtools/.github/workflows/reusable-dependabot-auto-merge.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab secrets: inherit diff --git a/.github/workflows/pr-automerge.yml b/.github/workflows/pr-automerge.yml index 9fa77265..264730d1 100644 --- a/.github/workflows/pr-automerge.yml +++ b/.github/workflows/pr-automerge.yml @@ -1,17 +1,14 @@ name: PR Auto-merge on: - push: - branches: - - feat/caller-workflows pull_request_review: types: [submitted] permissions: - contents: read - pull-requests: read + contents: write + pull-requests: write jobs: call: - uses: aws/agentcore-devx-devtools/.github/workflows/test-pr-automerge.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab + uses: aws/agentcore-devx-devtools/.github/workflows/reusable-pr-automerge.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab secrets: inherit diff --git a/.github/workflows/pr-security-review.yml b/.github/workflows/pr-security-review.yml index 9cd30938..80685aea 100644 --- a/.github/workflows/pr-security-review.yml +++ b/.github/workflows/pr-security-review.yml @@ -1,9 +1,6 @@ name: Claude Security Review on: - push: - branches: - - feat/caller-workflows pull_request_target: types: [opened, reopened, synchronize, labeled] branches: @@ -23,7 +20,7 @@ permissions: jobs: call: - uses: aws/agentcore-devx-devtools/.github/workflows/test-pr-security-review.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab + uses: aws/agentcore-devx-devtools/.github/workflows/reusable-pr-security-review.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab with: runner: ubuntu pr_number: ${{ inputs.pr_number || format('{0}', github.event.pull_request.number) }} diff --git a/.github/workflows/pr-size-title.yml b/.github/workflows/pr-size-title.yml index 00d97691..98c32b5b 100644 --- a/.github/workflows/pr-size-title.yml +++ b/.github/workflows/pr-size-title.yml @@ -1,9 +1,6 @@ name: PR Size and Title on: - push: - branches: - - feat/caller-workflows pull_request_target: branches: [main, feat/**] types: [opened, edited, synchronize, reopened] @@ -15,7 +12,7 @@ permissions: jobs: call: - uses: aws/agentcore-devx-devtools/.github/workflows/test-pr-size-title.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab + uses: aws/agentcore-devx-devtools/.github/workflows/reusable-pr-size-title.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab with: runner: ubuntu secrets: inherit diff --git a/.github/workflows/slack-issue-notification.yml b/.github/workflows/slack-issue-notification.yml index adecedd7..18b3f9c8 100644 --- a/.github/workflows/slack-issue-notification.yml +++ b/.github/workflows/slack-issue-notification.yml @@ -1,9 +1,6 @@ name: Slack Issue Notification on: - push: - branches: - - feat/caller-workflows issues: types: [opened] issue_comment: @@ -17,7 +14,7 @@ permissions: jobs: call: - uses: aws/agentcore-devx-devtools/.github/workflows/test-slack-issue-notification.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab + uses: aws/agentcore-devx-devtools/.github/workflows/reusable-slack-issue-notification.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab with: runner: ubuntu secrets: inherit diff --git a/.github/workflows/slack-open-prs-notification.yml b/.github/workflows/slack-open-prs-notification.yml index 28fbbd7e..0e7dcf21 100644 --- a/.github/workflows/slack-open-prs-notification.yml +++ b/.github/workflows/slack-open-prs-notification.yml @@ -1,9 +1,6 @@ name: Slack Open PRs Notification on: - push: - branches: - - feat/caller-workflows schedule: - cron: '0 13 * * *' workflow_dispatch: @@ -15,7 +12,7 @@ permissions: jobs: call: - uses: aws/agentcore-devx-devtools/.github/workflows/test-slack-open-prs-notification.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab + uses: aws/agentcore-devx-devtools/.github/workflows/reusable-slack-open-prs-notification.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab with: runner: ubuntu secrets: inherit diff --git a/.github/workflows/sync-from-public.yml b/.github/workflows/sync-from-public.yml index 4aacc958..ab37a51b 100644 --- a/.github/workflows/sync-from-public.yml +++ b/.github/workflows/sync-from-public.yml @@ -1,9 +1,6 @@ name: Sync From Public on: - push: - branches: - - feat/caller-workflows schedule: - cron: '0 */6 * * *' workflow_dispatch: @@ -15,5 +12,5 @@ permissions: jobs: call: - uses: aws/agentcore-devx-devtools/.github/workflows/test-sync-from-public.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab + uses: aws/agentcore-devx-devtools/.github/workflows/reusable-sync-from-public.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab secrets: inherit From 9db06be003b6975b67011c048bbfcf2d2ef9f316 Mon Sep 17 00:00:00 2001 From: Eric He Date: Fri, 24 Jul 2026 15:24:46 -0400 Subject: [PATCH 3/4] docs: clarify open PR notification schedule --- .github/workflows/slack-open-prs-notification.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/slack-open-prs-notification.yml b/.github/workflows/slack-open-prs-notification.yml index 0e7dcf21..29e13711 100644 --- a/.github/workflows/slack-open-prs-notification.yml +++ b/.github/workflows/slack-open-prs-notification.yml @@ -2,7 +2,7 @@ name: Slack Open PRs Notification on: schedule: - - cron: '0 13 * * *' + - cron: '0 13 * * *' # 8:00 AM EST / 9:00 AM EDT (13:00 UTC) workflow_dispatch: permissions: From 3d92c2bcf9b1a8e981800a386b0999d0f636e8e3 Mon Sep 17 00:00:00 2001 From: Junyang He <106281470+JunyangHe@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:03:43 -0400 Subject: [PATCH 4/4] Delete .github/workflows/sync-from-public.yml Signed-off-by: Junyang He <106281470+JunyangHe@users.noreply.github.com> --- .github/workflows/sync-from-public.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github/workflows/sync-from-public.yml diff --git a/.github/workflows/sync-from-public.yml b/.github/workflows/sync-from-public.yml deleted file mode 100644 index ab37a51b..00000000 --- a/.github/workflows/sync-from-public.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Sync From Public - -on: - schedule: - - cron: '0 */6 * * *' - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - id-token: write - -jobs: - call: - uses: aws/agentcore-devx-devtools/.github/workflows/reusable-sync-from-public.yml@458c0a684af0f9e3a013ec05cd23851def4f9cab - secrets: inherit