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.
@@ -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: 'Deploy mode: preview (wrangler versions upload) or production (wrangler deploy).'
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: 'latest'
35
+ default: latest
39
36
  environment:
40
- description: 'GitHub deployment environment. Defaults to Preview/Production based on the mode.'
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: vars.CI_BILLING_PAUSED != 'true'
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
- # Bun callers install workspace dependencies with Bun. Keep that
96
- # package-manager context for Wrangler's OpenNext auto-delegation;
97
- # Wrangler otherwise selects npm/npx and cannot resolve the local
98
- # opennextjs-cloudflare binary from a Bun workspace.
99
- if [ -n "$BUILD_SCRIPT" ]; then
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" = "production" ]; then
106
- "${wrangler[@]}" deploy 2>&1 | tee deploy-output.txt
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
- echo "Unsupported mode: $MODE (use preview or production)" >&2
111
- exit 1
154
+ "${wrangler[@]}" versions upload --message "preview ${GITHUB_SHA::7}"
112
155
  fi
113
- # Prefer version preview URLs; fall back to the first workers.dev URL.
114
- url=$(grep -Eo 'https://[a-zA-Z0-9][a-zA-Z0-9.-]*\.workers\.dev' deploy-output.txt | tail -1 || true)
115
- echo "url=$url" >> "$GITHUB_OUTPUT"
116
-
117
- - name: Record GitHub deployment
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
- MODE: ${{ inputs.mode }}
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
- if [ "$MODE" != "production" ] && [ "$MODE" != "preview" ]; then
126
- echo "Unsupported mode: $MODE" >&2
127
- exit 1
128
- fi
129
- environment="${ENVIRONMENT_OVERRIDE:-$([ "$MODE" = "production" ] && echo Production || echo Preview)}"
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
 
@@ -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
- deployments (Preview/Production environments with deployment statuses, like
183
- Vercel's integration) by adding a small caller for the runtime's reusable
184
- `cloudflare-deploy.yml` workflow. The workflow runs `wrangler versions upload`
185
- for pull-request previews and `wrangler deploy` for production, records a
186
- GitHub deployment plus status with the workers.dev URL, and respects
187
- `CI_BILLING_PAUSED`. It requires the `CLOUDFLARE_API_TOKEN` and
188
- `CLOUDFLARE_ACCOUNT_ID` secrets in the consumer repository. Bun consumers may
189
- also pass `build-script`, `install-working-directory`, and `bun-version`; the
190
- runtime installs the frozen lockfile and builds the Worker before invoking
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.