code-foundry 0.34.14 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -8,6 +8,11 @@ on:
8
8
  required: false
9
9
  type: string
10
10
  default: ubuntu-slim
11
+ base:
12
+ description: Base branch for the draft pull request (staging for the staging-release topology, main for the direct topology).
13
+ required: false
14
+ type: string
15
+ default: staging
11
16
  secrets:
12
17
  CODE_FOUNDRY_TOKEN:
13
18
  required: false
@@ -34,7 +39,7 @@ jobs:
34
39
  GH_TOKEN: ${{ github.token }}
35
40
  run: |
36
41
  BRANCH="$HEAD_REF"
37
- EXISTING=$(gh pr list --repo "$GITHUB_REPOSITORY" --limit 1 --base staging --head "$BRANCH" --state open --json number,title 2>/dev/null | node -e 'let d=""; process.stdin.on("data", c => d += c).on("end", () => console.log(JSON.parse(d || "[]").length))')
42
+ EXISTING=$(gh pr list --repo "$GITHUB_REPOSITORY" --limit 1 --base "${{ inputs.base }}" --head "$BRANCH" --state open --json number,title 2>/dev/null | node -e 'let d=""; process.stdin.on("data", c => d += c).on("end", () => console.log(JSON.parse(d || "[]").length))')
38
43
  echo "existing=$EXISTING" >> "$GITHUB_OUTPUT"
39
44
 
40
45
  - name: Create
@@ -86,7 +91,7 @@ jobs:
86
91
  CREATE_ARGS=(
87
92
  "repos/${GITHUB_REPOSITORY}/pulls"
88
93
  --method POST
89
- --field base=staging
94
+ --field base="${{ inputs.base }}"
90
95
  --field head="$BRANCH"
91
96
  --field title="$PR_TITLE"
92
97
  --field body="@$BODY_FILE"
@@ -21,6 +21,7 @@ jobs:
21
21
  uses: ./.github/workflows/draft-pr.yml
22
22
  with:
23
23
  runner: ubuntu-slim
24
+ base: staging
24
25
  secrets:
25
26
  CODE_FOUNDRY_TOKEN: ${{ secrets.CODE_FOUNDRY_TOKEN }}
26
27
  RELEASE_PLEASE_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.36.0](https://github.com/0xPlayerOne/code-foundry/compare/v0.35.0...v0.36.0) (2026-08-07)
4
+
5
+
6
+ ### Features
7
+
8
+ * make staging-release opt-in and direct-to-main the default ([#370](https://github.com/0xPlayerOne/code-foundry/issues/370)) ([7c66507](https://github.com/0xPlayerOne/code-foundry/commit/7c665071769f0b0332e2f47929ee0c19bbfd5af3))
9
+
10
+ ## [0.35.0](https://github.com/0xPlayerOne/code-foundry/compare/v0.34.14...v0.35.0) (2026-08-04)
11
+
12
+
13
+ ### Features
14
+
15
+ * support Apache 2.0 license sync ([538e7e4](https://github.com/0xPlayerOne/code-foundry/commit/538e7e4e85f9ee8c0af3e319f0440fc78e14dae9))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * validate license policy before sync ([5baffa8](https://github.com/0xPlayerOne/code-foundry/commit/5baffa80c59f907fbf27974b43dfa4e063842d71))
21
+
3
22
  ## [0.34.14](https://github.com/0xPlayerOne/code-foundry/compare/v0.34.13...v0.34.14) (2026-08-03)
4
23
 
5
24
 
package/README.md CHANGED
@@ -68,8 +68,8 @@ when present and otherwise uses native language tooling; choose `native` or
68
68
 
69
69
  The standard workflow triggers are:
70
70
 
71
- - Pushes to `main` and `staging`.
72
- - Pull requests targeting `staging`.
71
+ - Pushes to `main` (and `staging` when `git_workflow: staging-release` is configured).
72
+ - Pull requests targeting `main` (and `staging` in the staging-release topology).
73
73
  - Draft PR automation for supported feature/fix branches.
74
74
 
75
75
  Jobs are language-aware and skip irrelevant setup while remaining visible as
@@ -88,20 +88,25 @@ GitHub Code Quality or other paid GitHub features.
88
88
  See [Workflow and CI conventions](docs/WORKFLOWS.md) for triggers, required
89
89
  checks, runners, coverage, caching, and custom workflow extensions.
90
90
 
91
- The contribution policy uses the `staging-release` workflow: feature PRs squash
92
- into `staging`, the promotion PR rebases into `main` (`merge_strategy: rebase`),
93
- and Release Please version PRs rebase into `main` (`release_merge_strategy:
94
- rebase`). Release automation never defaults to a merge method and never merges
95
- with `--admin`; `code-foundry doctor` and `code-foundry sync` fail closed on
96
- any other strategy. GitHub Stacks is not part of this topology and does not
97
- reduce the required workflow runs.
91
+ The contribution policy defaults to the `direct` workflow: feature PRs squash
92
+ into `main`, and Release Please version PRs rebase into `main`
93
+ (`release_merge_strategy: rebase`). Repositories with a preview/staging
94
+ environment opt into `git_workflow: staging-release`, where feature PRs squash
95
+ into `staging`, the promotion PR rebases into `main` (`merge_strategy:
96
+ rebase`), and Release Please version PRs rebase into `main`. Release automation
97
+ never defaults to a merge method and never merges with `--admin`;
98
+ `code-foundry doctor` and `code-foundry sync` fail closed on any other
99
+ strategy. GitHub Stacks is not part of this topology and does not reduce the
100
+ required workflow runs.
98
101
 
99
102
  ## Releases and publishing
100
103
 
101
- The standard flow promotes `staging` into `main`, lets Release Please open a
102
- versioned release PR, and creates a GitHub release after that PR is merged.
103
- npm publication is opt-in through `npm_publish: true` and supports npm trusted
104
- publishing or an `NPM_TOKEN` fallback.
104
+ In the default `direct` flow, changes reach `main` through feature pull
105
+ requests and Release Please opens a versioned release PR against `main`. In
106
+ the `staging-release` flow, a promotion PR promotes `staging` into `main`
107
+ first. Either way, Release Please creates a GitHub release after the version
108
+ PR is merged. npm publication is opt-in through `npm_publish: true` and
109
+ supports npm trusted publishing or an `NPM_TOKEN` fallback.
105
110
 
106
111
  Read [Release management](docs/RELEASES.md) and
107
112
  [Publishing packages](docs/PUBLISHING.md) before enabling automated
@@ -46,15 +46,38 @@ repository manifests and source
46
46
  | `runtime_ref` | tag or branch | Reusable workflow version |
47
47
  | `release_type` | `node`, `python`, `rust`, `simple`, `none` | Release strategy |
48
48
  | `npm_publish` | `true` or `false` | Opt into npm publication |
49
- | `license` | `gpl-3.0-or-later`, `agpl-3.0-or-later`, `mit`, `preserve`, `none` | License policy; new repositories default to GPLv3 |
50
- | `git_workflow` | `staging-release` | Branch/release model; the standard model promotes `staging` into `main` |
51
- | `merge_strategy` | `rebase` | Promotion merge method for `staging` → `main`; the staging-release topology requires rebase |
49
+ | `license` | `gpl-3.0-or-later`, `agpl-3.0-or-later`, `apache-2.0`, `mit`, `preserve`, `none` | License policy; new repositories default to GPLv3 |
50
+ | `git_workflow` | `direct` (default), `staging-release` | Branch/release model; `direct` opens feature branches into `main`, `staging-release` promotes `staging` into `main` |
51
+ | `merge_strategy` | `rebase` | Promotion merge method for `staging` → `main`; only enforced by the `staging-release` topology |
52
52
  | `release_merge_strategy` | `rebase` | Merge method for Release Please version PRs into `main`; release automation fails closed unless rebase |
53
53
  | `runner` fields | GitHub runner names | Per-workflow runner policy |
54
54
 
55
55
  Supported features are `ci`, `codeql`, `security`, `test`, `draft-pr`,
56
56
  `release-pr`, `release`, and `dependabot`.
57
57
 
58
+ ## Git workflow
59
+
60
+ `git_workflow` selects the branch topology:
61
+
62
+ - `direct` (default): feature branches open pull requests directly into
63
+ `main`. Validation and security scans run on every PR. No `staging` branch
64
+ exists, no promotion caller is generated, and `merge_strategy` is not
65
+ enforced. Dependabot updates target `main`. This is the right choice when a
66
+ repository has no preview or staging environment.
67
+ - `staging-release` (opt-in): feature branches squash into `staging`, a
68
+ promotion PR rebases validated changes into `main` (`merge_strategy:
69
+ rebase`), and Release Please version PRs rebase into `main`
70
+ (`release_merge_strategy: rebase`). Choose this only when the repository
71
+ maintains a preview/staging environment that needs validated integration
72
+ before release.
73
+
74
+ ```yaml
75
+ # A repository with a preview/staging environment
76
+ git_workflow: staging-release
77
+ ```
78
+
79
+ Any other value is rejected by `code-foundry sync` and `code-foundry doctor`.
80
+
58
81
  ## Editing workflow
59
82
 
60
83
  `init` creates the file and renders the baseline. `sync` reads the file and
package/docs/RELEASES.md CHANGED
@@ -2,7 +2,14 @@
2
2
 
3
3
  ## Branch flow
4
4
 
5
- The standard environment flow is:
5
+ The default `direct` flow is:
6
+
7
+ ```text
8
+ topic branch -> main -> versioned release
9
+ ```
10
+
11
+ Repositories that maintain a preview/staging environment opt into the
12
+ `staging-release` flow:
6
13
 
7
14
  ```text
8
15
  topic branch -> staging -> main -> versioned release
@@ -12,13 +19,15 @@ Keep commits Conventional Commit-shaped (`feat:`, `fix:`, `docs:`, `ci:`,
12
19
  `chore:`, and so on). Release Please uses them to select patch/minor/major
13
20
  versions and generate grouped changelog notes.
14
21
 
15
- The merge audit pins one merge method per transition in this topology. Feature
16
- and fix branches land on `staging` with **squash** merges, the `staging` → `main`
17
- promotion PR merges with **rebase** (`merge_strategy: rebase`), and Release
18
- Please version PRs merge with **rebase** (`release_merge_strategy: rebase`).
19
- Release automation never defaults to a merge method and never merges with
20
- `--admin`: the release workflow fails closed unless `release_merge_strategy` is
21
- exactly `rebase`. The current supported `git_workflow` is `staging-release`.
22
+ The merge audit pins one merge method per transition. In the `staging-release`
23
+ topology, feature and fix branches land on `staging` with **squash** merges,
24
+ the `staging` → `main` promotion PR merges with **rebase** (`merge_strategy:
25
+ rebase`), and Release Please version PRs merge with **rebase**
26
+ (`release_merge_strategy: rebase`). In the `direct` topology, feature and fix
27
+ branches squash straight into `main` and only Release Please version PRs merge
28
+ with **rebase**; `merge_strategy` is not enforced. Release automation never
29
+ defaults to a merge method and never merges with `--admin`: the release
30
+ workflow fails closed unless `release_merge_strategy` is exactly `rebase`.
22
31
 
23
32
  The release workflow opens or updates a versioned PR after changes reach
24
33
  `main`. Merging that PR updates the changelog, creates the Git tag and GitHub
@@ -34,27 +43,32 @@ Set these values in `.github/code-foundry.yml`:
34
43
  ```yaml
35
44
  release_type: auto # auto, node, python, rust, simple, or none
36
45
  npm_publish: false # true only for an npm package
37
- merge_strategy: rebase # required: staging -> main promotion PRs rebase
46
+ git_workflow: direct # direct (default) or staging-release
47
+ merge_strategy: rebase # staging-release only: staging -> main promotion PRs rebase
38
48
  release_merge_strategy: rebase # required: Release Please version PRs rebase only
39
49
  ```
40
50
 
41
- `merge_strategy` applies to promotion PRs (`staging` into `main`) and
42
- `release_merge_strategy` to Release Please version PRs; feature PRs into
43
- `staging` use squash merges. The topology requires `merge_strategy: rebase`
44
- and `release_merge_strategy: rebase`; `code-foundry doctor` and
45
- `code-foundry sync` reject any other value, and the release workflow fails
46
- closed instead of falling back to `merge`. Both keep `main` fully linear,
47
- which is what makes the post-release reconciliation possible: release-only
48
- main commits cannot be discarded because they are allowed metadata-only, and
49
- all non-metadata drift is rejected before mutation.
50
-
51
- Patch-equivalent divergence between `main` and `staging` is treated as aligned.
52
- When `staging` has pending commits that are not yet represented on `main`, the
53
- release workflow replays those staging-only commits in order onto a detached
54
- worktree rooted at `main`, and then updates `staging` with an exact
51
+ `git_workflow: staging-release` is opt-in; without it, repositories use the
52
+ `direct` flow and no promotion PR exists. When the staging-release topology is
53
+ selected, `merge_strategy` applies to promotion PRs (`staging` into `main`)
54
+ and `release_merge_strategy` to Release Please version PRs; feature PRs into
55
+ `staging` use squash merges. `code-foundry doctor` and `code-foundry sync`
56
+ reject any non-`rebase` `merge_strategy` only when `staging-release` is
57
+ configured, and always reject a non-`rebase` `release_merge_strategy`; the
58
+ release workflow fails closed instead of falling back to `merge`. Both keep
59
+ `main` fully linear, which is what makes the post-release reconciliation
60
+ possible: release-only main commits cannot be discarded because they are
61
+ allowed metadata-only, and all non-metadata drift is rejected before mutation.
62
+
63
+ The `staging` `main` reconciliation exists only in the `staging-release`
64
+ topology. Patch-equivalent divergence between `main` and `staging` is treated
65
+ as aligned. When `staging` has pending commits that are not yet represented on
66
+ `main`, the release workflow replays those staging-only commits in order onto a
67
+ detached worktree rooted at `main`, and then updates `staging` with an exact
55
68
  `--force-with-lease` to prevent
56
69
  unintended branch rewrites. There is no unconditional mirror force-push and no
57
- fallback synchronization commit path.
70
+ fallback synchronization commit path. `direct` repositories skip this step
71
+ entirely.
58
72
 
59
73
  `auto` selects a supported manifest. Use `simple` with `version.txt` for a
60
74
  repository without a package manifest and `none` for a repository that should
@@ -101,8 +115,8 @@ already passed).
101
115
 
102
116
  ## Operational checklist
103
117
 
104
- 1. Merge tested changes from `staging` into `main`.
118
+ 1. Merge tested changes into `main` (direct: feature PRs; staging-release: promote `staging` into `main`).
105
119
  2. Review the generated Release Please PR and changelog.
106
120
  3. Merge the release PR with the repository's configured `release_merge_strategy` (**rebase**; the release workflow fails closed on any other value).
107
121
  4. Confirm the GitHub Release and any package publication.
108
- 5. Synchronize `staging` with the new `main` release commit.
122
+ 5. staging-release only: synchronize `staging` with the new `main` release commit.
package/docs/WORKFLOWS.md CHANGED
@@ -7,18 +7,22 @@ points; it does not run the same suites again on branch pushes:
7
7
 
8
8
  ```yaml
9
9
  pull_request:
10
- branches: [main, staging]
10
+ branches: [main, staging] # staging-release topology
11
+ # direct topology: branches: [main]
11
12
  schedule:
12
13
  - cron: '31 6 * * 1'
13
14
  workflow_dispatch:
14
15
  ```
15
16
 
16
- Pull requests into `staging` run the fast tier, ordinary pull requests into
17
- `main` run the full audit tier, and exact Release Please pull requests into
18
- `main` run only release policy. Scheduled and manual runs select the audit
19
- tier. Draft PR automation separately listens to supported topic-branch pushes,
20
- promotion automation listens to `staging` pushes, and release automation
21
- listens to `main` pushes.
17
+ In the `staging-release` topology, pull requests into `staging` run the fast
18
+ tier, ordinary pull requests into `main` run the full audit tier, and exact
19
+ Release Please pull requests into `main` run only release policy. In the
20
+ `direct` topology (the default) every pull request targets `main` and runs the
21
+ full audit tier, because there is no integration branch for a fast pass.
22
+ Scheduled and manual runs select the audit tier in both topologies. Draft PR
23
+ automation separately listens to supported topic-branch pushes, promotion
24
+ automation listens to `staging` pushes (staging-release only), and release
25
+ automation listens to `main` pushes.
22
26
  Custom deployment, indexing, search, Slither, or other workflows are
23
27
  repository-owned extensions and should keep their own triggers and permissions.
24
28
 
@@ -31,7 +35,7 @@ repository-owned extensions and should keep their own triggers and permissions.
31
35
  | Security | Profile, audits, and public-only Dependency Review |
32
36
  | CodeQL | GitHub-native code scanning, kept separate from CI |
33
37
  | Draft PR | Create/update development pull requests |
34
- | Release PR | Promote `staging` into `main` |
38
+ | Release PR | Promote `staging` into `main` (staging-release topology only) |
35
39
  | Release | Release Please, GitHub release, and optional npm publication |
36
40
 
37
41
  Use concise job names such as `CI / Format`, `Test / Unit`, and
@@ -46,10 +50,15 @@ workflow refuses to run unless its strategy is exactly `rebase`.
46
50
 
47
51
  | Transition | Merge method | Enforcement |
48
52
  | --- | --- | --- |
49
- | Feature/fix PR into `staging` | Squash | Contribution policy; see `CONTRIBUTING.md` |
50
- | `staging` `main` promotion PR | Rebase (`merge_strategy: rebase`) | `merge_strategy` must be `rebase`; merge commits are rejected |
53
+ | Feature/fix PR into `main` (direct topology) | Squash | Contribution policy; see `CONTRIBUTING.md` |
54
+ | Feature/fix PR into `staging` (staging-release topology) | Squash | Contribution policy; see `CONTRIBUTING.md` |
55
+ | `staging` → `main` promotion PR (staging-release topology) | Rebase (`merge_strategy: rebase`) | `merge_strategy` must be `rebase` when `git_workflow: staging-release`; merge commits are rejected |
51
56
  | Release Please version PR into `main` | Rebase (`release_merge_strategy: rebase`) | Release automation fails closed unless `rebase`; never defaults to `merge`, never uses `--admin` |
52
57
 
58
+ The promotion rows above apply only to `staging-release`; `direct`
59
+ repositories never generate a promotion caller and `merge_strategy` is not
60
+ enforced for them.
61
+
53
62
  Release auto-merge waits for required checks and then polls `mergeStateStatus`
54
63
  until it is `CLEAN`, or `UNSTABLE` with `mergeable` `MERGEABLE`, before
55
64
  merging. Non-required checks that branch policy does not require (for example
@@ -60,10 +69,13 @@ merge policy-blocked. The mergeability poll is bounded and fails closed on
60
69
  conflicts or timeout; releases without an automation token remain manual.
61
70
 
62
71
  Keeping `main` linear — rebase promotions and rebase release PRs — is what
63
- lets the post-release reconciliation fast-forward or replay `staging` safely.
72
+ lets the post-release reconciliation fast-forward or replay `staging` safely
73
+ in the `staging-release` topology. `direct` repositories have no reconciliation
74
+ step: releases merge straight into `main` with `release_merge_strategy: rebase`.
64
75
 
65
- Protect `staging` with the aggregate `Validation / Gate`, squash-only pull
66
- requests, and a single GitHub Actions integration path. That path uses the
76
+ Protect `main` with the aggregate `Validation / Gate` and squash-only pull
77
+ requests. In the `staging-release` topology, protect `staging` the same way
78
+ with a single GitHub Actions integration path. That path uses the
67
79
  GitHub Actions integration token by default, and optionally an SSH deploy key
68
80
  when `STAGING_DEPLOY_KEY` is configured. The deploy key is required only when
69
81
  a personal-repository ruleset for `staging` enforces a Deploy Key bypass for
@@ -99,8 +111,8 @@ GitHub Stacks (stacked pull requests) is not part of this topology and does
99
111
  not reduce required workflow runs. Every pull request in a stack still
100
112
  triggers its own validation run, and each branch keeps its own required
101
113
  checks; stacking never collapses or skips a required check in the tiered
102
- validation gate. Land changes through the standard `staging-release` flow
103
- instead.
114
+ validation gate. Land changes through the standard `direct` or
115
+ `staging-release` flow instead.
104
116
 
105
117
  ## Language defaults
106
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "0.34.14",
3
+ "version": "0.36.0",
4
4
  "description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",
@@ -70,16 +70,21 @@ export function doctor(root, options = {}) {
70
70
  }
71
71
 
72
72
  const features = config.features ?? 'all'
73
+ const workflow = config.git_workflow ?? 'direct'
74
+ if (!['direct', 'staging-release'].includes(workflow)) {
75
+ error(`unsupported git_workflow: ${workflow}; use direct or staging-release`)
76
+ }
73
77
  const mergeStrategy = config.merge_strategy ?? 'rebase'
74
- if (mergeStrategy !== 'rebase') {
78
+ if (workflow === 'staging-release' && mergeStrategy !== 'rebase') {
75
79
  error(`merge_strategy must be "rebase" for the staging-release promotion topology; got "${mergeStrategy}".`)
76
80
  }
77
81
  const releaseMergeStrategy = config.release_merge_strategy ?? ''
78
82
  if (includesValue(features, 'release') && releaseMergeStrategy !== 'rebase') {
79
83
  error(`release_merge_strategy must be "rebase" for automated release merges; got "${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}".`)
80
84
  }
81
- for (const workflow of ['validation', 'draft-pr', 'release-pr', 'release']) {
82
- if (includesValue(features, workflow) && !existsSync(join(target, `.github/workflows/${workflow}.yml`))) error(`missing enabled workflow: ${workflow}.yml`)
85
+ for (const name of ['validation', 'draft-pr', 'release-pr', 'release']) {
86
+ if (name === 'release-pr' && workflow !== 'staging-release') continue
87
+ if (includesValue(features, name) && !existsSync(join(target, `.github/workflows/${name}.yml`))) error(`missing enabled workflow: ${name}.yml`)
83
88
  }
84
89
  const validationEnabled = includesValue(features, 'validation') || ['ci', 'test', 'security', 'codeql'].some((legacy) => includesValue(features, legacy))
85
90
  const validationCaller = ['validation.yml', 'validation_self-ci.yml']
@@ -6,7 +6,7 @@ import { tmpdir } from 'node:os'
6
6
  import { spawnSync } from 'node:child_process'
7
7
  import { syncRepository } from './sync.mjs'
8
8
 
9
- /** @typedef {{ path: string, repository: string, runtimeRef: string, dirty: boolean, configured: boolean }} FleetRepository */
9
+ /** @typedef {{ path: string, repository: string, runtimeRef: string, dirty: boolean, configured: boolean, gitWorkflow: string }} FleetRepository */
10
10
 
11
11
  /** @param {string} root @returns {FleetRepository[]} */
12
12
  export function discoverRepositories(root) {
@@ -24,6 +24,7 @@ export function discoverRepositories(root) {
24
24
  runtimeRef: config.runtime_ref ?? '',
25
25
  dirty: Boolean(git(candidate, ['status', '--porcelain'])),
26
26
  configured,
27
+ gitWorkflow: config.git_workflow ?? 'direct',
27
28
  })
28
29
  }
29
30
  return result.sort((a, b) => a.path.localeCompare(b.path))
@@ -64,12 +65,13 @@ export function upgradeFleet(root, source, options) {
64
65
  /** @param {FleetRepository} repository @param {string} source @param {string} version */
65
66
  function upgradeRepository(repository, source, version) {
66
67
  const branch = `codex/code-foundry-upgrade-${version.replace(/^v/, '')}`
68
+ const base = repository.gitWorkflow === 'staging-release' ? 'staging' : 'main'
67
69
  const temporary = mkdtempSync(join(tmpdir(), 'code-foundry-fleet-'))
68
70
  try {
69
- const fetch = spawnSync('git', ['-C', repository.path, 'fetch', 'origin', 'staging', '--quiet'], { encoding: 'utf8' })
70
- if (fetch.status !== 0) return { path: repository.path, status: 'skipped', reason: fetch.stderr.trim() || 'unable to refresh staging baseline' }
71
- const baseline = spawnSync('git', ['-C', repository.path, 'rev-parse', 'origin/staging'], { encoding: 'utf8' })
72
- if (baseline.status !== 0) return { path: repository.path, status: 'skipped', reason: 'remote staging branch is unavailable' }
71
+ const fetch = spawnSync('git', ['-C', repository.path, 'fetch', 'origin', base, '--quiet'], { encoding: 'utf8' })
72
+ if (fetch.status !== 0) return { path: repository.path, status: 'skipped', reason: fetch.stderr.trim() || `unable to refresh ${base} baseline` }
73
+ const baseline = spawnSync('git', ['-C', repository.path, 'rev-parse', `origin/${base}`], { encoding: 'utf8' })
74
+ if (baseline.status !== 0) return { path: repository.path, status: 'skipped', reason: `remote ${base} branch is unavailable` }
73
75
  const add = spawnSync('git', ['-C', repository.path, 'worktree', 'add', '-b', branch, temporary, baseline.stdout.trim()], { encoding: 'utf8' })
74
76
  if (add.status !== 0) return { path: repository.path, status: 'skipped', reason: add.stderr.trim() || 'unable to create isolated worktree' }
75
77
  const result = syncRepository({ target: temporary, source, force: false })
@@ -90,7 +92,7 @@ function upgradeRepository(repository, source, version) {
90
92
  if (commit.status !== 0) return { path: repository.path, status: 'failed', reason: commit.stderr.trim() || 'commit failed' }
91
93
  const push = spawnSync('git', ['-C', temporary, 'push', '-u', 'origin', branch], { encoding: 'utf8' })
92
94
  if (push.status !== 0) return { path: repository.path, status: 'failed', reason: push.stderr.trim() || 'push failed' }
93
- const pr = spawnSync('gh', ['pr', 'create', '--repo', repository.repository, '--base', 'staging', '--head', branch, '--title', `chore(code-foundry): upgrade to ${version}`, '--body', `Automated isolated Code Foundry runtime upgrade to ${version}.\n\nThe sync preserved protected repository-owned documents and custom workflows.`], { encoding: 'utf8' })
95
+ const pr = spawnSync('gh', ['pr', 'create', '--repo', repository.repository, '--base', base, '--head', branch, '--title', `chore(code-foundry): upgrade to ${version}`, '--body', `Automated isolated Code Foundry runtime upgrade to ${version}.\n\nThe sync preserved protected repository-owned documents and custom workflows.`], { encoding: 'utf8' })
94
96
  return pr.status === 0
95
97
  ? { path: repository.path, status: 'pr-created', branch, pullRequest: pr.stdout.trim() }
96
98
  : { path: repository.path, status: 'failed', branch, reason: pr.stderr.trim() || 'pull request creation failed' }
@@ -4,7 +4,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync }
4
4
  import { dirname, join, resolve } from 'node:path'
5
5
  import { spawnSync } from 'node:child_process'
6
6
  import { detectLanguages, detectPackageManager, detectProfile, recommendRunners } from '../lib/profile.mjs'
7
- import { configured, includesValue, readConfig } from '../lib/config.mjs'
7
+ import { configured, gitWorkflow, includesValue, isStagingRelease, readConfig } from '../lib/config.mjs'
8
8
  import { buildReleaseConfig, buildReleaseManifest } from '../lib/release-manifest.mjs'
9
9
  import { customWorkflowFiles, overlayPolicy } from '../lib/overlay.mjs'
10
10
 
@@ -33,6 +33,14 @@ const protectedFiles = new Set([
33
33
  '.github/PULL_REQUEST_TEMPLATE.md', '.github/SECURITY.md', 'NOTICE',
34
34
  ])
35
35
 
36
+ /** @type {Record<string, string>} */
37
+ const licenseFiles = {
38
+ 'gpl-3.0-or-later': 'GPL-3.0-or-later.txt',
39
+ 'agpl-3.0-or-later': 'AGPL-3.0-or-later.txt',
40
+ 'apache-2.0': 'APACHE-2.0.txt',
41
+ 'mit': 'MIT.txt',
42
+ }
43
+
36
44
  const legacyFiles = [
37
45
  '.github/code-foundry.yml.example', '.github/template.yml', '.github/template.yml.example',
38
46
  '.github/scripts/bootstrap.sh', '.github/scripts/changed-files.sh', '.github/scripts/ci.sh',
@@ -57,6 +65,15 @@ export function syncRepository(options) {
57
65
  if (!Object.keys(existingConfig).length && !options.init) throw new Error('Missing .github/code-foundry.yml; run init first.')
58
66
  const defaults = createDefaultConfig(target, source)
59
67
  let config = { ...defaults, ...existingConfig }
68
+ // Resolve and validate the license policy before any sync writes occur
69
+ // (including config/default additions) so an unsupported policy fails
70
+ // fast without leaving partially generated files behind.
71
+ const license = configured(config.license, existsSync(join(target, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later')
72
+ const licenseFile = licenseFiles[license]
73
+ if (license !== 'preserve' && license !== 'none' && !licenseFile) {
74
+ const supported = [...Object.keys(licenseFiles), 'preserve', 'none'].join(', ')
75
+ throw new Error(`Unsupported license: ${license}; use ${supported}.`)
76
+ }
60
77
  if (!Object.keys(existingConfig).length) {
61
78
  writeOrReport(configPath, renderConfig(config), dryRun)
62
79
  } else {
@@ -79,15 +96,18 @@ export function syncRepository(options) {
79
96
  if (!['auto', 'native', 'mise'].includes(toolchain)) {
80
97
  throw new Error(`Unsupported toolchain: ${toolchain}; use auto, native, or mise.`)
81
98
  }
99
+ const workflow = gitWorkflow(config.git_workflow)
100
+ if (!['direct', 'staging-release'].includes(workflow)) {
101
+ throw new Error(`Unsupported git_workflow: ${workflow}; use direct or staging-release.`)
102
+ }
82
103
  const mergeStrategy = configured(config.merge_strategy, 'rebase')
83
- if (mergeStrategy !== 'rebase') {
104
+ if (workflow === 'staging-release' && mergeStrategy !== 'rebase') {
84
105
  throw new Error(`Unsupported merge_strategy: ${mergeStrategy}; the staging-release topology requires rebase for staging to main promotions.`)
85
106
  }
86
107
  const releaseMergeStrategy = configured(config.release_merge_strategy, '')
87
108
  if (includesValue(features, 'release') && releaseMergeStrategy !== 'rebase') {
88
109
  throw new Error(`Unsupported release_merge_strategy: ${releaseMergeStrategy || '(unset)'}; release automation requires rebase for Release Please version pull requests and never defaults to merge.`)
89
110
  }
90
- const license = configured(config.license, existsSync(join(target, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later')
91
111
  const changed = []
92
112
 
93
113
  // Keep normal semver pins current during sync while preserving intentional
@@ -125,6 +145,12 @@ export function syncRepository(options) {
125
145
  if (file.endsWith('.yml') && file.startsWith('.github/workflows/')) {
126
146
  content = Buffer.from(renderWorkflow(content.toString('utf8'), config, runtimeRepository, runtimeRef, rustCodeql))
127
147
  }
148
+ if (file === '.github/dependabot.yml') {
149
+ content = Buffer.from(renderDependabot(content.toString('utf8'), config))
150
+ }
151
+ if (['AGENTS.md', '.github/CONTRIBUTING.md', '.github/SECURITY.md'].includes(file)) {
152
+ content = Buffer.from(renderContributionDocs(content.toString('utf8'), file, config))
153
+ }
128
154
  if (file === '.gitignore' && existsSync(destination)) {
129
155
  content = Buffer.from(mergeGitignore(content.toString('utf8'), readFileSync(destination, 'utf8')))
130
156
  }
@@ -149,6 +175,18 @@ export function syncRepository(options) {
149
175
  }
150
176
  }
151
177
 
178
+ // A repository that no longer opts into the staging-release topology must
179
+ // not keep a generated staging promotion caller that would otherwise
180
+ // linger dormant (it triggers on pushes to a branch that does not exist).
181
+ if (!isStagingRelease(config.git_workflow)) {
182
+ const promotion = join(target, '.github/workflows/release-pr.yml')
183
+ if (existsSync(promotion) && isGeneratedEventCaller(readFileSync(promotion, 'utf8'), 'release-pr', runtimeRepository)) {
184
+ changed.push('.github/workflows/release-pr.yml')
185
+ if (dryRun) console.log('Would remove generated release-pr caller; the direct topology targets pull requests at main.')
186
+ else rmSync(promotion, { force: true })
187
+ }
188
+ }
189
+
152
190
  const releaseManifest = buildReleaseManifest(target, mergeReleaseConfig(target, sourcePath(source, 'release-please-config.json')))
153
191
  if (releaseManifest) {
154
192
  const manifestPath = join(target, '.release-please-manifest.json')
@@ -171,7 +209,6 @@ export function syncRepository(options) {
171
209
  }
172
210
 
173
211
  if (license !== 'preserve' && license !== 'none') {
174
- const licenseFile = license === 'mit' ? 'MIT.txt' : license === 'agpl-3.0-or-later' ? 'AGPL-3.0-or-later.txt' : 'GPL-3.0-or-later.txt'
175
212
  const sourceLicense = join(source, '.github/licenses', licenseFile)
176
213
  if (!existsSync(sourceLicense)) throw new Error(`License template missing: ${sourceLicense}`)
177
214
  const licenseContent = readFileSync(sourceLicense)
@@ -252,6 +289,9 @@ function shouldInclude(file, languages, features, config) {
252
289
  if (file === '.github/dependabot.yml') return includesValue(features, 'dependabot')
253
290
  if (file === '.github/workflows/opencode-security.yml') return ['true', 'auto'].includes(config.opencode_security ?? 'false')
254
291
  const workflow = file.match(/^\.github\/workflows\/([^/]+)\.yml$/)?.[1]
292
+ // The staging promotion caller only exists in the staging-release topology;
293
+ // direct repositories open feature branches into main and need no promotion.
294
+ if (workflow === 'release-pr' && !isStagingRelease(config.git_workflow)) return false
255
295
  // The tiered validation caller supersedes the legacy ci/test/security/codeql
256
296
  // event callers, so legacy feature names keep selecting it.
257
297
  if (workflow === 'validation') {
@@ -303,6 +343,18 @@ function renderWorkflow(content, config, repository, ref, rustCodeql) {
303
343
  release: config.release_runner ?? config.runner,
304
344
  }
305
345
  const workflow = content.match(/\.github\/workflows\/([^/]+)\.yml/)?.[1]
346
+ // The staging-release topology validates and scans pull requests against
347
+ // both main and the integration branch; direct repositories only ever
348
+ // target main, so their callers trigger on main alone.
349
+ if (!isStagingRelease(config.git_workflow)) {
350
+ rendered = rendered.replace(/^(\s+branches:)\s*\[main,\s*staging\]\s*$/gm, `$1 [main]`)
351
+ }
352
+ if (workflow === 'draft-pr') {
353
+ // The draft PR caller states the PR base explicitly so the shared
354
+ // reusable workflow creates pull requests against the repository's
355
+ // configured integration branch (staging) or main (direct).
356
+ rendered = rendered.replace(/^(\s+base:)\s+.*$/m, `$1 ${isStagingRelease(config.git_workflow) ? 'staging' : 'main'}`)
357
+ }
306
358
  const runner = workflow ? runners[workflow] : undefined
307
359
  if (runner) rendered = rendered.replace(/^(\s+runner:)\s+.*$/m, `$1 ${runner}`)
308
360
  if (workflow === 'test' && config.unit_runner) {
@@ -333,6 +385,142 @@ function renderWorkflow(content, config, repository, ref, rustCodeql) {
333
385
  return rendered
334
386
  }
335
387
 
388
+ /**
389
+ * Dependabot updates land on the repository's integration branch. Direct
390
+ * repositories have no staging branch, so every update targets main.
391
+ * @param {string} content
392
+ * @param {Record<string,string>} config
393
+ * @returns {string}
394
+ */
395
+ function renderDependabot(content, config) {
396
+ if (isStagingRelease(config.git_workflow)) return content
397
+ return content.replaceAll('target-branch: staging', 'target-branch: main')
398
+ }
399
+
400
+ /**
401
+ * Contribution policy documents describe the repository's branch flow. The
402
+ * canonical templates describe the staging-release topology (this runtime
403
+ * itself uses it); direct repositories render the equivalent main-targeting
404
+ * policy. The transformation is exact-string based so any template drift
405
+ * fails loudly (a missed replacement leaves staging prose intact) instead of
406
+ * producing a partial hybrid.
407
+ * @param {string} content
408
+ * @param {string} file
409
+ * @param {Record<string,string>} config
410
+ * @returns {string}
411
+ */
412
+ export function renderContributionDocs(content, file, config) {
413
+ if (isStagingRelease(config.git_workflow)) return content
414
+ const replacements = DIRECT_DOC_REPLACEMENTS[file]
415
+ if (!replacements) return content
416
+ let rendered = content
417
+ for (const [from, to] of replacements) {
418
+ if (!rendered.includes(from)) {
419
+ throw new Error(`Missing direct-workflow template marker in ${file}: ${JSON.stringify(from)}`)
420
+ }
421
+ rendered = rendered.replace(from, to)
422
+ }
423
+ return rendered
424
+ }
425
+
426
+ /** @type {Record<string, Array<[string, string]>>} */
427
+ const DIRECT_DOC_REPLACEMENTS = {
428
+ 'AGENTS.md': [
429
+ [
430
+ 'For normal feature work, branch from `staging` and target pull requests at `staging`. Treat `main` as the protected release branch.',
431
+ 'For normal feature work, branch from `main` and target pull requests at `main`. Treat `main` as the protected release branch.',
432
+ ],
433
+ [
434
+ 'Use `push` for `main, staging` and `pull_request` for `staging` unless a workflow has a documented event-specific reason.',
435
+ 'Use `push` for `main` and `pull_request` for `main` unless a workflow has a documented event-specific reason.',
436
+ ],
437
+ ],
438
+ '.github/CONTRIBUTING.md': [
439
+ [
440
+ '4. Branch from `staging` and target pull requests at `staging`; do not work directly on `main`.',
441
+ '4. Branch from `main` and target pull requests at `main`; do not push directly to `main`.',
442
+ ],
443
+ [
444
+ '```text\n release PR\n ┌──────────────┐\n │ ▼\nfeat/* fix/* chore/* ──PR──▶ staging ──PR──▶ main\ndocs/* test/* refactor/* │ │\n │ └── protected release branch\n └── integration branch\n```',
445
+ '```text\n release PR\n ┌──────────────┐\n │ ▼\nfeat/* fix/* chore/* ──PR──▶ main\ndocs/* test/* refactor/* │\n └── protected release branch\n```',
446
+ ],
447
+ [
448
+ '| `main` | Protected release branch | Merge through the `staging` → `main` release PR. No direct pushes. |',
449
+ '| `main` | Protected release branch | Merge through pull requests only. No direct pushes. |',
450
+ ],
451
+ [
452
+ '| `staging` | Integration branch | Target normal pull requests here. Required checks must pass before merge. |\n',
453
+ '',
454
+ ],
455
+ [
456
+ '| `feat/*`, `fix/*`, `chore/*`, `refactor/*`, `docs/*`, `test/*` | Focused work | Branch from `staging`; keep changes small and reviewable. |',
457
+ '| `feat/*`, `fix/*`, `chore/*`, `refactor/*`, `docs/*`, `test/*` | Focused work | Branch from `main`; keep changes small and reviewable. |',
458
+ ],
459
+ [
460
+ '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.',
461
+ 'The Git workflow is `direct`: topic branches **squash** directly into `main`, 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 release merge strategy. Feature branches never touch `staging`; repositories with a preview/staging environment opt into `git_workflow: staging-release` explicitly.',
462
+ ],
463
+ [
464
+ 'git switch staging\ngit pull --ff-only origin staging',
465
+ 'git switch main\ngit pull --ff-only origin main',
466
+ ],
467
+ [
468
+ '1. Start from an up-to-date `staging` branch.',
469
+ '1. Start from an up-to-date `main` branch.',
470
+ ],
471
+ [
472
+ '8. Push the branch and open a pull request into `staging`.',
473
+ '8. Push the branch and open a pull request into `main`.',
474
+ ],
475
+ [
476
+ '10. Merge with a squash after required checks pass and the change is ready; feature PRs land on `staging` with squash merges.',
477
+ '10. Merge with a squash after required checks pass and the change is ready; feature PRs land on `main` with squash merges.',
478
+ ],
479
+ [
480
+ '3. Branch from the upstream `staging` branch.',
481
+ '3. Branch from the upstream `main` branch.',
482
+ ],
483
+ [
484
+ '7. Push to the fork and open a pull request targeting `staging`.',
485
+ '7. Push to the fork and open a pull request targeting `main`.',
486
+ ],
487
+ [
488
+ '| Pull request targeting `staging` | Fast validation: CI plus unit tests, ending in `Validation / Gate` |\n',
489
+ '',
490
+ ],
491
+ [
492
+ '| Ordinary pull request targeting `main` | Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |',
493
+ '| Pull request targeting `main` | Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |',
494
+ ],
495
+ [
496
+ '| Push to `staging` | Promotion PR workflow; canonical validation waits for the PR event |\n',
497
+ '',
498
+ ],
499
+ [
500
+ '| Working branch | `staging` | Squash | All applicable required checks pass |',
501
+ '| Working branch | `main` | Squash | All applicable required checks pass |',
502
+ ],
503
+ [
504
+ '| `staging` → `main` promotion | `main` | Rebase (`merge_strategy`) | Current staging checks, release review, and rollout notes |\n',
505
+ '',
506
+ ],
507
+ [
508
+ '1. Create a focused branch from `staging`.',
509
+ '1. Create a focused branch from `main`.',
510
+ ],
511
+ ],
512
+ '.github/SECURITY.md': [
513
+ [
514
+ 'The latest commit on `staging` receives security patches. Patches are promoted to `main` through the next release cycle.',
515
+ 'The latest commit on `main` receives security patches.',
516
+ ],
517
+ [
518
+ '| `staging` | ✅ |\n',
519
+ '',
520
+ ],
521
+ ],
522
+ }
523
+
336
524
  /** @param {Record<string,string>} config */
337
525
  function validateRustCodeqlConfig(config) {
338
526
  const threads = configured(config.codeql_rust_threads, '1')
@@ -460,7 +648,7 @@ function createDefaultConfig(root, source) {
460
648
  post_release: 'false', post_release_workflow: '', post_release_mode: 'auto',
461
649
  opencode_security: 'false',
462
650
  sync_mode: 'overlay', custom_workflows: 'preserve',
463
- license: existsSync(join(root, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later', git_workflow: 'staging-release', merge_strategy: 'rebase', release_merge_strategy: 'rebase',
651
+ license: existsSync(join(root, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later', git_workflow: 'direct', merge_strategy: 'rebase', release_merge_strategy: 'rebase',
464
652
  }
465
653
  }
466
654
 
@@ -22,7 +22,7 @@ export function readConfig(file) {
22
22
  return config
23
23
  }
24
24
 
25
- /** @param {string} value @param {string} fallback */
25
+ /** @param {string | undefined} value @param {string} fallback */
26
26
  export function configured(value, fallback) {
27
27
  return value === undefined || value === '' ? fallback : value
28
28
  }
@@ -36,3 +36,20 @@ export function listValue(value) {
36
36
  export function includesValue(value, item) {
37
37
  return value === 'all' || listValue(value).includes(item)
38
38
  }
39
+
40
+ /**
41
+ * Supported Git workflows. `direct` (the default) opens feature branches
42
+ * directly into `main`; `staging-release` is opt-in for repositories that
43
+ * maintain a preview/staging integration branch.
44
+ */
45
+ export const GIT_WORKFLOWS = ['direct', 'staging-release']
46
+
47
+ /** @param {string|undefined} value @returns {string} */
48
+ export function gitWorkflow(value) {
49
+ return configured(value, 'direct')
50
+ }
51
+
52
+ /** @param {string|undefined} value @returns {boolean} */
53
+ export function isStagingRelease(value) {
54
+ return gitWorkflow(value) === 'staging-release'
55
+ }