Skip to content

Commit 8a0f705

Browse files
committed
chore(ci): protect bootstrap template
1 parent 80d4d15 commit 8a0f705

File tree

6 files changed

+282
-0
lines changed

6 files changed

+282
-0
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/bootstrap-template-protection.yml

Lines changed: 117 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.gitignore

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.projen/files.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.projenrc.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as pj from 'projen';
55
import { Stability } from 'projen/lib/cdk';
66
import type { Job } from 'projen/lib/github/workflows-model';
77
import { AdcPublishing } from './projenrc/adc-publishing';
8+
import { BootstrapTemplateProtection } from './projenrc/bootstrap-template-protection';
89
import { BundleCli } from './projenrc/bundle';
910
import { CdkCliIntegTestsWorkflow } from './projenrc/cdk-cli-integ-tests';
1011
import { CodeCovWorkflow } from './projenrc/codecov';
@@ -290,6 +291,7 @@ const repoProject = new yarn.Monorepo({
290291

291292
new AdcPublishing(repoProject);
292293
new RecordPublishingTimestamp(repoProject);
294+
new BootstrapTemplateProtection(repoProject);
293295

294296
// Eslint for projen config
295297
// @ts-ignore
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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

Comments
 (0)