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.
- package/.github/licenses/APACHE-2.0.txt +201 -0
- package/.github/workflows/draft-pr.yml +7 -2
- package/.github/workflows/draft-pr_self-ci.yml +1 -0
- package/CHANGELOG.md +19 -0
- package/README.md +18 -13
- package/docs/CONFIGURATION.md +26 -3
- package/docs/RELEASES.md +40 -26
- package/docs/WORKFLOWS.md +27 -15
- package/package.json +1 -1
- package/src/commands/doctor.mjs +8 -3
- package/src/commands/fleet.mjs +8 -6
- package/src/commands/sync.mjs +193 -5
- package/src/lib/config.mjs +18 -1
|
@@ -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
|
|
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=
|
|
94
|
+
--field base="${{ inputs.base }}"
|
|
90
95
|
--field head="$BRANCH"
|
|
91
96
|
--field title="$PR_TITLE"
|
|
92
97
|
--field body="@$BODY_FILE"
|
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
|
|
92
|
-
into `
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -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;
|
|
51
|
-
| `merge_strategy` | `rebase` | Promotion merge method for `staging` → `main`; the staging-release topology
|
|
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
|
|
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
|
|
16
|
-
and fix branches land on `staging` with **squash** merges,
|
|
17
|
-
promotion PR merges with **rebase** (`merge_strategy:
|
|
18
|
-
Please version PRs merge with **rebase**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
42
|
-
`
|
|
43
|
-
`
|
|
44
|
-
and `release_merge_strategy
|
|
45
|
-
`code-foundry
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
17
|
-
`main` run the full audit tier, and exact
|
|
18
|
-
`main` run only release policy.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 `
|
|
50
|
-
|
|
|
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 `
|
|
66
|
-
requests
|
|
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 `
|
|
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
package/src/commands/doctor.mjs
CHANGED
|
@@ -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
|
|
82
|
-
if (
|
|
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']
|
package/src/commands/fleet.mjs
CHANGED
|
@@ -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',
|
|
70
|
-
if (fetch.status !== 0) return { path: repository.path, status: 'skipped', reason: fetch.stderr.trim() ||
|
|
71
|
-
const baseline = spawnSync('git', ['-C', repository.path, 'rev-parse',
|
|
72
|
-
if (baseline.status !== 0) return { path: repository.path, status: 'skipped', reason:
|
|
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',
|
|
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' }
|
package/src/commands/sync.mjs
CHANGED
|
@@ -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: '
|
|
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
|
|
package/src/lib/config.mjs
CHANGED
|
@@ -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
|
+
}
|