code-foundry 1.11.0 → 1.13.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/CONTRIBUTING.md +7 -6
- package/.github/workflows/cloudflare-delivery.yml +354 -0
- package/.github/workflows/cloudflare-deploy.yml +90 -47
- package/.github/workflows/draft-enforcement_self-ci.yml +3 -4
- package/.github/workflows/opencode-security_self-ci.yml +2 -1
- package/.github/workflows/validation_self-ci.yml +6 -4
- package/AGENTS.md +6 -5
- package/CHANGELOG.md +14 -0
- package/README.md +7 -6
- package/docs/CONFIGURATION.md +17 -9
- package/docs/PERFORMANCE.md +3 -3
- package/docs/README.md +1 -0
- package/docs/WORKFLOWS.md +14 -14
- package/docs/cloudflare-delivery.md +188 -0
- package/docs/fleet-rollouts.md +139 -0
- package/package.json +1 -1
- package/src/commands/cloudflare-delivery.mjs +351 -0
- package/src/commands/doctor.mjs +8 -3
- package/src/commands/fleet.mjs +355 -42
- package/src/commands/sync.mjs +11 -6
- package/src/lib/cloudflare-delivery.mjs +233 -0
- package/src/lib/fleet-manifest.mjs +654 -0
package/.github/CONTRIBUTING.md
CHANGED
|
@@ -41,13 +41,14 @@ This repository uses the `direct` workflow. Topic pull requests target `main`.
|
|
|
41
41
|
- Open every ordinary pull request as a draft. Use `gh pr create --draft` or
|
|
42
42
|
set `draft: true` in the GitHub API; never create a ready ordinary pull
|
|
43
43
|
request as a shortcut.
|
|
44
|
-
- Keep ordinary pull requests in draft while
|
|
45
|
-
Draft Guard
|
|
46
|
-
|
|
44
|
+
- Keep ordinary pull requests in draft while preparing them. The generated
|
|
45
|
+
Draft Guard converts ready ordinary pull requests to draft when they are
|
|
46
|
+
opened or reopened, and runner-heavy validation starts only after an
|
|
47
47
|
explicit `ready_for_review` transition.
|
|
48
48
|
- Run local validation and finish review preparation before marking an ordinary
|
|
49
|
-
pull request ready.
|
|
50
|
-
the current head
|
|
49
|
+
pull request ready. Ready pull requests stay ready when new commits arrive,
|
|
50
|
+
and validation reruns for the current head; draft updates allocate no
|
|
51
|
+
validation runner until the pull request is ready.
|
|
51
52
|
- Release Please version pull requests are managed by the Code Foundry release
|
|
52
53
|
workflow; do not manually change their draft state unless the workflow asks.
|
|
53
54
|
|
|
@@ -183,7 +184,7 @@ Keep pull requests focused and reviewable. Include screenshots or recordings for
|
|
|
183
184
|
| Push to a working branch | Draft PR workflow |
|
|
184
185
|
| Push to `main` | Release workflow plus default-branch CodeQL scan; validation ran on the merged PR |
|
|
185
186
|
|
|
186
|
-
Draft pull requests do not start validation. The lightweight Draft Guard
|
|
187
|
+
Draft pull requests do not start validation. The lightweight Draft Guard converts ordinary pull requests opened or reopened while ready back to draft; it never checks out pull-request code and it excludes Release Please version heads, whose release workflow owns their state. Marking a pull request ready for review starts the applicable validation tier, and each new commit on a ready pull request reruns that tier for the current head. Draft updates allocate no validation runner. Converting a pull request to draft runs only the lightweight cancellation control.
|
|
187
188
|
|
|
188
189
|
Pull-request validation keys concurrency by event and pull-request head, so a newer update cancels its superseded run. Scheduled and manual audits use a separate caller pinned to the protected default branch; this prevents caller-selected runtime code from executing with default-branch cache access. Pull-request and scheduled/manual callers use the mode-aware orchestrator, which fans out only the required jobs and concludes with the stable aggregate gate. Main pushes run the default-branch CodeQL lane separately.
|
|
189
190
|
|
|
@@ -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
|
|
@@ -8,7 +8,6 @@ on:
|
|
|
8
8
|
types:
|
|
9
9
|
- opened
|
|
10
10
|
- reopened
|
|
11
|
-
- synchronize
|
|
12
11
|
|
|
13
12
|
permissions:
|
|
14
13
|
pull-requests: write
|
|
@@ -55,8 +54,8 @@ jobs:
|
|
|
55
54
|
;;
|
|
56
55
|
esac
|
|
57
56
|
|
|
58
|
-
# A newer state transition
|
|
59
|
-
#
|
|
57
|
+
# A newer state transition superseded this event. Do not undo a
|
|
58
|
+
# user's later explicit readiness decision with a stale event.
|
|
60
59
|
if [ "$head_sha" != "$EVENT_HEAD_SHA" ] || [ "$updated_at" != "$EVENT_UPDATED_AT" ]; then
|
|
61
60
|
echo 'A newer pull-request state or head commit superseded this draft-guard event; leaving the current state unchanged.'
|
|
62
61
|
exit 0
|
|
@@ -76,4 +75,4 @@ jobs:
|
|
|
76
75
|
"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" \
|
|
77
76
|
-F draft=true \
|
|
78
77
|
--silent >/dev/null
|
|
79
|
-
echo "Converted ordinary pull request #$PR_NUMBER to draft;
|
|
78
|
+
echo "Converted ordinary pull request #$PR_NUMBER to draft; validation waits for ready_for_review."
|
|
@@ -5,6 +5,7 @@ on:
|
|
|
5
5
|
branches: [main, staging]
|
|
6
6
|
types:
|
|
7
7
|
- ready_for_review
|
|
8
|
+
- synchronize
|
|
8
9
|
workflow_dispatch:
|
|
9
10
|
|
|
10
11
|
permissions:
|
|
@@ -13,7 +14,7 @@ permissions:
|
|
|
13
14
|
jobs:
|
|
14
15
|
detect:
|
|
15
16
|
name: OpenCode Security / Detect
|
|
16
|
-
if: vars.CI_BILLING_PAUSED != 'true'
|
|
17
|
+
if: vars.CI_BILLING_PAUSED != 'true' && (github.event_name == 'workflow_dispatch' || github.event.pull_request.draft == false)
|
|
17
18
|
runs-on: ubuntu-slim
|
|
18
19
|
outputs:
|
|
19
20
|
enabled: ${{ steps.detect.outputs.enabled }}
|