|
| 1 | +import type { IConstruct } from 'constructs'; |
| 2 | +import { Component, github as gh } from 'projen'; |
| 3 | +import { GitHub } from 'projen/lib/github'; |
| 4 | + |
| 5 | +export interface BootstrapTemplateProtectionOptions { |
| 6 | + readonly bootstrapTemplatePath?: string; |
| 7 | +} |
| 8 | + |
| 9 | +export class BootstrapTemplateProtection extends Component { |
| 10 | + constructor(scope: IConstruct, options: BootstrapTemplateProtectionOptions = {}) { |
| 11 | + super(scope); |
| 12 | + |
| 13 | + const SECURITY_REVIEWED_LABEL = 'pr/security-reviewed'; |
| 14 | + const VERSION_EXEMPT_LABEL = 'pr/exempt-bootstrap-version'; |
| 15 | + const BOOTSTRAP_TEMPLATE_PATH = options.bootstrapTemplatePath ?? 'packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml'; |
| 16 | + |
| 17 | + const github = GitHub.of(this.project); |
| 18 | + if (!github) { |
| 19 | + throw new Error('BootstrapTemplateProtection requires a GitHub project'); |
| 20 | + } |
| 21 | + |
| 22 | + const workflow = github.addWorkflow('bootstrap-template-protection'); |
| 23 | + |
| 24 | + workflow.on({ |
| 25 | + pullRequest: { |
| 26 | + types: ['opened', 'synchronize', 'reopened', 'labeled', 'unlabeled'], |
| 27 | + }, |
| 28 | + }); |
| 29 | + |
| 30 | + workflow.addJob('check-bootstrap-template', { |
| 31 | + name: 'Check Bootstrap Template Changes', |
| 32 | + runsOn: ['ubuntu-latest'], |
| 33 | + permissions: { |
| 34 | + contents: gh.workflows.JobPermission.READ, |
| 35 | + pullRequests: gh.workflows.JobPermission.WRITE, |
| 36 | + }, |
| 37 | + steps: [ |
| 38 | + { |
| 39 | + name: 'Checkout merge commit', |
| 40 | + uses: 'actions/checkout@v4', |
| 41 | + with: { |
| 42 | + 'fetch-depth': 0, |
| 43 | + 'ref': 'refs/pull/${{ github.event.pull_request.number }}/merge', |
| 44 | + }, |
| 45 | + }, |
| 46 | + { |
| 47 | + name: 'Checkout base branch', |
| 48 | + run: 'git fetch origin ${{ github.event.pull_request.base.ref }}', |
| 49 | + }, |
| 50 | + { |
| 51 | + name: 'Check if bootstrap template changed', |
| 52 | + id: 'template-changed', |
| 53 | + run: [ |
| 54 | + '# Check if the bootstrap template differs between base and merge commit', |
| 55 | + `if ! git diff --quiet --name-only origin/\${{ github.event.pull_request.base.ref }}..HEAD -- ${BOOTSTRAP_TEMPLATE_PATH}; then`, |
| 56 | + ' echo "Bootstrap template modified - protection checks required"', |
| 57 | + ' echo "changed=true" >> $GITHUB_OUTPUT', |
| 58 | + 'else', |
| 59 | + ' echo "✅ Bootstrap template not modified - no protection required"', |
| 60 | + ' echo "changed=false" >> $GITHUB_OUTPUT', |
| 61 | + 'fi', |
| 62 | + ].join('\n'), |
| 63 | + }, |
| 64 | + { |
| 65 | + name: 'Extract current and previous bootstrap versions', |
| 66 | + if: 'steps.template-changed.outputs.changed == \'true\'', |
| 67 | + id: 'version-check', |
| 68 | + run: [ |
| 69 | + '# Get current version from PR - look for CdkBootstrapVersion Value', |
| 70 | + `CURRENT_VERSION=$(yq '.Resources.CdkBootstrapVersion.Properties.Value' ${BOOTSTRAP_TEMPLATE_PATH})`, |
| 71 | + '', |
| 72 | + '# Get previous version from base branch', |
| 73 | + `git show origin/\${{ github.event.pull_request.base.ref }}:${BOOTSTRAP_TEMPLATE_PATH} > /tmp/base-template.yaml`, |
| 74 | + 'PREVIOUS_VERSION=$(yq \'.Resources.CdkBootstrapVersion.Properties.Value\' /tmp/base-template.yaml)', |
| 75 | + '', |
| 76 | + 'echo "current-version=$CURRENT_VERSION" >> $GITHUB_OUTPUT', |
| 77 | + 'echo "previous-version=$PREVIOUS_VERSION" >> $GITHUB_OUTPUT', |
| 78 | + '', |
| 79 | + 'if [ "$CURRENT_VERSION" -gt "$PREVIOUS_VERSION" ]; then', |
| 80 | + ' echo "version-incremented=true" >> $GITHUB_OUTPUT', |
| 81 | + 'else', |
| 82 | + ' echo "version-incremented=false" >> $GITHUB_OUTPUT', |
| 83 | + 'fi', |
| 84 | + ].join('\n'), |
| 85 | + }, |
| 86 | + { |
| 87 | + name: 'Check for security review and exemption labels', |
| 88 | + if: 'steps.template-changed.outputs.changed == \'true\'', |
| 89 | + id: 'label-check', |
| 90 | + run: [ |
| 91 | + `if [[ "\${{ contains(github.event.pull_request.labels.*.name, '${SECURITY_REVIEWED_LABEL}') }}" == "true" ]]; then`, |
| 92 | + ' echo "has-security-label=true" >> $GITHUB_OUTPUT', |
| 93 | + 'else', |
| 94 | + ' echo "has-security-label=false" >> $GITHUB_OUTPUT', |
| 95 | + 'fi', |
| 96 | + '', |
| 97 | + `if [[ "\${{ contains(github.event.pull_request.labels.*.name, '${VERSION_EXEMPT_LABEL}') }}" == "true" ]]; then`, |
| 98 | + ' echo "has-version-exempt-label=true" >> $GITHUB_OUTPUT', |
| 99 | + 'else', |
| 100 | + ' echo "has-version-exempt-label=false" >> $GITHUB_OUTPUT', |
| 101 | + 'fi', |
| 102 | + ].join('\n'), |
| 103 | + }, |
| 104 | + { |
| 105 | + name: 'Post comment', |
| 106 | + if: 'steps.template-changed.outputs.changed == \'true\'', |
| 107 | + uses: 'thollander/actions-comment-pull-request@v3', |
| 108 | + with: { |
| 109 | + 'comment-tag': 'bootstrap-template-protection', |
| 110 | + 'mode': 'recreate', |
| 111 | + 'message': [ |
| 112 | + '## ⚠️ Bootstrap Template Protection', |
| 113 | + '', |
| 114 | + `This PR modifies the bootstrap template (\`${BOOTSTRAP_TEMPLATE_PATH}\`), which requires special protections.`, |
| 115 | + '', |
| 116 | + '### Requirements:', |
| 117 | + '', |
| 118 | + `\${{ (steps.version-check.outputs.version-incremented == 'true' || steps.label-check.outputs.has-version-exempt-label == 'true') && (steps.version-check.outputs.version-incremented == 'true' && format('✅ **Version Incremented**: CdkBootstrapVersion updated 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' && '✅ **Version Increment Exempted**: PR has the \`${VERSION_EXEMPT_LABEL}\` label') || format('❌ **Version Increment Required**: The \`CdkBootstrapVersion\` must be incremented when the template changes.\\n - Current version: {0}\\n - Previous version: {1}\\n - Please increment the version in the \`CdkBootstrapVersion\` parameter value.\\n - Or add the \`${VERSION_EXEMPT_LABEL}\` label if version increment is not needed.', steps.version-check.outputs.current-version, steps.version-check.outputs.previous-version) }}`, |
| 119 | + '', |
| 120 | + `\${{ steps.label-check.outputs.has-security-label == 'true' && '✅ **Security Review Complete**: PR has the required \`${SECURITY_REVIEWED_LABEL}\` label' || '❌ **Security Review Required**: This PR must have the \`${SECURITY_REVIEWED_LABEL}\` label.\\n - A maintainer will conduct a security review\\n - Once reviewed, they will add the \`${SECURITY_REVIEWED_LABEL}\` label' }}`, |
| 121 | + '', |
| 122 | + '### Why these protections exist:', |
| 123 | + '- The bootstrap template contains critical infrastructure', |
| 124 | + '- Changes can affect IAM roles, policies, and resource access across all CDK deployments', |
| 125 | + '- Version increments ensure proper deployment coordination', |
| 126 | + '', |
| 127 | + '${{ ((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.**\' }}', |
| 128 | + ].join('\n'), |
| 129 | + }, |
| 130 | + }, |
| 131 | + { |
| 132 | + name: 'Check requirements', |
| 133 | + if: 'steps.template-changed.outputs.changed == \'true\'', |
| 134 | + run: [ |
| 135 | + '# Check version requirement (either incremented or exempted)', |
| 136 | + 'VERSION_INCREMENTED="${{ steps.version-check.outputs.version-incremented }}"', |
| 137 | + 'VERSION_EXEMPTED="${{ steps.label-check.outputs.has-version-exempt-label }}"', |
| 138 | + 'SECURITY_REVIEWED="${{ steps.label-check.outputs.has-security-label }}"', |
| 139 | + '', |
| 140 | + '# Both requirements must be met', |
| 141 | + 'if [[ "$VERSION_INCREMENTED" == "true" || "$VERSION_EXEMPTED" == "true" ]] && [[ "$SECURITY_REVIEWED" == "true" ]]; then', |
| 142 | + ' echo "✅ All requirements met!"', |
| 143 | + ' exit 0', |
| 144 | + 'fi', |
| 145 | + '', |
| 146 | + '# Show what\'s missing', |
| 147 | + 'echo "❌ Requirements not met:"', |
| 148 | + 'if [[ "$VERSION_INCREMENTED" != "true" && "$VERSION_EXEMPTED" != "true" ]]; then', |
| 149 | + ` echo " - Version must be incremented OR add '${VERSION_EXEMPT_LABEL}' label"`, |
| 150 | + 'fi', |
| 151 | + 'if [[ "$SECURITY_REVIEWED" != "true" ]]; then', |
| 152 | + ` echo " - PR must have '${SECURITY_REVIEWED_LABEL}' label"`, |
| 153 | + 'fi', |
| 154 | + 'exit 1', |
| 155 | + ].join('\n'), |
| 156 | + }, |
| 157 | + ], |
| 158 | + }); |
| 159 | + } |
| 160 | +} |
0 commit comments