code-foundry 1.9.5 → 1.9.6

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.
@@ -748,8 +748,9 @@ runs:
748
748
  ${{ runner.os }}-rust-build-v2-
749
749
  - name: Setup
750
750
  if: ${{ steps.profile.outputs.mise_required == 'true' }}
751
- uses: jdx/mise-action@v4
751
+ uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4
752
752
  with:
753
+ version: 2026.9.2
753
754
  cache: true
754
755
  install_args: ${{ steps.profile.outputs.mise_tools }}
755
756
  - name: Setup Rust components
@@ -35,10 +35,8 @@ prune_standard: false
35
35
  post_release: false
36
36
  post_release_workflow:
37
37
  post_release_mode: auto
38
- opencode_security: false
39
38
  sync_mode: overlay
40
39
  custom_workflows: preserve
41
- staging_validation_mode: fast
42
40
  performance: auto
43
41
  performance_command:
44
42
  performance_profile:
@@ -71,7 +71,7 @@ jobs:
71
71
  cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
72
72
  - name: Detect
73
73
  id: applicability
74
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci should_run format >> "$GITHUB_OUTPUT"
74
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci should_run format >> "$GITHUB_OUTPUT"
75
75
  - name: Setup
76
76
  if: steps.applicability.outputs.applicable == 'true'
77
77
  uses: ./.github/actions/setup
@@ -86,7 +86,7 @@ jobs:
86
86
  task: format
87
87
  - name: Format
88
88
  if: steps.applicability.outputs.applicable == 'true'
89
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci format
89
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci format
90
90
 
91
91
  lint:
92
92
  name: Lint
@@ -115,7 +115,7 @@ jobs:
115
115
  cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
116
116
  - name: Detect
117
117
  id: applicability
118
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci should_run lint >> "$GITHUB_OUTPUT"
118
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci should_run lint >> "$GITHUB_OUTPUT"
119
119
  - name: Setup
120
120
  if: steps.applicability.outputs.applicable == 'true'
121
121
  uses: ./.github/actions/setup
@@ -125,7 +125,7 @@ jobs:
125
125
  task: lint
126
126
  - name: Lint
127
127
  if: steps.applicability.outputs.applicable == 'true'
128
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci lint
128
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci lint
129
129
 
130
130
  type-check:
131
131
  name: Type-Check
@@ -154,7 +154,7 @@ jobs:
154
154
  cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
155
155
  - name: Detect
156
156
  id: applicability
157
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci should_run type_check >> "$GITHUB_OUTPUT"
157
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci should_run type_check >> "$GITHUB_OUTPUT"
158
158
  - name: Setup
159
159
  if: steps.applicability.outputs.applicable == 'true'
160
160
  uses: ./.github/actions/setup
@@ -164,7 +164,7 @@ jobs:
164
164
  task: type_check
165
165
  - name: Type-Check
166
166
  if: steps.applicability.outputs.applicable == 'true'
167
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci type_check
167
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci type_check
168
168
 
169
169
  build:
170
170
  name: Build
@@ -198,7 +198,7 @@ jobs:
198
198
  cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
199
199
  - name: Detect
200
200
  id: applicability
201
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci should_run build >> "$GITHUB_OUTPUT"
201
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci should_run build >> "$GITHUB_OUTPUT"
202
202
  - name: Setup
203
203
  if: steps.applicability.outputs.applicable == 'true'
204
204
  uses: ./.github/actions/setup
@@ -208,4 +208,4 @@ jobs:
208
208
  task: build
209
209
  - name: Build
210
210
  if: steps.applicability.outputs.applicable == 'true'
211
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci build
211
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci build
@@ -13,6 +13,21 @@ on:
13
13
  required: false
14
14
  type: string
15
15
  default: '.'
16
+ install-working-directory:
17
+ description: 'Directory containing bun.lock. Used only when build-script is set.'
18
+ required: false
19
+ type: string
20
+ default: '.'
21
+ build-script:
22
+ description: 'Optional Bun package script to run before Wrangler deploys.'
23
+ required: false
24
+ type: string
25
+ default: ''
26
+ bun-version:
27
+ description: 'Bun version used for dependency installation and the optional build.'
28
+ required: false
29
+ type: string
30
+ default: '1.4.0'
16
31
  runner:
17
32
  required: false
18
33
  type: string
@@ -48,6 +63,24 @@ jobs:
48
63
  with:
49
64
  persist-credentials: false
50
65
 
66
+ - name: Set up Bun
67
+ if: inputs.build-script != ''
68
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
69
+ with:
70
+ bun-version: ${{ inputs.bun-version }}
71
+
72
+ - name: Install dependencies
73
+ if: inputs.build-script != ''
74
+ working-directory: ${{ inputs.install-working-directory }}
75
+ run: bun install --frozen-lockfile
76
+
77
+ - name: Build Worker
78
+ if: inputs.build-script != ''
79
+ working-directory: ${{ inputs.working-directory }}
80
+ env:
81
+ BUILD_SCRIPT: ${{ inputs.build-script }}
82
+ run: bun run "$BUILD_SCRIPT"
83
+
51
84
  - name: Deploy to Cloudflare
52
85
  id: deploy
53
86
  working-directory: ${{ inputs.working-directory }}
@@ -131,7 +131,7 @@ jobs:
131
131
  REPO_FOUNDRY_CODE_SECURITY: ${{ steps.security.outputs.status }}
132
132
  REPO_FOUNDRY_PRIVATE: ${{ github.event.repository.private }}
133
133
  REPO_FOUNDRY_VISIBILITY: ${{ github.event.repository.visibility }}
134
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs codeql
134
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" codeql
135
135
  - id: matrix
136
136
  name: Build analysis matrix
137
137
  env:
@@ -0,0 +1,44 @@
1
+ name: Code Foundry Draft Control
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main, staging]
6
+ types:
7
+ - converted_to_draft
8
+
9
+ permissions:
10
+ actions: write
11
+ contents: read
12
+
13
+ jobs:
14
+ cancel:
15
+ name: Draft Control / Halt CI
16
+ if: vars.CI_BILLING_PAUSED != 'true'
17
+ runs-on: ubuntu-slim
18
+ timeout-minutes: 5
19
+ steps:
20
+ - name: Cancel queued and running checks for this branch
21
+ env:
22
+ GH_TOKEN: ${{ github.token }}
23
+ HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
24
+ CURRENT_RUN_ID: ${{ github.run_id }}
25
+ run: |
26
+ set -euo pipefail
27
+ for status in queued in_progress; do
28
+ gh api "repos/$GITHUB_REPOSITORY/actions/runs" \
29
+ --method GET \
30
+ -f branch="$HEAD_BRANCH" \
31
+ -f event=pull_request \
32
+ -f status="$status" \
33
+ --paginate \
34
+ --jq '.workflow_runs[].id' |
35
+ while read -r run_id; do
36
+ if [ -n "$run_id" ] && [ "$run_id" -lt "$CURRENT_RUN_ID" ]; then
37
+ gh api \
38
+ --method POST \
39
+ "repos/$GITHUB_REPOSITORY/actions/runs/$run_id/cancel" \
40
+ --silent || true
41
+ fi
42
+ done
43
+ done
44
+ echo "CI is halted while pull request #${{ github.event.pull_request.number }} remains draft."
@@ -4,11 +4,7 @@ on:
4
4
  pull_request:
5
5
  branches: [main, staging]
6
6
  types:
7
- - opened
8
- - synchronize
9
- - reopened
10
7
  - ready_for_review
11
- - converted_to_draft
12
8
  workflow_dispatch:
13
9
 
14
10
  permissions:
@@ -20,8 +16,7 @@ jobs:
20
16
  if: >-
21
17
  vars.CI_BILLING_PAUSED != 'true' &&
22
18
  (github.event_name == 'workflow_dispatch' ||
23
- (github.event.pull_request.draft == false &&
24
- startsWith(github.event.pull_request.head.ref, 'release-please--branches--main')))
19
+ startsWith(github.event.pull_request.head.ref, 'release-please--branches--main'))
25
20
  runs-on: ubuntu-slim
26
21
  outputs:
27
22
  enabled: ${{ steps.detect.outputs.enabled }}
@@ -39,14 +34,11 @@ jobs:
39
34
  run: |
40
35
  enabled=false
41
36
  token=false
42
- # The OPENCODE_SECURITY repository variable overrides the
43
- # configuration: true opts a repository in, false opts it out, and
44
- # an unset value falls back to the code-foundry configuration.
37
+ # OpenCode Security is controlled only by the repository or
38
+ # organization variable. An unset value is disabled.
45
39
  if [ "$OPENCODE_SECURITY_OVERRIDE" = "true" ]; then
46
40
  enabled=true
47
- elif [ "$OPENCODE_SECURITY_OVERRIDE" = "false" ]; then
48
- enabled=false
49
- elif grep -Eq '^opencode_security:\s*(true|auto)\s*$' .github/code-foundry.yml 2>/dev/null; then enabled=true; fi
41
+ fi
50
42
  if [ -n "$OPENCODE_API_KEY" ]; then token=true; fi
51
43
  echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
52
44
  echo "token=$token" >> "$GITHUB_OUTPUT"
@@ -101,9 +101,9 @@ jobs:
101
101
  releaseType = fs.existsSync('package.json') ? 'node' : fs.existsSync('pyproject.toml') ? 'python' : fs.existsSync('Cargo.toml') ? 'rust' : fs.existsSync('version.txt') ? 'simple' : 'none'
102
102
  }
103
103
  if (!['node', 'python', 'rust', 'simple', 'none'].includes(releaseType)) throw new Error(`Unsupported release_type: ${releaseType}`)
104
- const allowedStrategies = gitWorkflow === 'staging-release' ? ['rebase'] : ['rebase', 'squash']
104
+ const allowedStrategies = gitWorkflow === 'staging-release' ? ['rebase'] : ['squash']
105
105
  if (!allowedStrategies.includes(releaseMergeStrategy)) {
106
- throw new Error(`release_merge_strategy must be "rebase"${gitWorkflow === 'staging-release' ? '' : ' or "squash" (direct topology)'} for automated release merges; got ${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}`)
106
+ throw new Error(`release_merge_strategy must be "${gitWorkflow === 'staging-release' ? 'rebase' : 'squash'}" for automated release merges; got ${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}`)
107
107
  }
108
108
  if (releaseType === 'none' || !fs.existsSync('package.json')) npmPublish = 'false'
109
109
  let legacyReleaseType = ''
@@ -218,9 +218,13 @@ jobs:
218
218
  for pr in "${release_prs[@]}"; do
219
219
  PR_DRAFT=$(gh pr view "$pr" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft')
220
220
  if [ "$AUTO_MERGE" = true ]; then
221
- if [ "$PR_DRAFT" = true ]; then
222
- gh pr ready "$pr" --repo "$GITHUB_REPOSITORY"
221
+ if [ "$PR_DRAFT" = false ]; then
222
+ # A Release Please PR can be updated while already ready. Reset
223
+ # it so ready_for_review always starts validation for the exact
224
+ # current release head.
225
+ gh pr ready --undo "$pr" --repo "$GITHUB_REPOSITORY"
223
226
  fi
227
+ gh pr ready "$pr" --repo "$GITHUB_REPOSITORY"
224
228
  else
225
229
  if [ "$PR_DRAFT" = false ]; then
226
230
  gh pr ready --undo "$pr" --repo "$GITHUB_REPOSITORY"
@@ -99,7 +99,7 @@ jobs:
99
99
  env:
100
100
  REPO_FOUNDRY_PRIVATE: ${{ github.event.repository.private }}
101
101
  REPO_FOUNDRY_VISIBILITY: ${{ github.event.repository.visibility }}
102
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs security profile >> "$GITHUB_OUTPUT"
102
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" security profile >> "$GITHUB_OUTPUT"
103
103
  - id: matrix
104
104
  name: Build audit matrix
105
105
  env:
@@ -268,21 +268,24 @@ jobs:
268
268
  esac
269
269
  fi
270
270
 
271
- printf 'toolchain_mode=%s\n' "$toolchain" >> "$GITHUB_OUTPUT"
272
- printf 'package_manager=%s\n' "$package_manager" >> "$GITHUB_OUTPUT"
273
- printf 'mise_tools=%s\n' "$mise_tools" >> "$GITHUB_OUTPUT"
271
+ {
272
+ printf 'toolchain_mode=%s\n' "$toolchain"
273
+ printf 'package_manager=%s\n' "$package_manager"
274
+ printf 'mise_tools=%s\n' "$mise_tools"
275
+ } >> "$GITHUB_OUTPUT"
274
276
  - name: Detect JavaScript
275
277
  id: applicability-javascript
276
278
  if: matrix.entry.language == 'javascript'
277
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs security should_run javascript >> "$GITHUB_OUTPUT"
279
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" security should_run javascript >> "$GITHUB_OUTPUT"
278
280
  - name: Detect Rust
279
281
  id: applicability-rust
280
282
  if: matrix.entry.language == 'rust'
281
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs security should_run rust >> "$GITHUB_OUTPUT"
283
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" security should_run rust >> "$GITHUB_OUTPUT"
282
284
  - name: Setup mise toolchain
283
285
  if: steps.audit-toolchain.outputs.mise_tools != ''
284
286
  uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4
285
287
  with:
288
+ version: 2026.9.2
286
289
  cache: false
287
290
  install_args: ${{ steps.audit-toolchain.outputs.mise_tools }}
288
291
  - name: Setup Node
@@ -316,7 +319,7 @@ jobs:
316
319
  if: >-
317
320
  matrix.entry.language == 'javascript' &&
318
321
  steps.applicability-javascript.outputs.applicable == 'true'
319
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs security audit javascript
322
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" security audit javascript
320
323
  - name: Check Rust
321
324
  id: system-rust
322
325
  if: >-
@@ -349,7 +352,7 @@ jobs:
349
352
  if: >-
350
353
  matrix.entry.language == 'rust' &&
351
354
  steps.applicability-rust.outputs.applicable == 'true'
352
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs security audit rust
355
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" security audit rust
353
356
  - name: Setup Python
354
357
  if: >-
355
358
  matrix.entry.language == 'python' &&
@@ -375,4 +378,4 @@ jobs:
375
378
  enable-cache: false
376
379
  - name: Audit Python dependencies
377
380
  if: matrix.entry.language == 'python'
378
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs security audit python
381
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" security audit python
@@ -86,7 +86,7 @@ jobs:
86
86
  cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
87
87
  - name: Detect
88
88
  id: applicability
89
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci task_profile unit >> "$GITHUB_OUTPUT"
89
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci task_profile unit >> "$GITHUB_OUTPUT"
90
90
  - name: Setup
91
91
  if: steps.applicability.outputs.applicable == 'true'
92
92
  uses: ./.github/actions/setup
@@ -104,7 +104,7 @@ jobs:
104
104
  task-rust: ${{ steps.applicability.outputs.rust }}
105
105
  - name: Unit
106
106
  if: steps.applicability.outputs.applicable == 'true'
107
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci unit
107
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci unit
108
108
  - name: Upload coverage
109
109
  if: >-
110
110
  always() &&
@@ -151,7 +151,7 @@ jobs:
151
151
  cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
152
152
  - name: Detect
153
153
  id: applicability
154
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci task_profile performance >> "$GITHUB_OUTPUT"
154
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci task_profile performance >> "$GITHUB_OUTPUT"
155
155
  - name: Setup
156
156
  if: steps.applicability.outputs.applicable == 'true'
157
157
  uses: ./.github/actions/setup
@@ -170,7 +170,7 @@ jobs:
170
170
  task-rust: ${{ steps.applicability.outputs.rust }}
171
171
  - name: Performance
172
172
  if: steps.applicability.outputs.applicable == 'true'
173
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci performance
173
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci performance
174
174
  - name: Upload performance results
175
175
  if: >-
176
176
  always() &&
@@ -216,7 +216,7 @@ jobs:
216
216
  cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
217
217
  - name: Detect
218
218
  id: applicability
219
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci task_profile integration >> "$GITHUB_OUTPUT"
219
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci task_profile integration >> "$GITHUB_OUTPUT"
220
220
  - name: Setup
221
221
  if: steps.applicability.outputs.applicable == 'true'
222
222
  uses: ./.github/actions/setup
@@ -234,7 +234,7 @@ jobs:
234
234
  task-rust: ${{ steps.applicability.outputs.rust }}
235
235
  - name: Integration
236
236
  if: steps.applicability.outputs.applicable == 'true'
237
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci integration
237
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci integration
238
238
 
239
239
  e2e:
240
240
  name: E2E
@@ -265,7 +265,7 @@ jobs:
265
265
  cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
266
266
  - name: Detect
267
267
  id: applicability
268
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci task_profile e2e >> "$GITHUB_OUTPUT"
268
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci task_profile e2e >> "$GITHUB_OUTPUT"
269
269
  - name: Cache browser binaries
270
270
  if: steps.applicability.outputs.applicable == 'true' && steps.applicability.outputs.browser == 'true'
271
271
  uses: ./.github/actions/cache
@@ -295,7 +295,7 @@ jobs:
295
295
  task-rust: ${{ steps.applicability.outputs.rust }}
296
296
  - name: E2E
297
297
  if: steps.applicability.outputs.applicable == 'true'
298
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci e2e
298
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci e2e
299
299
 
300
300
  smoke:
301
301
  name: Smoke
@@ -326,7 +326,7 @@ jobs:
326
326
  cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
327
327
  - name: Detect
328
328
  id: applicability
329
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci task_profile smoke >> "$GITHUB_OUTPUT"
329
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci task_profile smoke >> "$GITHUB_OUTPUT"
330
330
  - name: Setup
331
331
  if: steps.applicability.outputs.applicable == 'true'
332
332
  uses: ./.github/actions/setup
@@ -344,4 +344,4 @@ jobs:
344
344
  task-rust: ${{ steps.applicability.outputs.rust }}
345
345
  - name: Smoke
346
346
  if: steps.applicability.outputs.applicable == 'true'
347
- run: node $RUNNER_TEMP/code-foundry/src/runtime.mjs ci smoke
347
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci smoke
@@ -4,11 +4,7 @@ on:
4
4
  pull_request:
5
5
  branches: [main, staging]
6
6
  types:
7
- - opened
8
- - synchronize
9
- - reopened
10
7
  - ready_for_review
11
- - converted_to_draft
12
8
 
13
9
  # Default every caller job to no repository permissions. Individual jobs grant
14
10
  # only the scopes their own validation path consumes.
@@ -22,10 +18,9 @@ concurrency:
22
18
  jobs:
23
19
  mode:
24
20
  name: Mode
25
- # Draft updates still create a lightweight skipped workflow record so a
26
- # converted-to-draft event can cancel in-flight validation, but no runner
27
- # or reusable validation workflow starts until the PR is ready for review.
28
- if: vars.CI_BILLING_PAUSED != 'true' && github.event.pull_request.draft == false
21
+ # The workflow is emitted only for ready_for_review, so draft pull requests
22
+ # register no skipped validation checks and allocate no validation runner.
23
+ if: vars.CI_BILLING_PAUSED != 'true'
29
24
  runs-on: ubuntu-slim
30
25
  timeout-minutes: 10
31
26
  permissions:
@@ -64,7 +59,7 @@ jobs:
64
59
  validation:
65
60
  name: Validation
66
61
  needs: mode
67
- if: vars.CI_BILLING_PAUSED != 'true' && github.event.pull_request.draft == false
62
+ if: vars.CI_BILLING_PAUSED != 'true'
68
63
  # Reusable workflows can only maintain or reduce the caller job's scopes.
69
64
  # The audit tier needs security-events: write for CodeQL uploads.
70
65
  permissions:
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.9.6](https://github.com/0xPlayerOne/code-foundry/compare/v1.9.5...v1.9.6) (2026-09-08)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **ci:** finalize direct fleet policy ([#513](https://github.com/0xPlayerOne/code-foundry/issues/513)) ([98c59a7](https://github.com/0xPlayerOne/code-foundry/commit/98c59a713f31d0bc5e3b9893434d89dba3257550))
9
+
3
10
  ## [1.9.5](https://github.com/0xPlayerOne/code-foundry/compare/v1.9.4...v1.9.5) (2026-09-08)
4
11
 
5
12
 
package/README.md CHANGED
@@ -75,9 +75,10 @@ The standard workflow triggers are:
75
75
  - Draft PR automation for supported feature/fix branches.
76
76
 
77
77
  Automated feature/fix and staging-promotion pull requests open as drafts.
78
- Pull-request validation waits until a PR is marked ready for review; later
79
- commits retrigger it, and converting the PR back to draft cancels in-flight
80
- validation. Scheduled and manually dispatched audits are unaffected.
78
+ Pull-request validation runs on the ready-for-review transition. After later
79
+ commits, convert the PR to draft and mark it ready again so checks attach to
80
+ the current head. Converting a PR to draft runs only the lightweight
81
+ cancellation control. Scheduled and manually dispatched audits are unaffected.
81
82
 
82
83
  Jobs are language-aware and skip irrelevant setup inside the applicable
83
84
  aggregate checks. TypeScript uses Oxlint, Oxfmt, and Bun's native
@@ -102,7 +103,8 @@ checks, runners, coverage, caching, and custom workflow extensions.
102
103
 
103
104
  The contribution policy defaults to the `direct` workflow: feature PRs squash
104
105
  into `main`, and Release Please version PRs use the configured
105
- `release_merge_strategy` (rebase by default, or squash when opted in).
106
+ `release_merge_strategy` (squash for direct repositories; rebase for the
107
+ optional staging-release topology).
106
108
  Repositories with a preview/staging environment opt into
107
109
  `git_workflow: staging-release`, where feature PRs squash into `staging`, the
108
110
  promotion PR rebases into `main` (`merge_strategy: rebase`), and Release Please
@@ -35,7 +35,7 @@ repository manifests and source
35
35
  | `languages` | detected list | TypeScript, Rust, Python, Solidity |
36
36
  | `package_manager` | `bun`, `pnpm`, `yarn`, `npm`, `none` | JavaScript setup |
37
37
  | `toolchain` | `auto`, `native`, `mise` | Environment setup policy; defaults to `auto` |
38
- | `staging_validation_mode` | `fast`, `audit` | Validation tier for pull requests targeting `staging`; defaults to `fast` |
38
+ | `staging_validation_mode` | `fast`, `audit` | Staging-release-only validation tier; omitted from direct repositories |
39
39
  | `performance` | `auto`, `true`, `false` | Run a deterministic performance task when a supported entrypoint exists; defaults to `auto` |
40
40
  | `performance_command` | JSON argv array or array of argv arrays | One or more ordered commands for non-package harnesses; package scripts take precedence |
41
41
  | `performance_profile` | empty, `node-package` | Optional shared package import, memory, archive, and dependency budget harness |
@@ -53,8 +53,8 @@ repository manifests and source
53
53
  | `npm_publish` | `true` or `false` | Opt into npm publication |
54
54
  | `license` | `gpl-3.0-or-later`, `agpl-3.0-or-later`, `apache-2.0`, `mit`, `preserve`, `none` | License policy; new repositories default to GPLv3 |
55
55
  | `git_workflow` | `direct` (default), `staging-release` | Branch/release model; `direct` opens feature branches into `main`, `staging-release` promotes `staging` into `main` |
56
- | `merge_strategy` | `rebase` | Promotion merge method for `staging` → `main`; only enforced by the `staging-release` topology |
57
- | `release_merge_strategy` | `rebase`, `squash` (direct topology only) | Merge method for Release Please version PRs into `main`; release automation fails closed on anything else |
56
+ | `merge_strategy` | `squash` (direct), `rebase` (staging-release) | Feature/promotion merge policy; direct repositories require squash |
57
+ | `release_merge_strategy` | `squash` (direct), `rebase` (staging-release) | Merge method for Release Please version PRs into `main`; release automation fails closed on anything else |
58
58
  | `runner` fields | GitHub runner names | Per-workflow runner policy, including `performance_runner` |
59
59
 
60
60
  Supported features are `ci`, `codeql`, `security`, `test`, `draft-pr`,
@@ -113,14 +113,13 @@ checks should remain separate from this deterministic validation task.
113
113
 
114
114
  ## OpenCode Security opt-in and opt-out
115
115
 
116
- `opencode_security` controls the optional OpenCode scan, and the generated
117
- caller ships in every repository. The `OPENCODE_SECURITY` repository variable
118
- overrides the configuration at run time, so a scan can be toggled on any
119
- repository without a configuration change or a new secret:
116
+ The generated OpenCode caller ships in every repository. The
117
+ `OPENCODE_SECURITY` repository or organization variable is its only enablement
118
+ control, so a scan can be toggled without a code change:
120
119
 
121
120
  - `OPENCODE_SECURITY: true` opts the repository in.
122
121
  - `OPENCODE_SECURITY: false` opts the repository out.
123
- - unset falls back to the `opencode_security` configuration (default `false`).
122
+ - unset is disabled.
124
123
 
125
124
  The scan only runs when it is enabled and the `OPENCODE_API_KEY` secret is
126
125
  present.
@@ -131,8 +130,8 @@ present.
131
130
 
132
131
  - `direct` (default): feature branches open pull requests directly into
133
132
  `main`. Validation and security scans run on every PR. No `staging` branch
134
- exists, no promotion caller is generated, and `merge_strategy` is not
135
- enforced. Release Please version PRs squash into `main`
133
+ exists, no promotion caller is generated, and `merge_strategy` must be
134
+ `squash`. Release Please version PRs squash into `main`
136
135
  (`release_merge_strategy: squash`). Dependabot updates target `main`. This is
137
136
  the right choice when a repository has no preview or staging environment.
138
137
  - `staging-release` (opt-in): feature branches squash into `staging`, a
@@ -181,4 +180,7 @@ Vercel's integration) by adding a small caller for the runtime's reusable
181
180
  for pull-request previews and `wrangler deploy` for production, records a
182
181
  GitHub deployment plus status with the workers.dev URL, and respects
183
182
  `CI_BILLING_PAUSED`. It requires the `CLOUDFLARE_API_TOKEN` and
184
- `CLOUDFLARE_ACCOUNT_ID` secrets in the consumer repository.
183
+ `CLOUDFLARE_ACCOUNT_ID` secrets in the consumer repository. Bun consumers may
184
+ also pass `build-script`, `install-working-directory`, and `bun-version`; the
185
+ runtime installs the frozen lockfile and builds the Worker before invoking
186
+ Wrangler.
package/docs/RELEASES.md CHANGED
@@ -27,7 +27,7 @@ rebase`), and Release Please version PRs merge with **rebase**
27
27
  branches squash straight into `main` and Release Please version PRs squash into
28
28
  `main` (`release_merge_strategy: squash`). `merge_strategy` is not enforced. Release automation never defaults
29
29
  to a merge method and never merges with `--admin`: `staging-release` accepts
30
- only rebase for release PRs, while `direct` accepts rebase or squash.
30
+ only rebase for release PRs, while `direct` requires squash.
31
31
 
32
32
  The release workflow opens or updates a versioned PR after changes reach
33
33
  `main`. Merging that PR updates the changelog, creates the Git tag and GitHub
@@ -54,7 +54,7 @@ selected, `merge_strategy` applies to promotion PRs (`staging` into `main`)
54
54
  and `release_merge_strategy` to Release Please version PRs; feature PRs into
55
55
  `staging` use squash merges. `code-foundry doctor`, `code-foundry sync`, and
56
56
  the release workflow reject any non-`rebase` `merge_strategy` or release
57
- strategy in `staging-release`; `direct` accepts `rebase` or `squash` for
57
+ strategy in `staging-release`; `direct` requires `squash` for
58
58
  release PRs and never falls back to `merge`. Both keep `main` fully linear,
59
59
  which is what makes the post-release reconciliation possible. The final
60
60
  branch trees are inspected before mutation; validated main-only changes are
package/docs/WORKFLOWS.md CHANGED
@@ -12,10 +12,12 @@ pull_request:
12
12
  # direct topology: branches: [main]
13
13
  ```
14
14
 
15
- The generated caller suppresses runner and reusable-workflow jobs while the
16
- pull request is a draft. It listens for `ready_for_review` to start validation,
17
- for later `synchronize` events to rerun it, and for `converted_to_draft` to
18
- cancel in-flight validation through the caller's concurrency group.
15
+ The generated validation caller listens only for `ready_for_review`, so draft
16
+ pull requests register no validation checks and allocate no validation runner.
17
+ After any later update, convert the pull request to draft and mark it ready
18
+ again so required checks attach to the current head. A separate lightweight
19
+ draft-control caller listens for `converted_to_draft` and cancels queued or
20
+ running pull-request workflows without creating skipped validation jobs.
19
21
 
20
22
  The separate `validation-audit.yml` caller is pinned to the configured released
21
23
  runtime and handles scheduled and manual audits:
@@ -39,7 +41,7 @@ opens those PRs as drafts. Promotion automation listens to `staging` pushes
39
41
  (staging-release only) and also always opens its PR as a draft. Release
40
42
  automation listens to `main` pushes.
41
43
  Custom deployment, indexing, search, Slither, or other workflows are
42
- repository-owned extensions and should keep their own triggers and permissions.
44
+ repository-owned extensions and should use the same ready-transition policy.
43
45
 
44
46
  ## Billing pause
45
47
 
@@ -83,9 +85,9 @@ throughout the bounded release flow.
83
85
  Custom workflows are repository-owned and are not rewritten by sync. Add
84
86
  `if: vars.CI_BILLING_PAUSED != 'true'` to each custom root job that should
85
87
  honor the shared billing pause. The optional OpenCode Security scan honors a
86
- second toggle: the `OPENCODE_SECURITY` repository variable (`true`/`false`)
87
- overrides the `opencode_security` configuration per run, so individual
88
- repositories can opt in or out without a code change.
88
+ second toggle: the `OPENCODE_SECURITY` repository or organization variable
89
+ (`true`/`false`) is its only enablement control, so individual repositories can
90
+ opt in or out without a code change.
89
91
 
90
92
  ## Standard workflow responsibilities
91
93
 
@@ -114,20 +116,20 @@ succeeds.
114
116
 
115
117
  The merge audit pins one merge method per transition. `code-foundry doctor`
116
118
  and `code-foundry sync` validate release strategy against the repository
117
- topology. Staging-release release PRs require `rebase`; direct release PRs
118
- allow `rebase` or `squash`. The release workflow fails closed on any other
119
- strategy and never falls back to `merge`.
119
+ topology. Direct feature and release PRs require `squash`; staging-release
120
+ promotion and release PRs require `rebase`. The release workflow fails closed
121
+ on any other strategy and never falls back to `merge`.
120
122
 
121
- | Transition | Merge method | Enforcement |
122
- | ---------------------------------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
123
- | Feature/fix PR into `main` (direct topology) | Squash | Contribution policy; see `CONTRIBUTING.md` |
124
- | Feature/fix PR into `staging` (staging-release topology) | Squash | Contribution policy; see `CONTRIBUTING.md` |
125
- | `staging` → `main` promotion PR (staging-release topology) | Rebase (`merge_strategy: rebase`) | Code Foundry creates a one-commit head with `main` as its parent and the exact validated `staging` tree; `merge_strategy` must be `rebase`, and merge commits are rejected |
126
- | Release Please version PR into `main` | Configured (`release_merge_strategy`: rebase default; squash direct only) | Release automation fails closed on unsupported topology/strategy; never defaults to `merge`, never uses `--admin` |
123
+ | Transition | Merge method | Enforcement |
124
+ | ---------------------------------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
125
+ | Feature/fix PR into `main` (direct topology) | Squash | Contribution policy; see `CONTRIBUTING.md` |
126
+ | Feature/fix PR into `staging` (staging-release topology) | Squash | Contribution policy; see `CONTRIBUTING.md` |
127
+ | `staging` → `main` promotion PR (staging-release topology) | Rebase (`merge_strategy: rebase`) | Code Foundry creates a one-commit head with `main` as its parent and the exact validated `staging` tree; `merge_strategy` must be `rebase`, and merge commits are rejected |
128
+ | Release Please version PR into `main` | Squash (direct) or rebase (staging-release) | Release automation fails closed on unsupported topology/strategy; never defaults to `merge`, never uses `--admin` |
127
129
 
128
130
  The promotion rows above apply only to `staging-release`; `direct`
129
- repositories never generate a promotion caller and `merge_strategy` is not
130
- enforced for them.
131
+ repositories never generate a promotion caller and require `merge_strategy:
132
+ squash` for feature pull requests.
131
133
 
132
134
  Release auto-merge waits for required checks and then polls `mergeStateStatus`
133
135
  until it is `CLEAN`, or `UNSTABLE` with `mergeable` `MERGEABLE`, before
@@ -139,7 +141,7 @@ merge policy-blocked. The mergeability poll is bounded and fails closed on
139
141
  conflicts or timeout; releases without an automation token remain manual.
140
142
 
141
143
  Keeping `main` linear — rebase promotions and release PRs in the
142
- `staging-release` topology, or a single-commit rebase/squash release PR in the
144
+ `staging-release` topology, or a single-commit squash release PR in the
143
145
  `direct` topology — is what lets the post-release reconciliation fast-forward
144
146
  or replay `staging` safely. `direct` repositories have no reconciliation step:
145
147
  releases merge straight into `main` with the configured
@@ -147,7 +149,7 @@ releases merge straight into `main` with the configured
147
149
 
148
150
  Protect `main` with the aggregate `Validation / Gate`. Require squash for
149
151
  feature/fix pull requests and permit the configured Release Please method:
150
- rebase in `staging-release`, or rebase/squash in `direct`. In the
152
+ rebase in `staging-release`, or squash in `direct`. In the
151
153
  `staging-release` topology, protect `staging` the same way with a single GitHub
152
154
  Actions integration path. That path uses the
153
155
  GitHub Actions integration token by default, and optionally an SSH deploy key
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.9.5",
3
+ "version": "1.9.6",
4
4
  "description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
5
5
  "homepage": "https://github.com/0xPlayerOne/code-foundry#readme",
6
6
  "bugs": {
@@ -116,30 +116,45 @@ export function doctor(root, options = {}) {
116
116
  error(`unsupported git_workflow: ${workflow}; use direct or staging-release`)
117
117
  }
118
118
  const mergeStrategy = config.merge_strategy ?? 'rebase'
119
+ if (workflow === 'direct' && mergeStrategy !== 'squash') {
120
+ error(
121
+ `merge_strategy must be "squash" for direct feature pull requests; got "${mergeStrategy}".`
122
+ )
123
+ }
119
124
  if (workflow === 'staging-release' && mergeStrategy !== 'rebase') {
120
125
  error(
121
126
  `merge_strategy must be "rebase" for the staging-release promotion topology; got "${mergeStrategy}".`
122
127
  )
123
128
  }
124
129
  const releaseMergeStrategy = config.release_merge_strategy ?? ''
125
- const allowedReleaseStrategies =
126
- workflow === 'staging-release' ? ['rebase'] : ['rebase', 'squash']
130
+ const allowedReleaseStrategies = workflow === 'staging-release' ? ['rebase'] : ['squash']
127
131
  if (
128
132
  includesValue(features, 'release') &&
129
133
  !allowedReleaseStrategies.includes(releaseMergeStrategy)
130
134
  ) {
131
135
  error(
132
- `release_merge_strategy must be "rebase"${workflow === 'staging-release' ? '' : ' or "squash" (direct topology)'} for automated release merges; got "${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}".`
136
+ `release_merge_strategy must be "${workflow === 'staging-release' ? 'rebase' : 'squash'}" for automated release merges; got "${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}".`
133
137
  )
134
138
  }
135
- for (const name of ['validation', 'draft-pr', 'release-pr', 'release']) {
136
- if (name === 'release-pr' && workflow !== 'staging-release') continue
137
- if (includesValue(features, name) && !existsSync(join(target, `.github/workflows/${name}.yml`)))
138
- error(`missing enabled workflow: ${name}.yml`)
139
- }
139
+ if ('opencode_security' in config)
140
+ error('opencode_security is obsolete; use the OPENCODE_SECURITY repository variable.')
141
+ if (workflow === 'direct' && 'staging_validation_mode' in config)
142
+ error(
143
+ 'staging_validation_mode is not valid in a direct-topology repository; run sync to prune it.'
144
+ )
140
145
  const validationEnabled =
141
146
  includesValue(features, 'validation') ||
142
147
  ['ci', 'test', 'security', 'codeql'].some((legacy) => includesValue(features, legacy))
148
+ for (const name of ['validation', 'draft-control', 'draft-pr', 'release-pr', 'release']) {
149
+ if (name === 'release-pr' && workflow !== 'staging-release') continue
150
+ const enabled = name === 'draft-control' ? validationEnabled : includesValue(features, name)
151
+ const candidates = [
152
+ join(target, `.github/workflows/${name}.yml`),
153
+ join(target, `.github/workflows/${name}_self-ci.yml`),
154
+ ]
155
+ if (enabled && !candidates.some((file) => existsSync(file)))
156
+ error(`missing enabled workflow: ${name}.yml`)
157
+ }
143
158
  const validationCaller = ['validation.yml', 'validation_self-ci.yml']
144
159
  .map((file) => join(target, `.github/workflows/${file}`))
145
160
  .find((file) => existsSync(file) && /pull_request:/.test(readFileSync(file, 'utf8')))
@@ -152,6 +167,12 @@ export function doctor(root, options = {}) {
152
167
  'validation caller is missing the Validation job; the Validation / Gate aggregate check cannot form.'
153
168
  )
154
169
  }
170
+ if (!/types:\n\s+- ready_for_review/.test(caller)) {
171
+ error('validation caller must wait for the ready_for_review transition.')
172
+ }
173
+ if (/\s+- (?:opened|synchronize|reopened|converted_to_draft)\s*$/.test(caller)) {
174
+ error('validation caller registers draft-time checks; run code-foundry sync.')
175
+ }
155
176
  if (
156
177
  !/uses:\s+(?:\.\/\.github\/workflows\/validation(?:-no-codeql)?\.yml|\S+\/\.github\/workflows\/validation(?:-no-codeql)?\.yml@)/.test(
157
178
  caller
@@ -43,6 +43,7 @@ const standardFiles = [
43
43
  '.github/ISSUE_TEMPLATE/feature_request.yml',
44
44
  '.github/workflows/validation.yml',
45
45
  '.github/workflows/validation-audit.yml',
46
+ '.github/workflows/draft-control.yml',
46
47
  '.github/workflows/draft-pr.yml',
47
48
  '.github/workflows/release-pr.yml',
48
49
  '.github/workflows/release.yml',
@@ -117,6 +118,15 @@ export function syncRepository(options) {
117
118
  throw new Error('Missing .github/code-foundry.yml; run init first.')
118
119
  const defaults = createDefaultConfig(target, source, existingConfig.git_workflow)
119
120
  let config = { ...defaults, ...existingConfig }
121
+ const workflow = gitWorkflow(config.git_workflow)
122
+ if (!['direct', 'staging-release'].includes(workflow)) {
123
+ throw new Error(`Unsupported git_workflow: ${workflow}; use direct or staging-release.`)
124
+ }
125
+ const obsoleteConfigKeys = [
126
+ 'opencode_security',
127
+ ...(workflow === 'direct' ? ['staging_validation_mode'] : []),
128
+ ]
129
+ for (const key of obsoleteConfigKeys) delete config[key]
120
130
  // Resolve and validate the license policy before any sync writes occur
121
131
  // (including config/default additions) so an unsupported policy fails
122
132
  // fast without leaving partially generated files behind.
@@ -133,10 +143,11 @@ export function syncRepository(options) {
133
143
  writeOrReport(configPath, renderConfig(config), dryRun)
134
144
  } else {
135
145
  const missing = Object.keys(defaults).filter((key) => !(key in existingConfig))
136
- if (missing.length) {
137
- const current = readFileSync(configPath, 'utf8').trimEnd()
146
+ const original = readFileSync(configPath, 'utf8')
147
+ const normalized = removeConfigKeys(original, obsoleteConfigKeys).trimEnd()
148
+ if (missing.length || normalized !== original.trimEnd()) {
138
149
  const additions = missing.map((key) => renderConfigLine(key, defaults[key])).join('\n')
139
- writeOrReport(configPath, `${current}\n${additions}\n`, dryRun)
150
+ writeOrReport(configPath, `${normalized}${additions ? `\n${additions}` : ''}\n`, dryRun)
140
151
  }
141
152
  }
142
153
 
@@ -155,17 +166,18 @@ export function syncRepository(options) {
155
166
  if (!['auto', 'native', 'mise'].includes(toolchain)) {
156
167
  throw new Error(`Unsupported toolchain: ${toolchain}; use auto, native, or mise.`)
157
168
  }
158
- const workflow = gitWorkflow(config.git_workflow)
159
- if (!['direct', 'staging-release'].includes(workflow)) {
160
- throw new Error(`Unsupported git_workflow: ${workflow}; use direct or staging-release.`)
161
- }
162
169
  const stagingValidationMode = configured(config.staging_validation_mode, 'fast')
163
- if (!['fast', 'audit'].includes(stagingValidationMode)) {
170
+ if (workflow === 'staging-release' && !['fast', 'audit'].includes(stagingValidationMode)) {
164
171
  throw new Error(
165
172
  `Unsupported staging_validation_mode: ${stagingValidationMode}; use fast or audit.`
166
173
  )
167
174
  }
168
175
  const mergeStrategy = configured(config.merge_strategy, 'rebase')
176
+ if (workflow === 'direct' && mergeStrategy !== 'squash') {
177
+ throw new Error(
178
+ `Unsupported merge_strategy: ${mergeStrategy}; the direct topology requires squash for feature pull requests.`
179
+ )
180
+ }
169
181
  if (workflow === 'staging-release' && mergeStrategy !== 'rebase') {
170
182
  throw new Error(
171
183
  `Unsupported merge_strategy: ${mergeStrategy}; the staging-release topology requires rebase for staging to main promotions.`
@@ -177,11 +189,10 @@ export function syncRepository(options) {
177
189
  // release commits; the direct topology has no reconciliation step, so it
178
190
  // may also squash Release Please version PRs (a single-commit release PR
179
191
  // squashes to the identical tree, and release-please recommends squash).
180
- const allowedReleaseStrategies =
181
- workflow === 'staging-release' ? ['rebase'] : ['rebase', 'squash']
192
+ const allowedReleaseStrategies = workflow === 'staging-release' ? ['rebase'] : ['squash']
182
193
  if (!allowedReleaseStrategies.includes(releaseMergeStrategy)) {
183
194
  throw new Error(
184
- `Unsupported release_merge_strategy: ${releaseMergeStrategy || '(unset)'}; release automation requires rebase (or squash in the direct topology) for Release Please version pull requests and never defaults to merge.`
195
+ `Unsupported release_merge_strategy: ${releaseMergeStrategy || '(unset)'}; release automation requires ${workflow === 'staging-release' ? 'rebase' : 'squash'} for Release Please version pull requests and never defaults to merge.`
185
196
  )
186
197
  }
187
198
  }
@@ -459,6 +470,7 @@ function shouldInclude(file, languages, features, config) {
459
470
  // the configuration or the variable enables it and the API key exists.
460
471
  if (file === '.github/workflows/opencode-security.yml') return true
461
472
  const workflow = file.match(/^\.github\/workflows\/([^/]+)\.yml$/)?.[1]
473
+ if (workflow === 'draft-control') return true
462
474
  // The staging promotion caller only exists in the staging-release topology;
463
475
  // direct repositories open feature branches into main and need no promotion.
464
476
  if (workflow === 'release-pr' && !isStagingRelease(config.git_workflow)) return false
@@ -695,7 +707,7 @@ const DIRECT_DOC_REPLACEMENTS = {
695
707
  ],
696
708
  [
697
709
  'The Git workflow is `staging-release`: topic branches **squash** into `staging`, a promotion PR **rebases** validated changes into `main` (`merge_strategy: rebase`), and the Release Please version PR **rebases** into `main` (`release_merge_strategy: rebase`). Release automation never defaults to a merge method and never merges with `--admin`; `code-foundry doctor` and `code-foundry sync` fail closed on any other merge strategy. Re-align `staging` with `main` after a release when needed.',
698
- 'The Git workflow is `direct`: topic branches **squash** directly into `main`, and the Release Please version PR **squashes** into `main` (`release_merge_strategy: squash`). Release automation never defaults to a merge method and never merges with `--admin`; `code-foundry doctor` and `code-foundry sync` fail closed on any other release merge strategy. Feature branches never touch `staging`; repositories with a preview/staging environment opt into `git_workflow: staging-release` explicitly.',
710
+ 'The Git workflow is `direct`: topic branches **squash** directly into `main`, and the Release Please version PR **squashes** into `main` (`release_merge_strategy: squash`). Release automation never defaults to a merge method and never merges with `--admin`; `code-foundry doctor` and `code-foundry sync` fail closed on any other release merge strategy. This repository has one protected integration and release branch: `main`.',
699
711
  ],
700
712
  [
701
713
  'git switch staging\ngit pull --ff-only origin staging',
@@ -723,6 +735,10 @@ const DIRECT_DOC_REPLACEMENTS = {
723
735
  '| Change | Target | Merge method | Merge gate |\n| ---------------------------- | --------- | ----------------------------------------------- | --------------------------------------------------------- |\n| Working branch | `staging` | Squash | All applicable required checks pass |\n| `staging` → `main` promotion | `main` | Rebase (`merge_strategy`) | Current staging checks, release review, and rollout notes |\n| Release Please version PR | `main` | Rebase (`release_merge_strategy`, fails closed) | Validation gate and release policy pass |\n',
724
736
  '| Change | Target | Merge method | Merge gate |\n|----------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| Working branch | `main` | Squash | All applicable required checks pass |\n| Release Please version PR | `main` | Squash (`release_merge_strategy`) | Validation gate and release policy pass |',
725
737
  ],
738
+ [
739
+ 'Draft pull requests do not start runner-heavy validation. Marking a pull request ready for review starts the applicable validation tier; converting it back to draft cancels in-flight validation, and no replacement starts until it is ready again.',
740
+ 'Draft pull requests do not start validation. 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.',
741
+ ],
726
742
  ['1. Create a focused branch from `staging`.', '1. Create a focused branch from `main`.'],
727
743
  ],
728
744
  '.github/SECURITY.md': [
@@ -1149,7 +1165,7 @@ function createDefaultConfig(root, source, configuredWorkflow) {
1149
1165
  runtime_ref: `v${readPackageVersion(source)}`,
1150
1166
  ...runners,
1151
1167
  toolchain: 'auto',
1152
- staging_validation_mode: 'fast',
1168
+ ...(stagingRelease ? { staging_validation_mode: 'fast' } : {}),
1153
1169
  performance: 'auto',
1154
1170
  performance_command: '',
1155
1171
  performance_profile: '',
@@ -1164,7 +1180,6 @@ function createDefaultConfig(root, source, configuredWorkflow) {
1164
1180
  post_release: 'false',
1165
1181
  post_release_workflow: '',
1166
1182
  post_release_mode: 'auto',
1167
- opencode_security: 'false',
1168
1183
  opencode_security_model: '',
1169
1184
  sync_mode: 'overlay',
1170
1185
  custom_workflows: 'preserve',
@@ -1181,6 +1196,20 @@ function renderConfig(config) {
1181
1196
  .map(([key, value]) => renderConfigLine(key, value))
1182
1197
  .join('\n')}\n`
1183
1198
  }
1199
+
1200
+ /** @param {string} content @param {string[]} keys */
1201
+ function removeConfigKeys(content, keys) {
1202
+ if (!keys.length) return content
1203
+ const rejected = new Set(keys)
1204
+ return content
1205
+ .split(/\r?\n/)
1206
+ .filter((line) => {
1207
+ const key = line.match(/^([A-Za-z0-9_-]+):/)?.[1]
1208
+ return !key || !rejected.has(key)
1209
+ })
1210
+ .join('\n')
1211
+ }
1212
+
1184
1213
  /** @param {string} key @param {string} value */
1185
1214
  function renderConfigLine(key, value) {
1186
1215
  if (value === '') return `${key}:`
@@ -71,7 +71,42 @@ export function doctorGithub(root) {
71
71
  )
72
72
  }
73
73
 
74
- const config = readConfig(root)
74
+ const config = readConfig(join(root, '.github/code-foundry.yml'))
75
+ const repositorySettings = ghJson(['api', `repos/${repository}`]) ?? {}
76
+ const configuredWorkflow = config.git_workflow ?? 'direct'
77
+ details.mergeMethods = {
78
+ squash: repositorySettings.allow_squash_merge,
79
+ rebase: repositorySettings.allow_rebase_merge,
80
+ merge: repositorySettings.allow_merge_commit,
81
+ autoMerge: repositorySettings.allow_auto_merge,
82
+ }
83
+ if (configuredWorkflow === 'direct') {
84
+ if (repositorySettings.allow_squash_merge !== true)
85
+ errors.push('direct topology requires squash merging to be enabled.')
86
+ if (
87
+ repositorySettings.allow_rebase_merge === true ||
88
+ repositorySettings.allow_merge_commit === true
89
+ )
90
+ errors.push('direct topology must disable rebase and merge-commit methods.')
91
+ if (config.release_type !== 'none' && repositorySettings.allow_auto_merge !== true)
92
+ errors.push('automated releases require repository auto-merge to be enabled.')
93
+ details.stagingBranchExists = Boolean(
94
+ ghJson(['api', `repos/${repository}/branches/staging`])?.name
95
+ )
96
+ if (details.stagingBranchExists)
97
+ errors.push('direct topology still has a staging branch; remove the obsolete branch.')
98
+ for (const ruleset of mainRulesets) {
99
+ for (const rule of ruleset.rules ?? []) {
100
+ if (rule?.type !== 'pull_request') continue
101
+ const methods = rule.parameters?.allowed_merge_methods ?? []
102
+ if (methods.length !== 1 || methods[0] !== 'squash') {
103
+ errors.push(
104
+ `main ruleset ${ruleset.name ?? ruleset.id} must allow only squash merges; got ${methods.join(', ') || '(none)'}.`
105
+ )
106
+ }
107
+ }
108
+ }
109
+ }
75
110
  if (
76
111
  ['workflow-dispatch', 'dispatch'].includes(config.post_release_mode) &&
77
112
  config.post_release !== 'false' &&
@@ -80,11 +115,19 @@ export function doctorGithub(root) {
80
115
  ) {
81
116
  errors.push('post-release workflow-dispatch mode requires CODE_FOUNDRY_TOKEN to be present.')
82
117
  }
118
+ const repositoryVariables =
119
+ ghJson(['variable', 'list', '--repo', repository, '--json', 'name,value']) ?? []
120
+ const opencodeSecurityVariable = Array.isArray(repositoryVariables)
121
+ ? repositoryVariables.find(
122
+ /** @param {{ name?: string }} variable */ (variable) =>
123
+ variable.name === 'OPENCODE_SECURITY'
124
+ )
125
+ : undefined
83
126
  const credentialChecks = {
84
127
  npmTokenPresent: secretNames.includes('NPM_TOKEN'),
85
128
  turboTokenPresent: secretNames.includes('TURBO_TOKEN'),
86
129
  turboTeamPresent: Boolean(
87
- ghJson(['variable', 'list', '--repo', repository, '--json', 'name'])?.some(
130
+ repositoryVariables.some(
88
131
  /** @param {{ name?: string }} variable */ (variable) => variable.name === 'TURBO_TEAM'
89
132
  )
90
133
  ),
@@ -103,18 +146,15 @@ export function doctorGithub(root) {
103
146
  'turbo_remote is enabled but TURBO_TOKEN and/or TURBO_TEAM is not configured; remote caching will be skipped.'
104
147
  )
105
148
  if (
106
- ['true', 'auto'].includes(config.opencode_security ?? 'false') &&
149
+ String(opencodeSecurityVariable?.value ?? 'false').toLowerCase() === 'true' &&
107
150
  !credentialChecks.opencodeApiKeyPresent
108
151
  )
109
152
  warnings.push(
110
- 'opencode_security is enabled but OPENCODE_API_KEY is not configured; the optional scan will be skipped.'
153
+ 'OPENCODE_SECURITY is true but OPENCODE_API_KEY is not configured; the optional scan will be skipped.'
111
154
  )
112
- if (['true', 'auto'].includes(config.opencode_security ?? 'false'))
113
- details.credentials.opencodeSecurityConfigured = true
114
- details.credentials.opencodeSecurityVariable =
115
- ghJson(['variable', 'list', '--repo', repository, '--json', 'name'])?.some(
116
- /** @param {{ name?: string }} variable */ (variable) => variable.name === 'OPENCODE_SECURITY'
117
- ) ?? false
155
+ details.credentials.opencodeSecurityEnabled =
156
+ String(opencodeSecurityVariable?.value ?? 'false').toLowerCase() === 'true'
157
+ details.credentials.opencodeSecurityVariable = Boolean(opencodeSecurityVariable)
118
158
 
119
159
  const prs = ghJson([
120
160
  'pr',