code-foundry 1.9.5 → 1.9.7

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.
@@ -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/AGENTS.md CHANGED
@@ -71,11 +71,11 @@ Ask for clarification when a missing decision would materially change the implem
71
71
  8. Run the broadest applicable validation available.
72
72
  9. Report what changed, exact checks and results, skipped checks with reasons, risks, and remaining work.
73
73
 
74
- For normal feature work, branch from `staging` and target pull requests at `staging`. Treat `main` as the protected release branch. Follow `.github/CONTRIBUTING.md` for the complete internal and external contribution flow.
74
+ For normal feature work, branch from `main` and target pull requests at `main`. Treat `main` as the protected release branch. Follow `.github/CONTRIBUTING.md` for the complete internal and external contribution flow.
75
75
 
76
76
  ## Git workflow and merging
77
77
 
78
- This repository uses the `staging-release` workflow: 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`). Feature PRs land on `staging` with squash merges; promotion and release PRs land on `main` with rebase merges. Re-align `staging` with `main` after a release when needed.
78
+ This repository uses the `direct` workflow: topic branches **squash** directly into `main`, and the Release Please version PR **squashes** into `main` (`release_merge_strategy: squash`). Feature and release PRs land on `main` with squash merges. No integration branch exists; all pull requests target `main`.
79
79
 
80
80
  Merge only with the repository's canonical method. Never merge with `--admin`, never default or auto-select a merge method, and never use a method the branch ruleset does not allow. When in doubt, prefer the merge button's configured method and verify the ruleset after merging. Check `.github/CONTRIBUTING.md` for the complete flow and merge table.
81
81
 
@@ -149,7 +149,7 @@ If a check cannot run, state the exact reason. A skipped check is not a passing
149
149
  ## GitHub workflows and configuration
150
150
 
151
151
  - Keep workflows concise, independently runnable, and safe to re-run.
152
- - Use `push` for `main, staging` and `pull_request` for `staging` unless a workflow has a documented event-specific reason.
152
+ - Use `push` for `main` and `pull_request` for `main` unless a workflow has a documented event-specific reason.
153
153
  - Give workflows clear names and jobs concise names; avoid repeating the workflow name in the job name.
154
154
  - Use per-workflow concurrency groups that cancel superseded runs while allowing independent workflows to run in parallel.
155
155
  - Keep setup language-aware and cache dependency downloads by lockfile; do not cache secrets, `node_modules`, virtual environments, or broad build output without a measured reason.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.9.7](https://github.com/0xPlayerOne/code-foundry/compare/v1.9.6...v1.9.7) (2026-09-08)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **ci:** run full validation on release PRs ([#517](https://github.com/0xPlayerOne/code-foundry/issues/517)) ([01a3555](https://github.com/0xPlayerOne/code-foundry/commit/01a355505d6372dc26e2d917e0e6d566ab25546c))
9
+ * **sync:** remove direct topology staging residue ([#515](https://github.com/0xPlayerOne/code-foundry/issues/515)) ([2765e6a](https://github.com/0xPlayerOne/code-foundry/commit/2765e6a6f66137d37f3d65f8eb8311ba28eeafd7))
10
+
11
+ ## [1.9.6](https://github.com/0xPlayerOne/code-foundry/compare/v1.9.5...v1.9.6) (2026-09-08)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **ci:** finalize direct fleet policy ([#513](https://github.com/0xPlayerOne/code-foundry/issues/513)) ([98c59a7](https://github.com/0xPlayerOne/code-foundry/commit/98c59a713f31d0bc5e3b9893434d89dba3257550))
17
+
3
18
  ## [1.9.5](https://github.com/0xPlayerOne/code-foundry/compare/v1.9.4...v1.9.5) (2026-09-08)
4
19
 
5
20
 
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.7",
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
@@ -539,6 +551,21 @@ function renderWorkflow(content, config, repository, ref, rustCodeql) {
539
551
  // target main, so their callers trigger on main alone.
540
552
  if (!isStagingRelease(config.git_workflow)) {
541
553
  rendered = rendered.replace(/^(\s+branches:)\s*\[main,\s*staging\]\s*$/gm, `$1 [main]`)
554
+ // Direct repositories have no staging branch or release reconciliation,
555
+ // so their generated release caller must not expose the legacy deploy-key
556
+ // secret. Keep it in the staging-release template for repositories that
557
+ // still explicitly select that topology.
558
+ if (workflow === 'release') {
559
+ rendered = rendered.replace(
560
+ /^\s+STAGING_DEPLOY_KEY:\s+\$\{\{\s*secrets\.STAGING_DEPLOY_KEY\s*\}\}\s*\n/m,
561
+ ''
562
+ )
563
+ }
564
+ } else if (workflow === 'release' && !rendered.includes('STAGING_DEPLOY_KEY')) {
565
+ rendered = rendered.replace(
566
+ /^(\s+)CODE_FOUNDRY_TOKEN:\s+\$\{\{\s*secrets\.CODE_FOUNDRY_TOKEN\s*\}\}\s*$/m,
567
+ '$&\n$1STAGING_DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}'
568
+ )
542
569
  }
543
570
  if (workflow === 'draft-pr') {
544
571
  // The draft PR caller states the PR base explicitly so the shared
@@ -645,21 +672,26 @@ function stripDependabotEcosystem(content, ecosystem) {
645
672
  * @returns {string}
646
673
  */
647
674
  export function renderContributionDocs(content, file, config) {
648
- if (isStagingRelease(config.git_workflow)) {
649
- if (configured(config.staging_validation_mode, 'fast') !== 'audit') return content
650
- return content.replace(
651
- 'Fast validation: CI plus unit tests, ending in `Validation / Gate`',
652
- 'Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate`'
653
- )
654
- }
655
675
  const replacements = DIRECT_DOC_REPLACEMENTS[file]
656
676
  if (!replacements) return content
657
677
  let rendered = content
658
- for (const [from, to] of replacements) {
659
- if (!rendered.includes(from)) {
660
- throw new Error(`Missing direct-workflow template marker in ${file}: ${JSON.stringify(from)}`)
678
+ const stagingRelease = isStagingRelease(config.git_workflow)
679
+ for (const [staging, direct] of replacements) {
680
+ const from = stagingRelease ? direct : staging
681
+ const to = stagingRelease ? staging : direct
682
+ if (rendered.includes(from)) {
683
+ rendered = rendered.replace(from, to)
684
+ } else if (!rendered.includes(to)) {
685
+ throw new Error(
686
+ `Missing ${stagingRelease ? 'staging-release' : 'direct'} workflow template marker in ${file}: ${JSON.stringify(from)}`
687
+ )
661
688
  }
662
- rendered = rendered.replace(from, to)
689
+ }
690
+ if (stagingRelease && configured(config.staging_validation_mode, 'fast') === 'audit') {
691
+ rendered = rendered.replace(
692
+ 'Fast validation: CI plus unit tests, ending in `Validation / Gate`',
693
+ 'Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate`'
694
+ )
663
695
  }
664
696
  return rendered
665
697
  }
@@ -695,7 +727,7 @@ const DIRECT_DOC_REPLACEMENTS = {
695
727
  ],
696
728
  [
697
729
  '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.',
730
+ '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
731
  ],
700
732
  [
701
733
  'git switch staging\ngit pull --ff-only origin staging',
@@ -717,11 +749,15 @@ const DIRECT_DOC_REPLACEMENTS = {
717
749
  ],
718
750
  [
719
751
  '| Event | Expected automation |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------- |\n| Draft pull request targeting `staging` | No runner-heavy validation; run local checks before requesting review |\n| Ready pull request targeting `staging` | Fast validation: CI plus unit tests, ending in `Validation / Gate` |\n| Draft ordinary pull request targeting `main` | No runner-heavy validation; run local checks before requesting review |\n| Ready ordinary pull request targeting `main` | Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Exact Release Please pull request targeting `main` | Release-policy validation only, ending in `Validation / Gate` |\n| Scheduled or manual validation | Full audit tier |\n| Push to a working branch | Draft PR workflow |\n| Push to `staging` | Promotion PR workflow; canonical validation waits for the PR event |\n| Push to `main` | Release workflow; canonical validation already ran on the merged PR |\n',
720
- '| Event | Expected automation |\n|------------------------------------------------------------------------------------------------------------------------------------------------|\n| Draft pull request targeting `main` | No runner-heavy validation; run local checks before requesting review |\n| Ready pull request targeting `main` | Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Exact Release Please pull request targeting `main` | Release-policy validation only, ending in `Validation / Gate` |\n| Scheduled or manual validation | Full audit tier |\n| Push to a working branch | Draft PR workflow |\n| Push to `main` | Release workflow; canonical validation already ran on the merged PR |',
752
+ '| Event | Expected automation |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------- |\n| Draft pull request targeting `main` | No runner-heavy validation; run local checks before requesting review |\n| Ready pull request targeting `main` | Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Exact Release Please pull request targeting `main` | Release-policy validation only, ending in `Validation / Gate` |\n| Scheduled or manual validation | Full audit tier |\n| Push to a working branch | Draft PR workflow |\n| Push to `main` | Release workflow; canonical validation already ran on the merged PR |\n',
721
753
  ],
722
754
  [
723
755
  '| 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
- '| 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 |',
756
+ '| 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 |\n',
757
+ ],
758
+ [
759
+ '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.',
760
+ '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.',
725
761
  ],
726
762
  ['1. Create a focused branch from `staging`.', '1. Create a focused branch from `main`.'],
727
763
  ],
@@ -1149,7 +1185,7 @@ function createDefaultConfig(root, source, configuredWorkflow) {
1149
1185
  runtime_ref: `v${readPackageVersion(source)}`,
1150
1186
  ...runners,
1151
1187
  toolchain: 'auto',
1152
- staging_validation_mode: 'fast',
1188
+ ...(stagingRelease ? { staging_validation_mode: 'fast' } : {}),
1153
1189
  performance: 'auto',
1154
1190
  performance_command: '',
1155
1191
  performance_profile: '',
@@ -1164,7 +1200,6 @@ function createDefaultConfig(root, source, configuredWorkflow) {
1164
1200
  post_release: 'false',
1165
1201
  post_release_workflow: '',
1166
1202
  post_release_mode: 'auto',
1167
- opencode_security: 'false',
1168
1203
  opencode_security_model: '',
1169
1204
  sync_mode: 'overlay',
1170
1205
  custom_workflows: 'preserve',
@@ -1181,6 +1216,20 @@ function renderConfig(config) {
1181
1216
  .map(([key, value]) => renderConfigLine(key, value))
1182
1217
  .join('\n')}\n`
1183
1218
  }
1219
+
1220
+ /** @param {string} content @param {string[]} keys */
1221
+ function removeConfigKeys(content, keys) {
1222
+ if (!keys.length) return content
1223
+ const rejected = new Set(keys)
1224
+ return content
1225
+ .split(/\r?\n/)
1226
+ .filter((line) => {
1227
+ const key = line.match(/^([A-Za-z0-9_-]+):/)?.[1]
1228
+ return !key || !rejected.has(key)
1229
+ })
1230
+ .join('\n')
1231
+ }
1232
+
1184
1233
  /** @param {string} key @param {string} value */
1185
1234
  function renderConfigLine(key, value) {
1186
1235
  if (value === '') return `${key}:`