diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 09ed2343c..64872ada1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -27,14 +27,35 @@ updates: prefix: 'chore' include: 'scope' open-pull-requests-limit: 10 + cooldown: + default-days: 1 - package-ecosystem: 'github-actions' directory: '/' schedule: - interval: 'weekly' + interval: 'daily' groups: - github-actions: + minor-and-patch: + patterns: + - '*' + update-types: + - 'minor' + - 'patch' + # Workaround for dependabot/dependabot-core#14202: without an explicit + # major group, major updates matching the minor-and-patch pattern are + # silently suppressed. Remove this group when #14202 is fixed to get + # individual (ungrouped) PRs per major bump instead. + major: patterns: - '*' + update-types: + - 'major' + labels: + - 'dependencies' + - 'github-actions' commit-message: prefix: 'ci' + include: 'scope' + open-pull-requests-limit: 10 + cooldown: + default-days: 1 diff --git a/.github/workflows/agent-restricted.yml b/.github/workflows/agent-restricted.yml index 7a0948861..9ba260dac 100644 --- a/.github/workflows/agent-restricted.yml +++ b/.github/workflows/agent-restricted.yml @@ -56,15 +56,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Check authorization + env: + AUTHORIZED_USERS: ${{ secrets.AUTHORIZED_USERS }} + ACTOR: ${{ github.actor }} run: | - AUTHORIZED_USERS="${{ secrets.AUTHORIZED_USERS }}" - if [[ ",$AUTHORIZED_USERS," != *",${{ github.actor }},"* ]]; then - echo "❌ User ${{ github.actor }} is NOT authorized" + if [[ ",$AUTHORIZED_USERS," != *",$ACTOR,"* ]]; then + echo "❌ User $ACTOR is NOT authorized" exit 1 fi - echo "✅ User ${{ github.actor }} is authorized" + echo "✅ User $ACTOR is authorized" - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Run Strands Agent uses: ./.github/actions/strands-action diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 88b1a857a..ad65f3740 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -25,9 +25,11 @@ jobs: node-version: [20.x, 22.x, 24.x] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ matrix.node-version }} cache: 'npm' @@ -36,14 +38,14 @@ jobs: git config --global user.email "bedrock-agentcore-npm+ci@amazon.com" git config --global user.name "CI" - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 - run: npm ci - run: npm run build --if-present - run: npm run test:unit - run: npm run test:integ - name: Upload coverage artifact if: matrix.node-version == '20.x' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: coverage-report path: coverage/ @@ -61,7 +63,7 @@ jobs: test -f sanitytest/agentcore/agentcore.json - name: Upload tarball artifact if: matrix.node-version == '20.x' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: tarball path: '*.tgz' @@ -75,14 +77,16 @@ jobs: pull-requests: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Download coverage artifact - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: coverage-report path: coverage/ - name: Coverage Report - uses: davelosert/vitest-coverage-report-action@v2 + uses: davelosert/vitest-coverage-report-action@02f3c2e641286b7fa308cd3e430783103ce6103b # v2 with: json-summary-path: coverage/coverage-summary.json json-final-path: coverage/coverage-final.json diff --git a/.github/workflows/cleanup-pr-tarballs.yml b/.github/workflows/cleanup-pr-tarballs.yml index 1892f3a17..082907ea7 100644 --- a/.github/workflows/cleanup-pr-tarballs.yml +++ b/.github/workflows/cleanup-pr-tarballs.yml @@ -14,7 +14,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Delete PR tarball releases older than 7 days env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1e9b0a4bd..1531bd41d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,6 +8,9 @@ on: pull_request_target: branches: ['main'] +permissions: + contents: read + # Cancel in-progress runs for PRs; never cancel runs on main (merges should not abort each other) concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -23,12 +26,14 @@ jobs: contents: read steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: languages: javascript-typescript - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 000000000..15a44138a --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,32 @@ +name: Dependabot Auto-Merge (Minor/Patch) + +on: + pull_request_target: + types: [opened, synchronize] + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'dependabot[bot]' + steps: + - name: Fetch Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Auto-approve and merge minor/patch github-actions updates + if: >- + steps.metadata.outputs.package-ecosystem == 'github_actions' && (steps.metadata.outputs.update-type == + 'version-update:semver-minor' || + steps.metadata.outputs.update-type == 'version-update:semver-patch') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: | + gh pr review "$PR_URL" --approve + gh pr merge "$PR_URL" --auto --merge diff --git a/.github/workflows/dependabot-major-analysis.yml b/.github/workflows/dependabot-major-analysis.yml new file mode 100644 index 000000000..8a86404e6 --- /dev/null +++ b/.github/workflows/dependabot-major-analysis.yml @@ -0,0 +1,144 @@ +name: Dependabot Major Version Analysis + +on: + pull_request_target: + types: [opened] + +permissions: + contents: read + pull-requests: write + +jobs: + analyze-major: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'dependabot[bot]' + steps: + - name: Fetch Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Analyze major version bump + if: >- + steps.metadata.outputs.package-ecosystem == 'github_actions' && steps.metadata.outputs.update-type == + 'version-update:semver-major' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + DEP_NAME: ${{ steps.metadata.outputs.dependency-names }} + PREV_VERSION: ${{ steps.metadata.outputs.previous-version }} + NEW_VERSION: ${{ steps.metadata.outputs.new-version }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const depName = process.env.DEP_NAME; + const prevVersion = process.env.PREV_VERSION; + const newVersion = process.env.NEW_VERSION; + const parts = depName.split('/'); + const owner = parts[0]; + const repo = parts[1]; + const repoSlug = `${owner}/${repo}`; + + let releases = []; + try { + const { data } = await github.rest.repos.listReleases({ owner, repo, per_page: 50 }); + releases = data; + } catch (err) { + core.warning(`Could not fetch releases for ${repoSlug}: ${err.message}`); + } + + const prevMajor = parseInt(prevVersion.replace(/^v/, ''), 10); + const newMajor = parseInt(newVersion.replace(/^v/, ''), 10); + + const relevantReleases = releases.filter(r => { + const major = parseInt(r.tag_name.replace(/^v/, ''), 10); + return major > prevMajor && major <= newMajor; + }); + + let releaseNotesSummary = ''; + let breakingChanges = ''; + + if (relevantReleases.length === 0) { + releaseNotesSummary = '_No releases found between these versions._'; + breakingChanges = `_Unable to determine breaking changes automatically. Please review the [full changelog](https://github.com/${repoSlug}/releases)._`; + } else { + for (const release of relevantReleases.slice(0, 10)) { + const body = release.body || '_No release notes._'; + releaseNotesSummary += `### ${release.tag_name}${release.name && release.name !== release.tag_name ? ' — ' + release.name : ''}\n\n`; + releaseNotesSummary += body.substring(0, 2000); + if (body.length > 2000) releaseNotesSummary += '\n\n_...truncated_'; + releaseNotesSummary += '\n\n---\n\n'; + const lines = body.split('\n'); + for (const line of lines) { + if (/breaking|BREAKING|removed|deprecated|incompatible|migration/i.test(line)) { + breakingChanges += `- ${line.trim()}\n`; + } + } + } + } + + if (!breakingChanges) { + breakingChanges = '_No explicit breaking changes detected in release notes. Manual review recommended._'; + } + + let commentBody = `## :warning: Major Version Update — Manual Review Required + + | Field | Value | + |-------|-------| + | **Action** | [\`${depName}\`](https://github.com/${repoSlug}) | + | **Previous** | \`v${prevVersion}\` | + | **New** | \`v${newVersion}\` | + | **Type** | Major (\`v${prevMajor}\` → \`v${newMajor}\`) | + + ### Breaking Changes + + ${breakingChanges} + + ### Release Notes (v${prevMajor + 1} → v${newMajor}) + + ${releaseNotesSummary} + + ### Next Steps + + 1. Review breaking changes above + 2. Check if workflow inputs/outputs changed + 3. Verify compatibility with your CI/CD configuration + + > Full changelog: https://github.com/${repoSlug}/releases + + --- + _Generated automatically for Dependabot major version PRs._`.replace(/^ /gm, ''); + + if (commentBody.length > 64000) { + commentBody = commentBody.substring(0, 63900) + '\n\n_...comment truncated due to size limit._'; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: commentBody, + }); + + try { + const labelsToAdd = ['major-update', 'needs-review']; + for (const label of labelsToAdd) { + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch { + const colors = { 'major-update': 'B60205', 'needs-review': 'FBCA04' }; + await github.rest.issues.createLabel({ + owner: context.repo.owner, repo: context.repo.repo, + name: label, color: colors[label] || 'EDEDED', + }); + } + } + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: labelsToAdd, + }); + } catch (err) { + core.warning(`Could not add labels: ${err.message}`); + } diff --git a/.github/workflows/e2e-tests-full.yml b/.github/workflows/e2e-tests-full.yml index 50528a8b3..ce6ae60e4 100644 --- a/.github/workflows/e2e-tests-full.yml +++ b/.github/workflows/e2e-tests-full.yml @@ -28,10 +28,11 @@ jobs: matrix: cdk-source: [npm, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'main' }} - - uses: actions/setup-node@v6 + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: '20.x' cache: 'npm' @@ -39,9 +40,9 @@ jobs: run: | git config --global user.email "ci@amazon.com" git config --global user.name "CI" - - uses: astral-sh/setup-uv@v7 + - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v6 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6 with: role-to-assume: ${{ secrets.E2E_AWS_ROLE_ARN }} aws-region: ${{ inputs.aws_region || 'us-east-1' }} @@ -49,7 +50,7 @@ jobs: id: aws run: echo "account_id=$(aws sts get-caller-identity --query Account --output text)" >> "$GITHUB_OUTPUT" - name: Get API keys from Secrets Manager - uses: aws-actions/aws-secretsmanager-get-secrets@v3 + uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 # v3 with: secret-ids: | E2E,${{ secrets.E2E_SECRET_ARN }} diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 025b7b600..c5254ca2c 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -2,14 +2,16 @@ name: E2E Tests on: workflow_dispatch: inputs: + pr_number: + description: 'PR number to test (checks out the PR merge ref)' + required: false + type: string aws_region: description: 'AWS region for deployment' default: 'us-east-1' - pull_request_target: - branches: [main] concurrency: - group: e2e-${{ github.event.pull_request.number || github.ref }} + group: e2e-${{ inputs.pr_number || github.ref }} cancel-in-progress: false permissions: @@ -17,33 +19,7 @@ permissions: contents: read jobs: - authorize: - runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' - outputs: - is_authorized: ${{ steps.check.outputs.is_authorized }} - steps: - - name: Check authorization - id: check - run: | - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - echo "✅ Manual workflow dispatch — authorized" - echo "is_authorized=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - AUTHORIZED_USERS="${{ secrets.AUTHORIZED_USERS }}" - if [[ ",$AUTHORIZED_USERS," == *",${{ github.actor }},"* ]]; then - echo "✅ User ${{ github.actor }} is authorized" - echo "is_authorized=true" >> "$GITHUB_OUTPUT" - else - echo "⏭️ User ${{ github.actor }} is not in AUTHORIZED_USERS — skipping E2E tests." - echo "ℹ️ External contributors: ask a maintainer to run the E2E tests manually via workflow_dispatch." - echo "is_authorized=false" >> "$GITHUB_OUTPUT" - fi - e2e: - needs: authorize - if: needs.authorize.outputs.is_authorized == 'true' runs-on: ubuntu-latest environment: e2e-testing timeout-minutes: 30 @@ -52,10 +28,24 @@ jobs: matrix: cdk-source: [npm, main] steps: - - uses: actions/checkout@v6 + - name: Resolve checkout ref + id: ref + env: + PR_NUMBER: ${{ inputs.pr_number }} + GH_REF: ${{ github.ref }} + run: | + if [[ -n "$PR_NUMBER" ]]; then + echo "ref=refs/pull/${PR_NUMBER}/merge" >> "$GITHUB_OUTPUT" + echo "Checking out PR #${PR_NUMBER} merge ref" + else + echo "ref=${GH_REF}" >> "$GITHUB_OUTPUT" + echo "Checking out ${GH_REF}" + fi + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - ref: ${{ github.event.pull_request.head.sha }} - - uses: actions/setup-node@v6 + ref: ${{ steps.ref.outputs.ref }} + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: '20.x' cache: 'npm' @@ -63,9 +53,9 @@ jobs: run: | git config --global user.email "ci@amazon.com" git config --global user.name "CI" - - uses: astral-sh/setup-uv@v7 + - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v6 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6 with: role-to-assume: ${{ secrets.E2E_AWS_ROLE_ARN }} aws-region: ${{ inputs.aws_region || 'us-east-1' }} @@ -73,7 +63,7 @@ jobs: id: aws run: echo "account_id=$(aws sts get-caller-identity --query Account --output text)" >> "$GITHUB_OUTPUT" - name: Get API keys from Secrets Manager - uses: aws-actions/aws-secretsmanager-get-secrets@v3 + uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 # v3 with: secret-ids: | E2E,${{ secrets.E2E_SECRET_ARN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 24a9317f6..647da4b3d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,15 +18,17 @@ jobs: setup: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 20.x cache: 'npm' - run: npm ci - name: Cache node_modules - uses: actions/cache/save@v5 + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: node_modules key: node-modules-${{ hashFiles('package-lock.json') }} @@ -35,11 +37,13 @@ jobs: needs: setup runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 20.x - - uses: actions/cache/restore@v5 + - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: node_modules key: node-modules-${{ hashFiles('package-lock.json') }} @@ -49,11 +53,13 @@ jobs: needs: setup runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 20.x - - uses: actions/cache/restore@v5 + - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: node_modules key: node-modules-${{ hashFiles('package-lock.json') }} @@ -63,11 +69,13 @@ jobs: needs: setup runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 20.x - - uses: actions/cache/restore@v5 + - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: node_modules key: node-modules-${{ hashFiles('package-lock.json') }} @@ -77,11 +85,13 @@ jobs: needs: setup runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 20.x - - uses: actions/cache/restore@v5 + - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: node_modules key: node-modules-${{ hashFiles('package-lock.json') }} @@ -91,11 +101,13 @@ jobs: needs: setup runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 20.x - - uses: actions/cache/restore@v5 + - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: node_modules key: node-modules-${{ hashFiles('package-lock.json') }} @@ -104,9 +116,10 @@ jobs: schema-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 + persist-credentials: false - name: Reject schema changes outside release PRs run: | if git diff --name-only origin/main...HEAD | grep -q '^schemas/agentcore\.schema\.v[0-9]*\.json$'; then diff --git a/.github/workflows/pr-size.yml b/.github/workflows/pr-size.yml index 1982c05fd..2e1f54a9a 100644 --- a/.github/workflows/pr-size.yml +++ b/.github/workflows/pr-size.yml @@ -6,6 +6,9 @@ on: pull_request_target: branches: [main] +permissions: + contents: read + jobs: label-size: runs-on: ubuntu-latest @@ -13,7 +16,7 @@ jobs: pull-requests: write steps: - name: Calculate PR size and apply label - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const prNumber = context.payload.pull_request.number; diff --git a/.github/workflows/pr-tarball-publish.yml b/.github/workflows/pr-tarball-publish.yml new file mode 100644 index 000000000..5f732467e --- /dev/null +++ b/.github/workflows/pr-tarball-publish.yml @@ -0,0 +1,88 @@ +name: PR Tarball (Publish) + +# Runs in the context of the BASE branch after PR Tarball (Build) completes. +# This workflow has write permissions but never checks out or executes PR code. +on: + workflow_run: + workflows: ['PR Tarball (Build)'] + types: [completed] + +permissions: + contents: write + pull-requests: write + +jobs: + publish-tarball: + runs-on: ubuntu-latest + if: >- + github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' + steps: + - name: Get PR number + id: pr + env: + GH_TOKEN: ${{ github.token }} + RUN_ID: ${{ github.event.workflow_run.id }} + REPO: ${{ github.repository }} + run: | + # The triggering workflow_run carries the PR info in its pull_requests array + PR_NUMBER=$(gh api "repos/${REPO}/actions/runs/${RUN_ID}" \ + --jq '.pull_requests[0].number') + if [[ -z "$PR_NUMBER" || "$PR_NUMBER" == "null" ]]; then + echo "::error::Could not determine PR number from workflow run" + exit 1 + fi + echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + + - name: Download tarball artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: pr-tarball + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Get tarball info + id: tarball + run: | + TARBALL_NAME=$(ls *.tgz | head -1 | xargs basename) + echo "name=$TARBALL_NAME" >> "$GITHUB_OUTPUT" + + - name: Create or update PR release + id: release + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + TARBALL_NAME: ${{ steps.tarball.outputs.name }} + PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + REPO: ${{ github.repository }} + run: | + TAG="pr-${PR_NUMBER}-tarball" + + # Delete existing release if it exists (to update the tarball) + gh release delete "$TAG" --yes --cleanup-tag 2>/dev/null || true + + # Create a new pre-release with the tarball + gh release create "$TAG" \ + "${TARBALL_NAME}" \ + --title "PR #${PR_NUMBER} Tarball" \ + --notes "Auto-generated tarball for PR #${PR_NUMBER}." \ + --draft \ + --target "$PR_HEAD_SHA" + + DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${TAG}/${TARBALL_NAME}" + echo "url=$DOWNLOAD_URL" >> "$GITHUB_OUTPUT" + + - name: Comment on PR + uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + with: + number: ${{ steps.pr.outputs.number }} + header: tarball + message: | + ## Package Tarball + + **[${{ steps.tarball.outputs.name }}](${{ steps.release.outputs.url }})** + + ### How to install + + ```bash + npm install ${{ steps.release.outputs.url }} + ``` diff --git a/.github/workflows/pr-tarball.yml b/.github/workflows/pr-tarball.yml index 3c5c5c522..41cd6b9e6 100644 --- a/.github/workflows/pr-tarball.yml +++ b/.github/workflows/pr-tarball.yml @@ -1,39 +1,19 @@ -name: PR Tarball +name: PR Tarball (Build) on: - pull_request_target: + pull_request: branches: [main] permissions: - contents: write - pull-requests: write + contents: read jobs: - authorize: + build-tarball: runs-on: ubuntu-latest - outputs: - is_authorized: ${{ steps.check.outputs.is_authorized }} steps: - - name: Check authorization - id: check - run: | - AUTHORIZED_USERS="${{ secrets.AUTHORIZED_USERS }}" - if [[ ",$AUTHORIZED_USERS," == *",${{ github.actor }},"* ]]; then - echo "✅ User ${{ github.actor }} is authorized" - echo "is_authorized=true" >> "$GITHUB_OUTPUT" - else - echo "⏭️ User ${{ github.actor }} is not in AUTHORIZED_USERS — skipping." - echo "is_authorized=false" >> "$GITHUB_OUTPUT" - fi - - pr-tarball: - needs: authorize - if: needs.authorize.outputs.is_authorized == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - ref: ${{ github.event.pull_request.head.sha }} - - uses: actions/setup-node@v6 + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: '20.x' cache: 'npm' @@ -41,7 +21,7 @@ jobs: run: | git config --global user.email "bedrock-agentcore-npm+ci@amazon.com" git config --global user.name "CI" - - uses: astral-sh/setup-uv@v7 + - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 - run: npm ci - run: npm run build --if-present - run: npm pack @@ -49,40 +29,10 @@ jobs: id: tarball run: | TARBALL_NAME=$(ls *.tgz | head -1 | xargs basename) - echo "name=$TARBALL_NAME" >> $GITHUB_OUTPUT - - name: Create or update PR release - id: release - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - TARBALL_NAME: ${{ steps.tarball.outputs.name }} - run: | - TAG="pr-${PR_NUMBER}-tarball" - - # Delete existing release if it exists (to update the tarball) - gh release delete "$TAG" --yes --cleanup-tag 2>/dev/null || true - - # Create a new pre-release with the tarball - gh release create "$TAG" \ - "${TARBALL_NAME}" \ - --title "PR #${PR_NUMBER} Tarball" \ - --notes "Auto-generated tarball for PR #${PR_NUMBER}." \ - --draft \ - --target "${{ github.event.pull_request.head.sha }}" - - DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/${TARBALL_NAME}" - echo "url=$DOWNLOAD_URL" >> $GITHUB_OUTPUT - - name: Comment on PR - uses: marocchino/sticky-pull-request-comment@v3 + echo "name=$TARBALL_NAME" >> "$GITHUB_OUTPUT" + - name: Upload tarball as artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - header: tarball - message: | - ## Package Tarball - - **[${{ steps.tarball.outputs.name }}](${{ steps.release.outputs.url }})** - - ### How to install - - ```bash - npm install ${{ steps.release.outputs.url }} - ``` + name: pr-tarball + path: '*.tgz' + retention-days: 30 diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 3ab976395..217f6e8c4 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -14,7 +14,7 @@ jobs: permissions: pull-requests: read steps: - - uses: amannn/action-semantic-pull-request@v6.1.1 + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21afc4e1f..89caef3bc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,19 +38,21 @@ jobs: steps: - name: Validate running from main + env: + GIT_REF: ${{ github.ref }} run: | - if [[ "${{ github.ref }}" != "refs/heads/main" ]]; then - echo "⚠️ WARNING: Running from ${{ github.ref }}" + if [[ "$GIT_REF" != "refs/heads/main" ]]; then + echo "⚠️ WARNING: Running from $GIT_REF" echo "⚠️ Production releases should only run from main branch" fi - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 20.x @@ -200,9 +202,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: release/v${{ needs.prepare-release.outputs.version }} + persist-credentials: false - name: Verify version before build env: @@ -221,7 +224,7 @@ jobs: echo "✓ Version verified: $ACTUAL_VERSION" - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 20.x @@ -252,7 +255,7 @@ jobs: run: npm pack - name: Upload artifacts - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: dist path: | @@ -295,7 +298,7 @@ jobs: steps: - name: Checkout latest main (AFTER PR merge) - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: main fetch-depth: 0 @@ -309,7 +312,7 @@ jobs: git log -1 --oneline - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22.x registry-url: 'https://registry.npmjs.org' @@ -321,7 +324,7 @@ jobs: echo "Updated npm version: $(npm --version)" - name: Download artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: dist path: . @@ -377,7 +380,7 @@ jobs: git push origin "v$VERSION" - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 env: VERSION: ${{ steps.version.outputs.version }} GITHUB_REPOSITORY: ${{ github.repository }} diff --git a/.github/workflows/security_zizmor.yml b/.github/workflows/security_zizmor.yml new file mode 100644 index 000000000..217bb0689 --- /dev/null +++ b/.github/workflows/security_zizmor.yml @@ -0,0 +1,47 @@ +name: security / zizmor + +on: + push: + branches: [main, master] + paths: + - '.github/workflows/**' + - '.github/actions/**' + - '.github/zizmor.yml' + - '.github/dependabot.yml' + pull_request: + paths: + - '.github/workflows/**' + - '.github/actions/**' + - '.github/zizmor.yml' + - '.github/dependabot.yml' + schedule: + - cron: '0 9 * * 1' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'schedule' && github.run_id || github.ref }} + cancel-in-progress: ${{ github.event_name != 'schedule' }} + +permissions: + contents: read + +jobs: + zizmor: + name: Static analysis (zizmor) + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + with: + min-severity: medium + advanced-security: false + annotations: true + config: .github/zizmor.yml diff --git a/.github/workflows/slack-issue-notification.yml b/.github/workflows/slack-issue-notification.yml index 4f2b76597..d046bb00d 100644 --- a/.github/workflows/slack-issue-notification.yml +++ b/.github/workflows/slack-issue-notification.yml @@ -4,12 +4,15 @@ on: issues: types: [opened] +permissions: + contents: read + jobs: notify-slack: runs-on: ubuntu-latest steps: - name: Send issue details to Slack - uses: slackapi/slack-github-action@v3.0.1 + uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: webhook-trigger diff --git a/.github/workflows/slack-open-prs-notification.yml b/.github/workflows/slack-open-prs-notification.yml index 51ec8078b..bd8d5936c 100644 --- a/.github/workflows/slack-open-prs-notification.yml +++ b/.github/workflows/slack-open-prs-notification.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Get open PRs id: open-prs - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { data: prs } = await github.rest.pulls.list({ @@ -40,7 +40,7 @@ jobs: ); - name: Send open PRs summary to Slack - uses: slackapi/slack-github-action@v3.0.1 + uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 with: webhook: ${{ secrets.SLACK_OPEN_PRS_WEBHOOK_URL }} webhook-type: webhook-trigger diff --git a/.github/workflows/strands-command.yml b/.github/workflows/strands-command.yml index bfba8c80d..a964f215e 100644 --- a/.github/workflows/strands-command.yml +++ b/.github/workflows/strands-command.yml @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check authorization - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | // Skip auth check for workflow_dispatch (manual runs) @@ -65,12 +65,12 @@ jobs: } - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - name: Add strands-running label - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const issueNumber = ${{ inputs.issue_id || github.event.issue.number || github.event.pull_request.number }}; @@ -83,7 +83,7 @@ jobs: - name: Process inputs and build prompts id: process-inputs - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const processInputs = require('./.github/scripts/javascript/process-inputs.cjs'); @@ -112,7 +112,7 @@ jobs: - name: Remove strands-running label if: always() - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | try { diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 000000000..289154cf2 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,52 @@ +rules: + dangerous-triggers: + ignore: + # Dependabot auto-merge: uses pull_request_target for write token. + # Does NOT checkout PR code. Actor-gated to dependabot[bot]. + - dependabot-auto-merge.yml + # Dependabot major analysis: uses pull_request_target for PR comments. + # Does NOT checkout PR code. Actor-gated to dependabot[bot]. + - dependabot-major-analysis.yml + # Pre-existing: codeql.yml uses pull_request_target for code scanning. + - codeql.yml + # Pre-existing: pr-size.yml uses pull_request_target for PR labels. + - pr-size.yml + # Pre-existing: pr-title.yml uses pull_request_target for title validation. + - pr-title.yml + # pr-tarball-publish.yml uses workflow_run to run in base-branch context + # with write permissions. It never checks out or executes PR code — it only + # downloads the build artifact and publishes it as a GitHub release. + - pr-tarball-publish.yml + + # Pre-existing: template expansions in composite action and workflow run steps. + # strands-action uses inputs.* in run steps (composite action, inputs are typed). + # strands-command.yml interpolates inputs.* and github.event.issue.number into + # actions/github-script blocks; write-access-only triggers make exploitation unlikely. + template-injection: + ignore: + - action.yml + - strands-command.yml + + # Pre-existing: composite action uses unpinned action references. + # These are first-party or well-known actions within the composite action. + unpinned-uses: + ignore: + - action.yml + + # Pre-existing: release.yml sets broad permissions at workflow level. + # release needs contents/pull-requests write for creating releases. + excessive-permissions: + ignore: + - release.yml + + # e2e-tests.yml writes to GITHUB_ENV (CDK_TARBALL path from npm pack). + # The value comes from a trusted local command, not user input. + # Workflow is now workflow_dispatch-only (maintainer-triggered). + github-env: + ignore: + - e2e-tests.yml + + # Pre-existing: release.yml uses setup-node with caching enabled. + cache-poisoning: + ignore: + - release.yml