code-foundry 1.10.1 → 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/code-foundry.yml +4 -0
- package/.github/workflows/ci.yml +4 -0
- package/.github/workflows/cloudflare-delivery.yml +354 -0
- package/.github/workflows/cloudflare-deploy.yml +90 -47
- package/.github/workflows/codeql.yml +2 -0
- package/.github/workflows/security.yml +2 -0
- package/.github/workflows/test.yml +5 -0
- package/.github/workflows/validation-no-codeql.yml +1 -0
- package/.github/workflows/validation.yml +1 -0
- package/.github/workflows/validation_self-ci.yml +1 -0
- package/CHANGELOG.md +14 -0
- package/docs/CONFIGURATION.md +23 -10
- package/docs/README.md +1 -0
- package/docs/cloudflare-delivery.md +188 -0
- package/docs/required-capabilities.md +82 -0
- package/package.json +1 -1
- package/src/commands/cloudflare-delivery.mjs +351 -0
- package/src/commands/sync.mjs +4 -0
- package/src/lib/cloudflare-delivery.mjs +233 -0
- package/src/lib/task-policy.mjs +290 -0
- package/src/runtime-core.mjs +794 -0
- package/src/runtime.mjs +179 -764
package/.github/code-foundry.yml
CHANGED
|
@@ -23,7 +23,11 @@ unit_runner: ubuntu-slim
|
|
|
23
23
|
performance_runner: ubuntu-latest
|
|
24
24
|
cache_packages: auto
|
|
25
25
|
cache_build: auto
|
|
26
|
+
required_capabilities:
|
|
27
|
+
coverage_enforcement: auto
|
|
26
28
|
coverage_minimum: 80
|
|
29
|
+
coverage_metrics: lines
|
|
30
|
+
coverage_report:
|
|
27
31
|
turbo_remote: auto
|
|
28
32
|
ci_runner: ubuntu-latest
|
|
29
33
|
test_runner: ubuntu-latest
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -63,6 +63,7 @@ jobs:
|
|
|
63
63
|
sparse-checkout: |
|
|
64
64
|
.github/actions
|
|
65
65
|
src/lib
|
|
66
|
+
src/runtime-core.mjs
|
|
66
67
|
src/runtime.mjs
|
|
67
68
|
- name: Install runtime
|
|
68
69
|
run: |
|
|
@@ -107,6 +108,7 @@ jobs:
|
|
|
107
108
|
sparse-checkout: |
|
|
108
109
|
.github/actions
|
|
109
110
|
src/lib
|
|
111
|
+
src/runtime-core.mjs
|
|
110
112
|
src/runtime.mjs
|
|
111
113
|
- name: Install runtime
|
|
112
114
|
run: |
|
|
@@ -146,6 +148,7 @@ jobs:
|
|
|
146
148
|
sparse-checkout: |
|
|
147
149
|
.github/actions
|
|
148
150
|
src/lib
|
|
151
|
+
src/runtime-core.mjs
|
|
149
152
|
src/runtime.mjs
|
|
150
153
|
- name: Install runtime
|
|
151
154
|
run: |
|
|
@@ -190,6 +193,7 @@ jobs:
|
|
|
190
193
|
sparse-checkout: |
|
|
191
194
|
.github/actions
|
|
192
195
|
src/lib
|
|
196
|
+
src/runtime-core.mjs
|
|
193
197
|
src/runtime.mjs
|
|
194
198
|
- name: Install runtime
|
|
195
199
|
run: |
|
|
@@ -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
|
|
@@ -118,6 +118,7 @@ jobs:
|
|
|
118
118
|
sparse-checkout: |
|
|
119
119
|
.github/actions
|
|
120
120
|
src/lib
|
|
121
|
+
src/runtime-core.mjs
|
|
121
122
|
src/runtime.mjs
|
|
122
123
|
- name: Install runtime
|
|
123
124
|
run: |
|
|
@@ -210,6 +211,7 @@ jobs:
|
|
|
210
211
|
sparse-checkout: |
|
|
211
212
|
.github/actions
|
|
212
213
|
src/lib
|
|
214
|
+
src/runtime-core.mjs
|
|
213
215
|
src/runtime.mjs
|
|
214
216
|
- name: Install runtime
|
|
215
217
|
if: ${{ matrix.entry.changed == true }}
|
|
@@ -88,6 +88,7 @@ jobs:
|
|
|
88
88
|
sparse-checkout: |
|
|
89
89
|
.github/actions
|
|
90
90
|
src/lib
|
|
91
|
+
src/runtime-core.mjs
|
|
91
92
|
src/runtime.mjs
|
|
92
93
|
- name: Install runtime
|
|
93
94
|
run: |
|
|
@@ -210,6 +211,7 @@ jobs:
|
|
|
210
211
|
sparse-checkout: |
|
|
211
212
|
.github/actions
|
|
212
213
|
src/lib
|
|
214
|
+
src/runtime-core.mjs
|
|
213
215
|
src/runtime.mjs
|
|
214
216
|
- name: Install runtime
|
|
215
217
|
run: |
|