Skip to content

Commit 3f195bc

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

File tree

6 files changed

+299
-0
lines changed

6 files changed

+299
-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: 125 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: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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+
'${{ ((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.**\' }}',
117+
'',
118+
'### Requirements',
119+
'',
120+
'**Version Increment**',
121+
`\${{ (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' }}`,
122+
'${{ 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) || \'\' }}',
123+
'${{ 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) || \'\' }}',
124+
'${{ steps.version-check.outputs.version-incremented != \'true\' && steps.label-check.outputs.has-version-exempt-label != \'true\' && \' - Please increment the version in `CdkBootstrapVersion`\' || \'\' }}',
125+
`\${{ 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}') || '' }}`,
126+
'',
127+
'**Security Review**',
128+
`\${{ (steps.label-check.outputs.has-security-label == 'true' && format('✅ Review completed (PR has \`{0}\` label)', '${SECURITY_REVIEWED_LABEL}')) || '❌ Review required' }}`,
129+
'${{ steps.label-check.outputs.has-security-label != \'true\' && \' - A maintainer will conduct a security review\' || \'\' }}',
130+
`\${{ steps.label-check.outputs.has-security-label != 'true' && format(' - Once reviewed, they will add the \`{0}\` label', '${SECURITY_REVIEWED_LABEL}') || '' }}`,
131+
'',
132+
'### Why these protections exist',
133+
'- The bootstrap template contains critical infrastructure',
134+
'- Changes can affect IAM roles, policies, and resource access across all CDK deployments',
135+
'- Version increments ensure users are notified of updates',
136+
'',
137+
].join('\n'),
138+
},
139+
},
140+
{
141+
name: 'Check requirements',
142+
if: 'steps.template-changed.outputs.changed == \'true\'',
143+
run: [
144+
'# Check version requirement (either incremented or exempted)',
145+
'VERSION_INCREMENTED="${{ steps.version-check.outputs.version-incremented }}"',
146+
'VERSION_EXEMPTED="${{ steps.label-check.outputs.has-version-exempt-label }}"',
147+
'SECURITY_REVIEWED="${{ steps.label-check.outputs.has-security-label }}"',
148+
'',
149+
'# Both requirements must be met',
150+
'if [[ "$VERSION_INCREMENTED" == "true" || "$VERSION_EXEMPTED" == "true" ]] && [[ "$SECURITY_REVIEWED" == "true" ]]; then',
151+
' echo "✅ All requirements met!"',
152+
' exit 0',
153+
'fi',
154+
'',
155+
'# Show what\'s missing',
156+
'echo "❌ Requirements not met:"',
157+
'if [[ "$VERSION_INCREMENTED" != "true" && "$VERSION_EXEMPTED" != "true" ]]; then',
158+
` echo " - Version must be incremented OR add '${VERSION_EXEMPT_LABEL}' label"`,
159+
'fi',
160+
'if [[ "$SECURITY_REVIEWED" != "true" ]]; then',
161+
` echo " - PR must have '${SECURITY_REVIEWED_LABEL}' label"`,
162+
'fi',
163+
'exit 1',
164+
].join('\n'),
165+
},
166+
],
167+
});
168+
}
169+
}

0 commit comments

Comments
 (0)