-
Notifications
You must be signed in to change notification settings - Fork 0
176 lines (156 loc) · 6.99 KB
/
Copy pathe2e-cleanup.yml
File metadata and controls
176 lines (156 loc) · 6.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
name: e2e-cleanup
on:
workflow_dispatch:
inputs:
stack_suffix:
description: "10 hex chars from sandbox-<suffix> or the full sandbox-<suffix> name"
required: true
type: string
permissions:
id-token: write
contents: read
concurrency:
group: e2e-cleanup-${{ inputs.stack_suffix }}
cancel-in-progress: false
jobs:
cleanup:
name: cleanup AccountFoundation e2e stack
runs-on: ubuntu-latest
timeout-minutes: 15
# Destructive: assumes the sandbox OIDC role, drains S3, runs
# `pulumi destroy`, and removes the stack. Gated behind a protected
# GitHub Environment with required reviewers (maintainer must
# configure `e2e-cleanup` with required reviewers — see PR body).
# `if:` is defense-in-depth so a non-main ref can never trigger it
# even before the environment review is reached.
environment: e2e-cleanup
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
- name: Validate cleanup target and required configuration
id: env
env:
STACK_SUFFIX: ${{ inputs.stack_suffix }}
ACCOUNT_ID: ${{ secrets.AWS_SANDBOX_ACCOUNT_ID }}
ROLE_ARN: ${{ secrets.AWS_SANDBOX_OIDC_ROLE_ARN }}
REGION: ${{ vars.AWS_SANDBOX_REGION }}
PULUMI_BACKEND_URL: ${{ secrets.PULUMI_BACKEND_URL }}
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
run: |
: "${ACCOUNT_ID:?AWS_SANDBOX_ACCOUNT_ID secret not set}"
: "${ROLE_ARN:?AWS_SANDBOX_OIDC_ROLE_ARN secret not set}"
: "${REGION:?AWS_SANDBOX_REGION variable not set}"
node <<'NODE'
const fs = require("node:fs");
const out = process.env.GITHUB_OUTPUT;
const stackSuffix = (process.env.STACK_SUFFIX || "").trim();
const backendUrl = (process.env.PULUMI_BACKEND_URL || "").trim();
const token = (process.env.PULUMI_ACCESS_TOKEN || "").trim();
const region = (process.env.REGION || "").trim();
const accountId = (process.env.ACCOUNT_ID || "").trim();
const roleArn = (process.env.ROLE_ARN || "").trim();
const match = /^(?:sandbox-)?([a-f0-9]{10})$/.exec(stackSuffix);
function write(name, value) {
fs.appendFileSync(out, `${name}=${value}\n`);
}
for (const value of [accountId, roleArn, backendUrl, token]) {
if (value) {
console.log(`::add-mask::${value}`);
}
}
if (!match) {
console.error("stack_suffix must be 10 lowercase hex chars or sandbox-<10 lowercase hex chars>.");
process.exit(1);
}
if (backendUrl && token) {
console.error("Set exactly one Pulumi backend: PULUMI_BACKEND_URL or PULUMI_ACCESS_TOKEN.");
process.exit(1);
}
if (!backendUrl && !token) {
console.error("Cleanup requires a real Pulumi backend.");
process.exit(1);
}
if (backendUrl) {
let parsed;
try {
parsed = new URL(backendUrl);
} catch (err) {
console.error(`PULUMI_BACKEND_URL is not a valid URL: ${err.message}`);
process.exit(1);
}
if (parsed.protocol !== "s3:") {
console.error("PULUMI_BACKEND_URL must use s3:// for this public-repo workflow.");
process.exit(1);
}
const bucket = parsed.hostname;
if (!bucket.startsWith("hulumi-") || !bucket.endsWith(accountId)) {
console.error("State bucket name must start with 'hulumi-' and end with the sandbox account ID.");
process.exit(1);
}
console.log(`::add-mask::${bucket}`);
}
write("suffix", match[1]);
write("region", region);
NODE
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
with:
version: 9.12.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6
with:
role-to-assume: ${{ secrets.AWS_SANDBOX_OIDC_ROLE_ARN }}
aws-region: ${{ steps.env.outputs.region }}
role-session-name: hulumi-e2e-cleanup
- name: Verify state bucket is owned by the sandbox account
env:
PULUMI_BACKEND_URL: ${{ secrets.PULUMI_BACKEND_URL }}
AWS_SANDBOX_ACCOUNT_ID: ${{ secrets.AWS_SANDBOX_ACCOUNT_ID }}
run: |
# The name-shape check in the validation step (hulumi- prefix +
# account-id suffix) does not prove ownership. Before this
# workflow drains S3 and runs `pulumi destroy`, resolve the
# canonical bucket owner and fail closed unless it is the
# sandbox account. Pulumi-Cloud-token mode has no bucket — skip.
: "${AWS_SANDBOX_ACCOUNT_ID:?AWS_SANDBOX_ACCOUNT_ID secret not set}"
if [ -z "${PULUMI_BACKEND_URL:-}" ]; then
echo "No S3 backend URL — Pulumi Cloud token mode, no bucket to verify."
exit 0
fi
BUCKET="$(node -e 'process.stdout.write(new URL(process.env.PULUMI_BACKEND_URL).hostname)')"
echo "::add-mask::$BUCKET"
if [ -z "$BUCKET" ]; then
echo "::error::Could not resolve state bucket from PULUMI_BACKEND_URL."
exit 1
fi
if aws s3api head-bucket \
--bucket "$BUCKET" \
--expected-bucket-owner "$AWS_SANDBOX_ACCOUNT_ID" >/dev/null 2>&1; then
OWNER_ID="$(aws s3api get-bucket-acl \
--bucket "$BUCKET" \
--expected-bucket-owner "$AWS_SANDBOX_ACCOUNT_ID" \
--query 'Owner.ID' --output text)"
CANONICAL_ID="$(aws s3api list-buckets --query 'Owner.ID' --output text)"
if [ -z "$OWNER_ID" ] || [ "$OWNER_ID" != "$CANONICAL_ID" ]; then
echo "::error::State bucket owner does not match the sandbox account canonical user. Refusing to destroy."
exit 1
fi
echo "State bucket ownership verified against the sandbox account."
else
echo "::error::State bucket is missing or not owned by the sandbox account. Refusing to destroy."
exit 1
fi
- name: Build cleanup dependencies
run: pnpm --filter @hulumi/drift build
- name: Cleanup selected e2e stack
env:
HULUMI_E2E_STACK_SUFFIX: ${{ steps.env.outputs.suffix }}
AWS_REGION: ${{ steps.env.outputs.region }}
AWS_DEFAULT_REGION: ${{ steps.env.outputs.region }}
PULUMI_BACKEND_URL: ${{ secrets.PULUMI_BACKEND_URL }}
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
run: node scripts/cleanup-e2e-stack.mjs