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.
@@ -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 making commits. The generated
45
- Draft Guard resets a ready ordinary pull request to draft on creation,
46
- reopening, or a new commit, and runner-heavy validation starts only after an
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. After any later commit, mark it ready again only after
50
- the current head is ready for review.
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 also converts ordinary pull requests opened, reopened, or updated 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. Convert it back to draft after an update, then mark it ready again after every update so the required checks attach to the current head. Converting it to draft runs only the lightweight cancellation control.
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: '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
@@ -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 or commit superseded this event. Do not
59
- # undo a user's later explicit readiness decision with a stale event.
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; runner-heavy validation waits for ready_for_review."
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 }}