diff --git a/.gitattributes b/.gitattributes index ea5ed37e3..934a32579 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,6 +8,7 @@ /.github/pull_request_template.md linguist-generated /.github/workflows/auto-approve.yml linguist-generated /.github/workflows/auto-queue.yml linguist-generated +/.github/workflows/bootstrap-template-protection.yml linguist-generated /.github/workflows/build.yml linguist-generated /.github/workflows/codecov.yml linguist-generated /.github/workflows/integ.yml linguist-generated diff --git a/.github/workflows/bootstrap-template-protection.yml b/.github/workflows/bootstrap-template-protection.yml new file mode 100644 index 000000000..684aa43af --- /dev/null +++ b/.github/workflows/bootstrap-template-protection.yml @@ -0,0 +1,125 @@ +# ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". + +name: bootstrap-template-protection +on: + pull_request: + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled +jobs: + check-bootstrap-template: + name: Check Bootstrap Template Changes + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout merge commit + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: refs/pull/${{ github.event.pull_request.number }}/merge + - name: Checkout base branch + run: git fetch origin ${{ github.event.pull_request.base.ref }} + - name: Check if bootstrap template changed + id: template-changed + run: |- + # Check if the bootstrap template differs between base and merge commit + if ! git diff --quiet --name-only origin/${{ github.event.pull_request.base.ref }}..HEAD -- packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml; then + echo "Bootstrap template modified - protection checks required" + echo "changed=true" >> $GITHUB_OUTPUT + else + echo "✅ Bootstrap template not modified - no protection required" + echo "changed=false" >> $GITHUB_OUTPUT + fi + - name: Extract current and previous bootstrap versions + id: version-check + if: steps.template-changed.outputs.changed == 'true' + run: |- + # Get current version from PR - look for CdkBootstrapVersion Value + CURRENT_VERSION=$(yq '.Resources.CdkBootstrapVersion.Properties.Value' packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml) + + # Get previous version from base branch + git show origin/${{ github.event.pull_request.base.ref }}:packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml > /tmp/base-template.yaml + PREVIOUS_VERSION=$(yq '.Resources.CdkBootstrapVersion.Properties.Value' /tmp/base-template.yaml) + + echo "current-version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "previous-version=$PREVIOUS_VERSION" >> $GITHUB_OUTPUT + + if [ "$CURRENT_VERSION" -gt "$PREVIOUS_VERSION" ]; then + echo "version-incremented=true" >> $GITHUB_OUTPUT + else + echo "version-incremented=false" >> $GITHUB_OUTPUT + fi + - name: Check for security review and exemption labels + id: label-check + if: steps.template-changed.outputs.changed == 'true' + run: |- + if [[ "${{ contains(github.event.pull_request.labels.*.name, 'pr/security-reviewed') }}" == "true" ]]; then + echo "has-security-label=true" >> $GITHUB_OUTPUT + else + echo "has-security-label=false" >> $GITHUB_OUTPUT + fi + + if [[ "${{ contains(github.event.pull_request.labels.*.name, 'pr/exempt-bootstrap-version') }}" == "true" ]]; then + echo "has-version-exempt-label=true" >> $GITHUB_OUTPUT + else + echo "has-version-exempt-label=false" >> $GITHUB_OUTPUT + fi + - name: Post comment + if: steps.template-changed.outputs.changed == 'true' + uses: thollander/actions-comment-pull-request@v3 + with: + comment-tag: bootstrap-template-protection + mode: recreate + message: | + ## ⚠️ Bootstrap Template Protection + + This PR modifies the bootstrap template (`packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml`), which requires special protections. + + ${{ ((steps.version-check.outputs.version-incremented == 'true' || steps.label-check.outputs.has-version-exempt-label == 'true') && steps.label-check.outputs.has-security-label == 'true') && '**✅ All requirements met! This PR can proceed with normal review process.**' || '**❌ This PR cannot be merged until all requirements are met.**' }} + + ### Requirements + + **Version Increment** + ${{ (steps.version-check.outputs.version-incremented == 'true' && format('✅ Version incremented from {0} to {1}', steps.version-check.outputs.previous-version, steps.version-check.outputs.current-version)) || (steps.label-check.outputs.has-version-exempt-label == 'true' && format('✅ Version increment exempted (PR has `{0}` label)', 'pr/exempt-bootstrap-version')) || '❌ Version increment required' }} + ${{ steps.version-check.outputs.version-incremented != 'true' && steps.label-check.outputs.has-version-exempt-label != 'true' && format(' - Current version: `{0}`', steps.version-check.outputs.current-version) || '' }} + ${{ steps.version-check.outputs.version-incremented != 'true' && steps.label-check.outputs.has-version-exempt-label != 'true' && format(' - Previous version: `{0}`', steps.version-check.outputs.previous-version) || '' }} + ${{ steps.version-check.outputs.version-incremented != 'true' && steps.label-check.outputs.has-version-exempt-label != 'true' && ' - Please increment the version in `CdkBootstrapVersion`' || '' }} + ${{ steps.version-check.outputs.version-incremented != 'true' && steps.label-check.outputs.has-version-exempt-label != 'true' && format(' - Or add the `{0}` label if not needed', 'pr/exempt-bootstrap-version') || '' }} + + **Security Review** + ${{ (steps.label-check.outputs.has-security-label == 'true' && format('✅ Review completed (PR has `{0}` label)', 'pr/security-reviewed')) || '❌ Review required' }} + ${{ steps.label-check.outputs.has-security-label != 'true' && ' - A maintainer will conduct a security review' || '' }} + ${{ steps.label-check.outputs.has-security-label != 'true' && format(' - Once reviewed, they will add the `{0}` label', 'pr/security-reviewed') || '' }} + + ### Why these protections exist + - The bootstrap template contains critical infrastructure + - Changes can affect IAM roles, policies, and resource access across all CDK deployments + - Version increments ensure users are notified of updates + - name: Check requirements + if: steps.template-changed.outputs.changed == 'true' + run: |- + # Check version requirement (either incremented or exempted) + VERSION_INCREMENTED="${{ steps.version-check.outputs.version-incremented }}" + VERSION_EXEMPTED="${{ steps.label-check.outputs.has-version-exempt-label }}" + SECURITY_REVIEWED="${{ steps.label-check.outputs.has-security-label }}" + + # Both requirements must be met + if [[ "$VERSION_INCREMENTED" == "true" || "$VERSION_EXEMPTED" == "true" ]] && [[ "$SECURITY_REVIEWED" == "true" ]]; then + echo "✅ All requirements met!" + exit 0 + fi + + # Show what's missing + echo "❌ Requirements not met:" + if [[ "$VERSION_INCREMENTED" != "true" && "$VERSION_EXEMPTED" != "true" ]]; then + echo " - Version must be incremented OR add 'pr/exempt-bootstrap-version' label" + fi + if [[ "$SECURITY_REVIEWED" != "true" ]]; then + echo " - PR must have 'pr/security-reviewed' label" + fi + exit 1 diff --git a/.gitignore b/.gitignore index f0a2cc9a9..ccfa01a6c 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ jspm_packages/ !/aws-cdk-cli.code-workspace /.nx !/nx.json +!/.github/workflows/bootstrap-template-protection.yml !/.eslintrc.json !/.github/dependabot.yml !/.github/workflows/integ.yml diff --git a/.projen/files.json b/.projen/files.json index 53527d06a..35a2c716b 100644 --- a/.projen/files.json +++ b/.projen/files.json @@ -6,6 +6,7 @@ ".github/pull_request_template.md", ".github/workflows/auto-approve.yml", ".github/workflows/auto-queue.yml", + ".github/workflows/bootstrap-template-protection.yml", ".github/workflows/build.yml", ".github/workflows/codecov.yml", ".github/workflows/integ.yml", diff --git a/.projenrc.ts b/.projenrc.ts index e34bdf76d..26c1087fe 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -5,6 +5,7 @@ import * as pj from 'projen'; import { Stability } from 'projen/lib/cdk'; import type { Job } from 'projen/lib/github/workflows-model'; import { AdcPublishing } from './projenrc/adc-publishing'; +import { BootstrapTemplateProtection } from './projenrc/bootstrap-template-protection'; import { BundleCli } from './projenrc/bundle'; import { CdkCliIntegTestsWorkflow } from './projenrc/cdk-cli-integ-tests'; import { CodeCovWorkflow } from './projenrc/codecov'; @@ -290,6 +291,7 @@ const repoProject = new yarn.Monorepo({ new AdcPublishing(repoProject); new RecordPublishingTimestamp(repoProject); +new BootstrapTemplateProtection(repoProject); // Eslint for projen config // @ts-ignore diff --git a/projenrc/bootstrap-template-protection.ts b/projenrc/bootstrap-template-protection.ts new file mode 100644 index 000000000..00df4c96e --- /dev/null +++ b/projenrc/bootstrap-template-protection.ts @@ -0,0 +1,169 @@ +import type { IConstruct } from 'constructs'; +import { Component, github as gh } from 'projen'; +import { GitHub } from 'projen/lib/github'; + +export interface BootstrapTemplateProtectionOptions { + readonly bootstrapTemplatePath?: string; +} + +export class BootstrapTemplateProtection extends Component { + constructor(scope: IConstruct, options: BootstrapTemplateProtectionOptions = {}) { + super(scope); + + const SECURITY_REVIEWED_LABEL = 'pr/security-reviewed'; + const VERSION_EXEMPT_LABEL = 'pr/exempt-bootstrap-version'; + const BOOTSTRAP_TEMPLATE_PATH = options.bootstrapTemplatePath ?? 'packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml'; + + const github = GitHub.of(this.project); + if (!github) { + throw new Error('BootstrapTemplateProtection requires a GitHub project'); + } + + const workflow = github.addWorkflow('bootstrap-template-protection'); + + workflow.on({ + pullRequest: { + types: ['opened', 'synchronize', 'reopened', 'labeled', 'unlabeled'], + }, + }); + + workflow.addJob('check-bootstrap-template', { + name: 'Check Bootstrap Template Changes', + runsOn: ['ubuntu-latest'], + permissions: { + contents: gh.workflows.JobPermission.READ, + pullRequests: gh.workflows.JobPermission.WRITE, + }, + steps: [ + { + name: 'Checkout merge commit', + uses: 'actions/checkout@v4', + with: { + 'fetch-depth': 0, + 'ref': 'refs/pull/${{ github.event.pull_request.number }}/merge', + }, + }, + { + name: 'Checkout base branch', + run: 'git fetch origin ${{ github.event.pull_request.base.ref }}', + }, + { + name: 'Check if bootstrap template changed', + id: 'template-changed', + run: [ + '# Check if the bootstrap template differs between base and merge commit', + `if ! git diff --quiet --name-only origin/\${{ github.event.pull_request.base.ref }}..HEAD -- ${BOOTSTRAP_TEMPLATE_PATH}; then`, + ' echo "Bootstrap template modified - protection checks required"', + ' echo "changed=true" >> $GITHUB_OUTPUT', + 'else', + ' echo "✅ Bootstrap template not modified - no protection required"', + ' echo "changed=false" >> $GITHUB_OUTPUT', + 'fi', + ].join('\n'), + }, + { + name: 'Extract current and previous bootstrap versions', + if: 'steps.template-changed.outputs.changed == \'true\'', + id: 'version-check', + run: [ + '# Get current version from PR - look for CdkBootstrapVersion Value', + `CURRENT_VERSION=$(yq '.Resources.CdkBootstrapVersion.Properties.Value' ${BOOTSTRAP_TEMPLATE_PATH})`, + '', + '# Get previous version from base branch', + `git show origin/\${{ github.event.pull_request.base.ref }}:${BOOTSTRAP_TEMPLATE_PATH} > /tmp/base-template.yaml`, + 'PREVIOUS_VERSION=$(yq \'.Resources.CdkBootstrapVersion.Properties.Value\' /tmp/base-template.yaml)', + '', + 'echo "current-version=$CURRENT_VERSION" >> $GITHUB_OUTPUT', + 'echo "previous-version=$PREVIOUS_VERSION" >> $GITHUB_OUTPUT', + '', + 'if [ "$CURRENT_VERSION" -gt "$PREVIOUS_VERSION" ]; then', + ' echo "version-incremented=true" >> $GITHUB_OUTPUT', + 'else', + ' echo "version-incremented=false" >> $GITHUB_OUTPUT', + 'fi', + ].join('\n'), + }, + { + name: 'Check for security review and exemption labels', + if: 'steps.template-changed.outputs.changed == \'true\'', + id: 'label-check', + run: [ + `if [[ "\${{ contains(github.event.pull_request.labels.*.name, '${SECURITY_REVIEWED_LABEL}') }}" == "true" ]]; then`, + ' echo "has-security-label=true" >> $GITHUB_OUTPUT', + 'else', + ' echo "has-security-label=false" >> $GITHUB_OUTPUT', + 'fi', + '', + `if [[ "\${{ contains(github.event.pull_request.labels.*.name, '${VERSION_EXEMPT_LABEL}') }}" == "true" ]]; then`, + ' echo "has-version-exempt-label=true" >> $GITHUB_OUTPUT', + 'else', + ' echo "has-version-exempt-label=false" >> $GITHUB_OUTPUT', + 'fi', + ].join('\n'), + }, + { + name: 'Post comment', + if: 'steps.template-changed.outputs.changed == \'true\'', + uses: 'thollander/actions-comment-pull-request@v3', + with: { + 'comment-tag': 'bootstrap-template-protection', + 'mode': 'recreate', + 'message': [ + '## ⚠️ Bootstrap Template Protection', + '', + `This PR modifies the bootstrap template (\`${BOOTSTRAP_TEMPLATE_PATH}\`), which requires special protections.`, + '', + '${{ ((steps.version-check.outputs.version-incremented == \'true\' || steps.label-check.outputs.has-version-exempt-label == \'true\') && steps.label-check.outputs.has-security-label == \'true\') && \'**✅ All requirements met! This PR can proceed with normal review process.**\' || \'**❌ This PR cannot be merged until all requirements are met.**\' }}', + '', + '### Requirements', + '', + '**Version Increment**', + `\${{ (steps.version-check.outputs.version-incremented == \'true\' && format(\'✅ Version incremented from {0} to {1}\', steps.version-check.outputs.previous-version, steps.version-check.outputs.current-version)) || (steps.label-check.outputs.has-version-exempt-label == 'true' && format('✅ Version increment exempted (PR has \`{0}\` label)', '${VERSION_EXEMPT_LABEL}')) || '❌ Version increment required' }}`, + '${{ steps.version-check.outputs.version-incremented != \'true\' && steps.label-check.outputs.has-version-exempt-label != \'true\' && format(\' - Current version: `{0}`\', steps.version-check.outputs.current-version) || \'\' }}', + '${{ steps.version-check.outputs.version-incremented != \'true\' && steps.label-check.outputs.has-version-exempt-label != \'true\' && format(\' - Previous version: `{0}`\', steps.version-check.outputs.previous-version) || \'\' }}', + '${{ steps.version-check.outputs.version-incremented != \'true\' && steps.label-check.outputs.has-version-exempt-label != \'true\' && \' - Please increment the version in `CdkBootstrapVersion`\' || \'\' }}', + `\${{ steps.version-check.outputs.version-incremented != 'true' && steps.label-check.outputs.has-version-exempt-label != 'true' && format(' - Or add the \`{0}\` label if not needed', '${VERSION_EXEMPT_LABEL}') || '' }}`, + '', + '**Security Review**', + `\${{ (steps.label-check.outputs.has-security-label == 'true' && format('✅ Review completed (PR has \`{0}\` label)', '${SECURITY_REVIEWED_LABEL}')) || '❌ Review required' }}`, + '${{ steps.label-check.outputs.has-security-label != \'true\' && \' - A maintainer will conduct a security review\' || \'\' }}', + `\${{ steps.label-check.outputs.has-security-label != 'true' && format(' - Once reviewed, they will add the \`{0}\` label', '${SECURITY_REVIEWED_LABEL}') || '' }}`, + '', + '### Why these protections exist', + '- The bootstrap template contains critical infrastructure', + '- Changes can affect IAM roles, policies, and resource access across all CDK deployments', + '- Version increments ensure users are notified of updates', + '', + ].join('\n'), + }, + }, + { + name: 'Check requirements', + if: 'steps.template-changed.outputs.changed == \'true\'', + run: [ + '# Check version requirement (either incremented or exempted)', + 'VERSION_INCREMENTED="${{ steps.version-check.outputs.version-incremented }}"', + 'VERSION_EXEMPTED="${{ steps.label-check.outputs.has-version-exempt-label }}"', + 'SECURITY_REVIEWED="${{ steps.label-check.outputs.has-security-label }}"', + '', + '# Both requirements must be met', + 'if [[ "$VERSION_INCREMENTED" == "true" || "$VERSION_EXEMPTED" == "true" ]] && [[ "$SECURITY_REVIEWED" == "true" ]]; then', + ' echo "✅ All requirements met!"', + ' exit 0', + 'fi', + '', + '# Show what\'s missing', + 'echo "❌ Requirements not met:"', + 'if [[ "$VERSION_INCREMENTED" != "true" && "$VERSION_EXEMPTED" != "true" ]]; then', + ` echo " - Version must be incremented OR add '${VERSION_EXEMPT_LABEL}' label"`, + 'fi', + 'if [[ "$SECURITY_REVIEWED" != "true" ]]; then', + ` echo " - PR must have '${SECURITY_REVIEWED_LABEL}' label"`, + 'fi', + 'exit 1', + ].join('\n'), + }, + ], + }); + } +}