code-foundry 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/cloudflare-delivery.yml +354 -0
- package/.github/workflows/cloudflare-deploy.yml +90 -47
- package/CHANGELOG.md +7 -0
- package/docs/CONFIGURATION.md +17 -9
- package/docs/cloudflare-delivery.md +188 -0
- package/package.json +1 -1
- package/src/commands/cloudflare-delivery.mjs +351 -0
- package/src/lib/cloudflare-delivery.mjs +233 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
name: Code Foundry Cloudflare Delivery
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
inputs:
|
|
6
|
+
runtime-ref:
|
|
7
|
+
description: 'Immutable 40-character Code Foundry commit SHA containing this workflow and its helper.'
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
worker-name:
|
|
11
|
+
description: 'Exact effective Worker name, including any Wrangler environment suffix.'
|
|
12
|
+
required: true
|
|
13
|
+
type: string
|
|
14
|
+
artifact-path:
|
|
15
|
+
description: 'Build output subdirectory relative to working-directory; must cover uploaded code/assets.'
|
|
16
|
+
required: true
|
|
17
|
+
type: string
|
|
18
|
+
verify-command:
|
|
19
|
+
description: 'Required JSON argv array. Receives BASE_URL and FOUNDRY_DEPLOYMENT_PHASE.'
|
|
20
|
+
required: true
|
|
21
|
+
type: string
|
|
22
|
+
mode:
|
|
23
|
+
required: false
|
|
24
|
+
type: string
|
|
25
|
+
default: preview
|
|
26
|
+
working-directory:
|
|
27
|
+
required: false
|
|
28
|
+
type: string
|
|
29
|
+
default: '.'
|
|
30
|
+
install-working-directory:
|
|
31
|
+
required: false
|
|
32
|
+
type: string
|
|
33
|
+
default: '.'
|
|
34
|
+
build-script:
|
|
35
|
+
required: false
|
|
36
|
+
type: string
|
|
37
|
+
default: build
|
|
38
|
+
bun-version:
|
|
39
|
+
required: false
|
|
40
|
+
type: string
|
|
41
|
+
default: '1.4.0'
|
|
42
|
+
wrangler-version:
|
|
43
|
+
description: 'local uses the locked workspace installation; otherwise specify an exact version.'
|
|
44
|
+
required: false
|
|
45
|
+
type: string
|
|
46
|
+
default: local
|
|
47
|
+
wrangler-environment:
|
|
48
|
+
required: false
|
|
49
|
+
type: string
|
|
50
|
+
default: ''
|
|
51
|
+
wrangler-config:
|
|
52
|
+
required: false
|
|
53
|
+
type: string
|
|
54
|
+
default: ''
|
|
55
|
+
production-url:
|
|
56
|
+
required: false
|
|
57
|
+
type: string
|
|
58
|
+
default: ''
|
|
59
|
+
canary-verify-command:
|
|
60
|
+
description: 'Required JSON argv when canary-percentage is below 100; must prove the live request served this version.'
|
|
61
|
+
required: false
|
|
62
|
+
type: string
|
|
63
|
+
default: ''
|
|
64
|
+
smoke-path:
|
|
65
|
+
required: false
|
|
66
|
+
type: string
|
|
67
|
+
default: '/'
|
|
68
|
+
binding-policy-file:
|
|
69
|
+
description: 'Optional reviewed binding policy; undeclared stateful bindings fail closed.'
|
|
70
|
+
required: false
|
|
71
|
+
type: string
|
|
72
|
+
default: ''
|
|
73
|
+
canary-percentage:
|
|
74
|
+
description: '100 promotes directly; a lower value verifies a canary before final promotion.'
|
|
75
|
+
required: false
|
|
76
|
+
type: number
|
|
77
|
+
default: 100
|
|
78
|
+
auto-rollback:
|
|
79
|
+
description: 'Requires explicit rollbackSafe policy and no stateful bindings. Never restores resource data.'
|
|
80
|
+
required: false
|
|
81
|
+
type: boolean
|
|
82
|
+
default: false
|
|
83
|
+
secrets:
|
|
84
|
+
CLOUDFLARE_API_TOKEN:
|
|
85
|
+
required: true
|
|
86
|
+
CLOUDFLARE_ACCOUNT_ID:
|
|
87
|
+
required: true
|
|
88
|
+
outputs:
|
|
89
|
+
preview-url:
|
|
90
|
+
value: ${{ jobs.candidate.outputs.url }}
|
|
91
|
+
version-id:
|
|
92
|
+
value: ${{ jobs.candidate.outputs.version-id }}
|
|
93
|
+
source-sha:
|
|
94
|
+
value: ${{ jobs.candidate.outputs.source-sha }}
|
|
95
|
+
artifact-digest:
|
|
96
|
+
value: ${{ jobs.candidate.outputs.artifact-digest }}
|
|
97
|
+
cloudflare-deployment-id:
|
|
98
|
+
value: ${{ jobs.promote.outputs.cloudflare-deployment-id }}
|
|
99
|
+
github-deployment-id:
|
|
100
|
+
value: ${{ jobs.promote.outputs.github-deployment-id || jobs.candidate.outputs.github-deployment-id }}
|
|
101
|
+
|
|
102
|
+
permissions:
|
|
103
|
+
contents: read
|
|
104
|
+
deployments: write
|
|
105
|
+
|
|
106
|
+
# All production deliveries for an application serialize; old commits are checked
|
|
107
|
+
# again after environment approval. Never cancel a deployment mid-promotion.
|
|
108
|
+
concurrency:
|
|
109
|
+
group: code-foundry-cloudflare-${{ github.repository }}-${{ inputs.worker-name }}-${{ inputs.mode }}
|
|
110
|
+
cancel-in-progress: false
|
|
111
|
+
|
|
112
|
+
jobs:
|
|
113
|
+
candidate:
|
|
114
|
+
if: >-
|
|
115
|
+
vars.CI_BILLING_PAUSED != 'true' &&
|
|
116
|
+
github.event_name != 'pull_request_target' &&
|
|
117
|
+
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
|
118
|
+
runs-on: ubuntu-latest
|
|
119
|
+
timeout-minutes: 30
|
|
120
|
+
environment:
|
|
121
|
+
name: Preview
|
|
122
|
+
url: ${{ steps.upload.outputs.url }}
|
|
123
|
+
defaults:
|
|
124
|
+
run:
|
|
125
|
+
working-directory: ${{ inputs.working-directory }}
|
|
126
|
+
env:
|
|
127
|
+
FOUNDRY_MODE: ${{ inputs.mode }}
|
|
128
|
+
FOUNDRY_WORKER: ${{ inputs.worker-name }}
|
|
129
|
+
FOUNDRY_PHASE: candidate
|
|
130
|
+
FOUNDRY_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
|
131
|
+
ARTIFACT_PATH: ${{ inputs.artifact-path }}
|
|
132
|
+
VERIFY_COMMAND: ${{ inputs.verify-command }}
|
|
133
|
+
SMOKE_PATH: ${{ inputs.smoke-path }}
|
|
134
|
+
WRANGLER_VERSION: ${{ inputs.wrangler-version }}
|
|
135
|
+
WRANGLER_ENVIRONMENT: ${{ inputs.wrangler-environment }}
|
|
136
|
+
WRANGLER_CONFIG: ${{ inputs.wrangler-config }}
|
|
137
|
+
BINDING_POLICY_FILE: ${{ inputs.binding-policy-file }}
|
|
138
|
+
DEPLOYMENT_ENVIRONMENT: Preview
|
|
139
|
+
outputs:
|
|
140
|
+
url: ${{ steps.upload.outputs.url }}
|
|
141
|
+
version-id: ${{ steps.upload.outputs.version-id }}
|
|
142
|
+
artifact-digest: ${{ steps.upload.outputs.artifact-digest }}
|
|
143
|
+
source-sha: ${{ steps.upload.outputs.source-sha }}
|
|
144
|
+
verified: ${{ steps.verify.outputs.verified }}
|
|
145
|
+
github-deployment-id: ${{ steps.start.outputs.github-deployment-id }}
|
|
146
|
+
steps:
|
|
147
|
+
- name: Checkout source
|
|
148
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
149
|
+
with:
|
|
150
|
+
persist-credentials: false
|
|
151
|
+
- name: Validate inputs
|
|
152
|
+
env:
|
|
153
|
+
RUNTIME_REF: ${{ inputs.runtime-ref }}
|
|
154
|
+
CANARY_PERCENTAGE: ${{ inputs.canary-percentage }}
|
|
155
|
+
CANARY_VERIFY_COMMAND: ${{ inputs.canary-verify-command }}
|
|
156
|
+
PRODUCTION_URL: ${{ inputs.production-url }}
|
|
157
|
+
run: |
|
|
158
|
+
set -euo pipefail
|
|
159
|
+
[[ "$RUNTIME_REF" =~ ^[0-9a-fA-F]{40}$ ]]
|
|
160
|
+
[[ "$FOUNDRY_MODE" == preview || "$FOUNDRY_MODE" == production ]]
|
|
161
|
+
node --input-type=module <<'NODE'
|
|
162
|
+
const percentage = Number(process.env.CANARY_PERCENTAGE)
|
|
163
|
+
if (!Number.isFinite(percentage) || percentage < 0.01 || percentage > 100 || (percentage < 100 && percentage > 99.99)) process.exit(1)
|
|
164
|
+
if (process.env.FOUNDRY_MODE === 'production' && percentage < 100) {
|
|
165
|
+
let argv
|
|
166
|
+
try { argv = JSON.parse(process.env.CANARY_VERIFY_COMMAND ?? '') } catch { process.exit(1) }
|
|
167
|
+
if (!Array.isArray(argv) || !argv.length || argv.some((arg) => typeof arg !== 'string' || !arg.length || arg.includes('\0'))) process.exit(1)
|
|
168
|
+
}
|
|
169
|
+
NODE
|
|
170
|
+
if [[ "$FOUNDRY_MODE" == production ]]; then
|
|
171
|
+
[[ "$GITHUB_REF" == "refs/heads/$FOUNDRY_DEFAULT_BRANCH" ]]
|
|
172
|
+
[[ "$PRODUCTION_URL" == https://* ]]
|
|
173
|
+
fi
|
|
174
|
+
- name: Checkout delivery runtime
|
|
175
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
176
|
+
with:
|
|
177
|
+
repository: 0xPlayerOne/code-foundry
|
|
178
|
+
ref: ${{ inputs.runtime-ref }}
|
|
179
|
+
path: .github/.code-foundry-delivery
|
|
180
|
+
persist-credentials: false
|
|
181
|
+
sparse-checkout: src
|
|
182
|
+
- name: Prepare delivery state
|
|
183
|
+
run: |
|
|
184
|
+
echo "FOUNDRY_EXECUTOR=$GITHUB_WORKSPACE/.github/.code-foundry-delivery/src/commands/cloudflare-delivery.mjs" >> "$GITHUB_ENV"
|
|
185
|
+
echo "FOUNDRY_STATE_FILE=$RUNNER_TEMP/cloudflare-candidate.json" >> "$GITHUB_ENV"
|
|
186
|
+
- name: Record pending application deployment
|
|
187
|
+
id: start
|
|
188
|
+
env:
|
|
189
|
+
GH_TOKEN: ${{ github.token }}
|
|
190
|
+
run: node "$FOUNDRY_EXECUTOR" record-start
|
|
191
|
+
- name: Set up Bun
|
|
192
|
+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
193
|
+
with:
|
|
194
|
+
bun-version: ${{ inputs.bun-version }}
|
|
195
|
+
- name: Install locked dependencies
|
|
196
|
+
working-directory: ${{ inputs.install-working-directory }}
|
|
197
|
+
run: bun install --frozen-lockfile
|
|
198
|
+
- name: Build candidate
|
|
199
|
+
if: inputs.build-script != ''
|
|
200
|
+
env:
|
|
201
|
+
BUILD_SCRIPT: ${{ inputs.build-script }}
|
|
202
|
+
run: bun run "$BUILD_SCRIPT"
|
|
203
|
+
- name: Reject stale production source
|
|
204
|
+
if: inputs.mode == 'production'
|
|
205
|
+
env:
|
|
206
|
+
GH_TOKEN: ${{ github.token }}
|
|
207
|
+
run: node "$FOUNDRY_EXECUTOR" assert-current
|
|
208
|
+
- name: Upload candidate without changing traffic
|
|
209
|
+
id: upload
|
|
210
|
+
env:
|
|
211
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
212
|
+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
213
|
+
run: node "$FOUNDRY_EXECUTOR" upload
|
|
214
|
+
- name: Inspect actual candidate bindings
|
|
215
|
+
env:
|
|
216
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
217
|
+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
218
|
+
run: node "$FOUNDRY_EXECUTOR" inspect
|
|
219
|
+
- name: Verify candidate
|
|
220
|
+
id: verify
|
|
221
|
+
run: node "$FOUNDRY_EXECUTOR" verify
|
|
222
|
+
- name: Complete application deployment status
|
|
223
|
+
if: always() && steps.start.outputs.github-deployment-id != ''
|
|
224
|
+
env:
|
|
225
|
+
GH_TOKEN: ${{ github.token }}
|
|
226
|
+
DEPLOYMENT_STATUS: ${{ job.status }}
|
|
227
|
+
run: node "$FOUNDRY_EXECUTOR" record-finish
|
|
228
|
+
- name: Preserve candidate evidence
|
|
229
|
+
if: always() && steps.start.outputs.github-deployment-id != ''
|
|
230
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
231
|
+
with:
|
|
232
|
+
name: cloudflare-candidate-${{ inputs.worker-name }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
233
|
+
path: ${{ runner.temp }}/cloudflare-candidate.json
|
|
234
|
+
if-no-files-found: error
|
|
235
|
+
retention-days: 14
|
|
236
|
+
|
|
237
|
+
promote:
|
|
238
|
+
needs: candidate
|
|
239
|
+
if: >-
|
|
240
|
+
vars.CI_BILLING_PAUSED != 'true' &&
|
|
241
|
+
inputs.mode == 'production' && needs.candidate.outputs.verified == 'true'
|
|
242
|
+
runs-on: ubuntu-latest
|
|
243
|
+
timeout-minutes: 30
|
|
244
|
+
environment:
|
|
245
|
+
name: Production
|
|
246
|
+
url: ${{ inputs.production-url }}
|
|
247
|
+
defaults:
|
|
248
|
+
run:
|
|
249
|
+
working-directory: ${{ inputs.working-directory }}
|
|
250
|
+
env:
|
|
251
|
+
FOUNDRY_MODE: production
|
|
252
|
+
FOUNDRY_PHASE: production
|
|
253
|
+
FOUNDRY_WORKER: ${{ inputs.worker-name }}
|
|
254
|
+
FOUNDRY_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
|
255
|
+
VERIFY_COMMAND: ${{ inputs.verify-command }}
|
|
256
|
+
SMOKE_PATH: ${{ inputs.smoke-path }}
|
|
257
|
+
PRODUCTION_URL: ${{ inputs.production-url }}
|
|
258
|
+
BINDING_POLICY_FILE: ${{ inputs.binding-policy-file }}
|
|
259
|
+
DEPLOYMENT_ENVIRONMENT: Production
|
|
260
|
+
AUTO_ROLLBACK: ${{ inputs.auto-rollback }}
|
|
261
|
+
outputs:
|
|
262
|
+
cloudflare-deployment-id: ${{ steps.promote.outputs.cloudflare-deployment-id || steps.canary.outputs.cloudflare-deployment-id }}
|
|
263
|
+
github-deployment-id: ${{ steps.start.outputs.github-deployment-id }}
|
|
264
|
+
steps:
|
|
265
|
+
- name: Checkout verified source
|
|
266
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
267
|
+
with:
|
|
268
|
+
ref: ${{ needs.candidate.outputs.source-sha }}
|
|
269
|
+
persist-credentials: false
|
|
270
|
+
- name: Checkout delivery runtime
|
|
271
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
272
|
+
with:
|
|
273
|
+
repository: 0xPlayerOne/code-foundry
|
|
274
|
+
ref: ${{ inputs.runtime-ref }}
|
|
275
|
+
path: .github/.code-foundry-delivery
|
|
276
|
+
persist-credentials: false
|
|
277
|
+
sparse-checkout: src
|
|
278
|
+
- name: Prepare production state
|
|
279
|
+
run: |
|
|
280
|
+
echo "FOUNDRY_EXECUTOR=$GITHUB_WORKSPACE/.github/.code-foundry-delivery/src/commands/cloudflare-delivery.mjs" >> "$GITHUB_ENV"
|
|
281
|
+
echo "FOUNDRY_STATE_FILE=$RUNNER_TEMP/cloudflare-production.json" >> "$GITHUB_ENV"
|
|
282
|
+
- name: Bind promotion to verified candidate
|
|
283
|
+
env:
|
|
284
|
+
CANDIDATE_VERIFIED: ${{ needs.candidate.outputs.verified }}
|
|
285
|
+
CANDIDATE_SOURCE_SHA: ${{ needs.candidate.outputs.source-sha }}
|
|
286
|
+
CANDIDATE_VERSION_ID: ${{ needs.candidate.outputs.version-id }}
|
|
287
|
+
CANDIDATE_URL: ${{ needs.candidate.outputs.url }}
|
|
288
|
+
CANDIDATE_ARTIFACT_DIGEST: ${{ needs.candidate.outputs.artifact-digest }}
|
|
289
|
+
run: node "$FOUNDRY_EXECUTOR" init-production
|
|
290
|
+
- name: Record pending application deployment
|
|
291
|
+
id: start
|
|
292
|
+
env:
|
|
293
|
+
GH_TOKEN: ${{ github.token }}
|
|
294
|
+
run: node "$FOUNDRY_EXECUTOR" record-start
|
|
295
|
+
- name: Set up Bun for verification only
|
|
296
|
+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
297
|
+
with:
|
|
298
|
+
bun-version: ${{ inputs.bun-version }}
|
|
299
|
+
- name: Install verification dependencies without rebuilding
|
|
300
|
+
working-directory: ${{ inputs.install-working-directory }}
|
|
301
|
+
run: bun install --frozen-lockfile --ignore-scripts
|
|
302
|
+
- name: Reject stale source after approval
|
|
303
|
+
env:
|
|
304
|
+
GH_TOKEN: ${{ github.token }}
|
|
305
|
+
run: node "$FOUNDRY_EXECUTOR" assert-current
|
|
306
|
+
- name: Activate canary
|
|
307
|
+
id: canary
|
|
308
|
+
if: inputs.canary-percentage < 100
|
|
309
|
+
env:
|
|
310
|
+
DEPLOY_PERCENTAGE: ${{ inputs.canary-percentage }}
|
|
311
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
312
|
+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
313
|
+
run: node "$FOUNDRY_EXECUTOR" activate
|
|
314
|
+
- name: Verify canary
|
|
315
|
+
if: inputs.canary-percentage < 100
|
|
316
|
+
env:
|
|
317
|
+
FOUNDRY_PHASE: canary
|
|
318
|
+
VERIFY_COMMAND: ${{ inputs.canary-verify-command }}
|
|
319
|
+
run: node "$FOUNDRY_EXECUTOR" verify
|
|
320
|
+
- name: Recheck source before final promotion
|
|
321
|
+
env:
|
|
322
|
+
GH_TOKEN: ${{ github.token }}
|
|
323
|
+
run: node "$FOUNDRY_EXECUTOR" assert-current
|
|
324
|
+
- name: Promote the verified version to 100 percent
|
|
325
|
+
id: promote
|
|
326
|
+
env:
|
|
327
|
+
DEPLOY_PERCENTAGE: '100'
|
|
328
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
329
|
+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
330
|
+
run: node "$FOUNDRY_EXECUTOR" activate
|
|
331
|
+
- name: Verify production
|
|
332
|
+
run: node "$FOUNDRY_EXECUTOR" verify
|
|
333
|
+
- name: Optional stateless rollback
|
|
334
|
+
if: >-
|
|
335
|
+
failure() && inputs.auto-rollback &&
|
|
336
|
+
(steps.canary.outcome == 'success' || steps.promote.outcome == 'success')
|
|
337
|
+
env:
|
|
338
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
339
|
+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
340
|
+
run: node "$FOUNDRY_EXECUTOR" rollback
|
|
341
|
+
- name: Complete application deployment status
|
|
342
|
+
if: always() && steps.start.outputs.github-deployment-id != ''
|
|
343
|
+
env:
|
|
344
|
+
GH_TOKEN: ${{ github.token }}
|
|
345
|
+
DEPLOYMENT_STATUS: ${{ job.status }}
|
|
346
|
+
run: node "$FOUNDRY_EXECUTOR" record-finish
|
|
347
|
+
- name: Preserve promotion and recovery evidence
|
|
348
|
+
if: always() && steps.start.outputs.github-deployment-id != ''
|
|
349
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
350
|
+
with:
|
|
351
|
+
name: cloudflare-production-${{ inputs.worker-name }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
352
|
+
path: ${{ runner.temp }}/cloudflare-production.json
|
|
353
|
+
if-no-files-found: error
|
|
354
|
+
retention-days: 30
|
|
@@ -4,27 +4,23 @@ on:
|
|
|
4
4
|
workflow_call:
|
|
5
5
|
inputs:
|
|
6
6
|
mode:
|
|
7
|
-
description: '
|
|
7
|
+
description: 'Legacy direct deployment. Prefer cloudflare-delivery.yml for verified promotion.'
|
|
8
8
|
required: false
|
|
9
9
|
type: string
|
|
10
10
|
default: preview
|
|
11
11
|
working-directory:
|
|
12
|
-
description: 'Directory containing the wrangler configuration.'
|
|
13
12
|
required: false
|
|
14
13
|
type: string
|
|
15
14
|
default: '.'
|
|
16
15
|
install-working-directory:
|
|
17
|
-
description: 'Directory containing bun.lock. Used only when build-script is set.'
|
|
18
16
|
required: false
|
|
19
17
|
type: string
|
|
20
18
|
default: '.'
|
|
21
19
|
build-script:
|
|
22
|
-
description: 'Optional Bun package script to run before Wrangler deploys.'
|
|
23
20
|
required: false
|
|
24
21
|
type: string
|
|
25
22
|
default: ''
|
|
26
23
|
bun-version:
|
|
27
|
-
description: 'Bun version used for dependency installation and the optional build.'
|
|
28
24
|
required: false
|
|
29
25
|
type: string
|
|
30
26
|
default: '1.4.0'
|
|
@@ -33,11 +29,16 @@ on:
|
|
|
33
29
|
type: string
|
|
34
30
|
default: ubuntu-latest
|
|
35
31
|
wrangler-version:
|
|
32
|
+
description: 'local uses installed Wrangler; latest preserves legacy behavior; exact versions are recommended.'
|
|
36
33
|
required: false
|
|
37
34
|
type: string
|
|
38
|
-
default:
|
|
35
|
+
default: latest
|
|
39
36
|
environment:
|
|
40
|
-
|
|
37
|
+
required: false
|
|
38
|
+
type: string
|
|
39
|
+
default: ''
|
|
40
|
+
deployment-url:
|
|
41
|
+
description: 'Optional HTTPS production URL when Wrangler targets are route patterns.'
|
|
41
42
|
required: false
|
|
42
43
|
type: string
|
|
43
44
|
default: ''
|
|
@@ -46,41 +47,81 @@ on:
|
|
|
46
47
|
required: true
|
|
47
48
|
CLOUDFLARE_ACCOUNT_ID:
|
|
48
49
|
required: true
|
|
50
|
+
outputs:
|
|
51
|
+
url:
|
|
52
|
+
value: ${{ jobs.deploy.outputs.url }}
|
|
53
|
+
version-id:
|
|
54
|
+
value: ${{ jobs.deploy.outputs.version-id }}
|
|
55
|
+
github-deployment-id:
|
|
56
|
+
value: ${{ jobs.deploy.outputs.github-deployment-id }}
|
|
49
57
|
|
|
50
58
|
permissions:
|
|
51
59
|
contents: read
|
|
52
60
|
deployments: write
|
|
53
61
|
|
|
62
|
+
concurrency:
|
|
63
|
+
group: code-foundry-cloudflare-legacy-${{ github.repository }}-${{ inputs.working-directory }}-${{ inputs.environment || inputs.mode }}
|
|
64
|
+
cancel-in-progress: false
|
|
65
|
+
|
|
54
66
|
jobs:
|
|
55
67
|
deploy:
|
|
56
68
|
name: Cloudflare Deploy / ${{ inputs.mode }}
|
|
57
|
-
if:
|
|
69
|
+
if: >-
|
|
70
|
+
vars.CI_BILLING_PAUSED != 'true' &&
|
|
71
|
+
github.event_name != 'pull_request_target' &&
|
|
72
|
+
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
|
58
73
|
runs-on: ${{ inputs.runner }}
|
|
59
74
|
timeout-minutes: 20
|
|
75
|
+
environment:
|
|
76
|
+
name: ${{ inputs.environment || (inputs.mode == 'production' && 'Production' || 'Preview') }}
|
|
77
|
+
url: ${{ steps.deploy.outputs.url }}
|
|
78
|
+
outputs:
|
|
79
|
+
url: ${{ steps.deploy.outputs.url }}
|
|
80
|
+
version-id: ${{ steps.deploy.outputs.version-id }}
|
|
81
|
+
github-deployment-id: ${{ steps.start.outputs.id }}
|
|
60
82
|
steps:
|
|
61
83
|
- name: Checkout
|
|
62
84
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
63
85
|
with:
|
|
64
86
|
persist-credentials: false
|
|
65
|
-
|
|
87
|
+
- name: Validate deployment inputs
|
|
88
|
+
env:
|
|
89
|
+
MODE: ${{ inputs.mode }}
|
|
90
|
+
WRANGLER_VERSION: ${{ inputs.wrangler-version }}
|
|
91
|
+
run: |
|
|
92
|
+
set -euo pipefail
|
|
93
|
+
[[ "$MODE" == preview || "$MODE" == production ]]
|
|
94
|
+
[[ "$WRANGLER_VERSION" == local || "$WRANGLER_VERSION" == latest || "$WRANGLER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]
|
|
95
|
+
- name: Record pending deployment
|
|
96
|
+
id: start
|
|
97
|
+
env:
|
|
98
|
+
GH_TOKEN: ${{ github.token }}
|
|
99
|
+
MODE: ${{ inputs.mode }}
|
|
100
|
+
ENVIRONMENT: ${{ inputs.environment || (inputs.mode == 'production' && 'Production' || 'Preview') }}
|
|
101
|
+
run: |
|
|
102
|
+
set -euo pipefail
|
|
103
|
+
body=$(jq -n --arg ref "$GITHUB_SHA" --arg environment "$ENVIRONMENT" \
|
|
104
|
+
--argjson production "$([ "$MODE" = production ] && echo true || echo false)" \
|
|
105
|
+
'{ref: $ref, environment: $environment, auto_merge: false, production_environment: $production, required_contexts: []}')
|
|
106
|
+
id=$(gh api "repos/$GITHUB_REPOSITORY/deployments" --input - <<< "$body" --jq .id)
|
|
107
|
+
[[ "$id" =~ ^[0-9]+$ ]]
|
|
108
|
+
echo "id=$id" >> "$GITHUB_OUTPUT"
|
|
109
|
+
gh api -X POST "repos/$GITHUB_REPOSITORY/deployments/$id/statuses" -f state=in_progress -F auto_inactive=false --silent
|
|
66
110
|
- name: Set up Bun
|
|
67
111
|
if: inputs.build-script != ''
|
|
68
112
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
69
113
|
with:
|
|
70
114
|
bun-version: ${{ inputs.bun-version }}
|
|
71
|
-
|
|
72
115
|
- name: Install dependencies
|
|
73
116
|
if: inputs.build-script != ''
|
|
74
117
|
working-directory: ${{ inputs.install-working-directory }}
|
|
75
118
|
run: bun install --frozen-lockfile
|
|
76
|
-
|
|
77
119
|
- name: Build Worker
|
|
78
120
|
if: inputs.build-script != ''
|
|
79
121
|
working-directory: ${{ inputs.working-directory }}
|
|
80
122
|
env:
|
|
81
123
|
BUILD_SCRIPT: ${{ inputs.build-script }}
|
|
82
124
|
run: bun run "$BUILD_SCRIPT"
|
|
83
|
-
|
|
84
125
|
- name: Deploy to Cloudflare
|
|
85
126
|
id: deploy
|
|
86
127
|
working-directory: ${{ inputs.working-directory }}
|
|
@@ -90,52 +131,54 @@ jobs:
|
|
|
90
131
|
BUILD_SCRIPT: ${{ inputs.build-script }}
|
|
91
132
|
WRANGLER_VERSION: ${{ inputs.wrangler-version }}
|
|
92
133
|
MODE: ${{ inputs.mode }}
|
|
134
|
+
DEPLOYMENT_URL: ${{ inputs.deployment-url }}
|
|
93
135
|
run: |
|
|
94
136
|
set -euo pipefail
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
137
|
+
if [ "$WRANGLER_VERSION" = local ]; then
|
|
138
|
+
if [ -n "$BUILD_SCRIPT" ]; then
|
|
139
|
+
wrangler=(bunx --bun --no-install wrangler)
|
|
140
|
+
else
|
|
141
|
+
wrangler=(npx --no-install wrangler)
|
|
142
|
+
fi
|
|
143
|
+
elif [ -n "$BUILD_SCRIPT" ]; then
|
|
100
144
|
wrangler=(bunx --bun "wrangler@$WRANGLER_VERSION")
|
|
101
145
|
else
|
|
102
146
|
wrangler=(npx -y "wrangler@$WRANGLER_VERSION")
|
|
103
147
|
fi
|
|
148
|
+
export WRANGLER_OUTPUT_FILE_PATH="$RUNNER_TEMP/cloudflare-legacy.ndjson"
|
|
149
|
+
: > "$WRANGLER_OUTPUT_FILE_PATH"
|
|
104
150
|
"${wrangler[@]}" --version
|
|
105
|
-
if [ "$MODE" =
|
|
106
|
-
"${wrangler[@]}" deploy
|
|
107
|
-
elif [ "$MODE" = "preview" ]; then
|
|
108
|
-
"${wrangler[@]}" versions upload --message "preview ${GITHUB_SHA::7}" 2>&1 | tee deploy-output.txt
|
|
151
|
+
if [ "$MODE" = production ]; then
|
|
152
|
+
"${wrangler[@]}" deploy
|
|
109
153
|
else
|
|
110
|
-
|
|
111
|
-
exit 1
|
|
154
|
+
"${wrangler[@]}" versions upload --message "preview ${GITHUB_SHA::7}"
|
|
112
155
|
fi
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
156
|
+
node --input-type=module <<'NODE'
|
|
157
|
+
import { appendFileSync, readFileSync } from 'node:fs'
|
|
158
|
+
const records = readFileSync(process.env.WRANGLER_OUTPUT_FILE_PATH, 'utf8').split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line))
|
|
159
|
+
if (records.some(record => record.type === 'command-failed')) throw new Error('Wrangler command failed')
|
|
160
|
+
const type = process.env.MODE === 'production' ? 'deploy' : 'version-upload'
|
|
161
|
+
const matches = records.filter(record => record.type === type)
|
|
162
|
+
if (matches.length !== 1 || matches[0].version !== 1) throw new Error('Expected one supported structured deployment record')
|
|
163
|
+
const record = matches[0]
|
|
164
|
+
if (!/^[0-9a-f-]{36}$/i.test(record.version_id ?? '')) throw new Error('Missing Worker version ID')
|
|
165
|
+
const value = process.env.MODE === 'preview' ? record.preview_url : (process.env.DEPLOYMENT_URL || record.targets?.find(target => target.startsWith('https://')))
|
|
166
|
+
if (!value) throw new Error('Missing deployment URL; enable previews or configure deployment-url')
|
|
167
|
+
const url = new URL(value)
|
|
168
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.hash) throw new Error('Invalid deployment URL')
|
|
169
|
+
appendFileSync(process.env.GITHUB_OUTPUT, `url=${url.href}\nversion-id=${record.version_id}\n`)
|
|
170
|
+
NODE
|
|
171
|
+
- name: Complete deployment status
|
|
172
|
+
if: always() && steps.start.outputs.id != ''
|
|
118
173
|
env:
|
|
119
174
|
GH_TOKEN: ${{ github.token }}
|
|
120
|
-
|
|
121
|
-
ENVIRONMENT_OVERRIDE: ${{ inputs.environment }}
|
|
175
|
+
DEPLOYMENT_ID: ${{ steps.start.outputs.id }}
|
|
122
176
|
DEPLOY_URL: ${{ steps.deploy.outputs.url }}
|
|
177
|
+
JOB_STATUS: ${{ job.status }}
|
|
123
178
|
run: |
|
|
124
179
|
set -euo pipefail
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
fi
|
|
129
|
-
|
|
130
|
-
body=$(jq -n \
|
|
131
|
-
--arg ref "$GITHUB_SHA" \
|
|
132
|
-
--arg environment "$environment" \
|
|
133
|
-
--argjson production "$([ "$MODE" = "production" ] && echo true || echo false)" \
|
|
134
|
-
'{ref: $ref, environment: $environment, auto_merge: false, production_environment: $production, required_contexts: []}')
|
|
135
|
-
deployment=$(gh api "repos/$GITHUB_REPOSITORY/deployments" --input - <<< "$body" --jq .id)
|
|
136
|
-
status_args=(-f state=success -f description="Cloudflare $MODE deploy (${GITHUB_SHA::7})")
|
|
137
|
-
if [ -n "$DEPLOY_URL" ]; then
|
|
138
|
-
status_args+=(-f environment_url="$DEPLOY_URL" -f log_url="$DEPLOY_URL")
|
|
139
|
-
fi
|
|
140
|
-
gh api -X POST "repos/$GITHUB_REPOSITORY/deployments/$deployment/statuses" "${status_args[@]}" --jq .state
|
|
141
|
-
echo "Recorded $environment deployment for ${GITHUB_SHA::7}"
|
|
180
|
+
state=failure
|
|
181
|
+
if [ "$JOB_STATUS" = success ]; then state=success; fi
|
|
182
|
+
args=(-f "state=$state" -F auto_inactive=false)
|
|
183
|
+
if [ -n "$DEPLOY_URL" ]; then args+=(-f "environment_url=$DEPLOY_URL"); fi
|
|
184
|
+
gh api -X POST "repos/$GITHUB_REPOSITORY/deployments/$DEPLOYMENT_ID/statuses" "${args[@]}" --silent
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.12.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.11.0...v1.12.0) (2026-09-09)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **cloudflare:** verify candidates before protected version promotion ([#535](https://github.com/0xPlayerOne/code-foundry/issues/535)) ([5dd9afa](https://github.com/0xPlayerOne/code-foundry/commit/5dd9afa8418718823844875b0aedce926b887184))
|
|
9
|
+
|
|
3
10
|
## [1.11.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.10.1...v1.11.0) (2026-09-09)
|
|
4
11
|
|
|
5
12
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -179,15 +179,23 @@ shards. Do not split a single crate by arbitrary non-Rust directories: use
|
|
|
179
179
|
## Cloudflare Workers deployments
|
|
180
180
|
|
|
181
181
|
Repositories that deploy to Cloudflare Workers can opt into GitHub-native
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
`cloudflare-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
`
|
|
188
|
-
`CLOUDFLARE_ACCOUNT_ID` secrets in the consumer
|
|
189
|
-
|
|
190
|
-
|
|
182
|
+
verified delivery (fixed `Preview`/`Production` environments, candidate
|
|
183
|
+
verification, and version-identity promotion) by adding a caller for the
|
|
184
|
+
runtime's reusable `cloudflare-delivery.yml` workflow. Use the same immutable
|
|
185
|
+
40-character Code Foundry commit SHA for both the reusable workflow ref and
|
|
186
|
+
`runtime-ref`; configure required reviewers and branch restrictions on the
|
|
187
|
+
`Production` environment. The workflow requires the
|
|
188
|
+
`CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` secrets in the consumer
|
|
189
|
+
repository. See [Verified Cloudflare delivery](./cloudflare-delivery.md) for
|
|
190
|
+
binding policy, canary, rollback, and evidence requirements.
|
|
191
|
+
|
|
192
|
+
The legacy `cloudflare-deploy.yml` workflow remains available for direct
|
|
193
|
+
(unverified) deployments. It runs `wrangler versions upload` for previews and
|
|
194
|
+
`wrangler deploy` for production, records a GitHub deployment plus status, and
|
|
195
|
+
respects `CI_BILLING_PAUSED`. Its legacy-compatible Wrangler default is `latest`;
|
|
196
|
+
callers should prefer `local` or provide an exact `wrangler-version` for
|
|
197
|
+
reproducibility. Bun consumers may pass `build-script`, `install-working-directory`, and `bun-version`;
|
|
198
|
+
the runtime installs the frozen lockfile and builds the Worker before invoking
|
|
191
199
|
Wrangler. Bun-backed callers invoke Wrangler through `bunx` so OpenNext's
|
|
192
200
|
production delegation resolves the workspace-local `opennextjs-cloudflare`
|
|
193
201
|
binary; callers without `build-script` retain the npm/npx path.
|