code-foundry 0.35.0 → 0.36.1
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/dependabot.yml +6 -0
- package/.github/workflows/draft-pr.yml +7 -2
- package/.github/workflows/draft-pr_self-ci.yml +1 -0
- package/CHANGELOG.md +14 -0
- package/README.md +18 -13
- package/docs/CONFIGURATION.md +25 -2
- 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 +156 -3
- package/src/lib/config.mjs +18 -1
package/.github/dependabot.yml
CHANGED
|
@@ -7,6 +7,12 @@ updates:
|
|
|
7
7
|
interval: weekly
|
|
8
8
|
day: sunday
|
|
9
9
|
open-pull-requests-limit: 99
|
|
10
|
+
ignore:
|
|
11
|
+
# Code Foundry runtime pins are sync-managed (runtime_ref in
|
|
12
|
+
# code-foundry.yml). Dependabot must never bump them; a stale
|
|
13
|
+
# github-actions group PR branched before a sync can resurrect
|
|
14
|
+
# legacy caller files when merged.
|
|
15
|
+
- dependency-name: "0xPlayerOne/code-foundry*"
|
|
10
16
|
groups:
|
|
11
17
|
github-actions:
|
|
12
18
|
applies-to: version-updates
|
|
@@ -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,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.36.1](https://github.com/0xPlayerOne/code-foundry/compare/v0.36.0...v0.36.1) (2026-08-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **sync:** prettier-canonical direct doc tables + dependabot ignore for runtime pins ([f4286b6](https://github.com/0xPlayerOne/code-foundry/commit/f4286b62ac06145b250d02ede9421f36df72a344))
|
|
9
|
+
|
|
10
|
+
## [0.36.0](https://github.com/0xPlayerOne/code-foundry/compare/v0.35.0...v0.36.0) (2026-08-07)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* 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))
|
|
16
|
+
|
|
3
17
|
## [0.35.0](https://github.com/0xPlayerOne/code-foundry/compare/v0.34.14...v0.35.0) (2026-08-04)
|
|
4
18
|
|
|
5
19
|
|
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
|
@@ -47,14 +47,37 @@ repository manifests and source
|
|
|
47
47
|
| `release_type` | `node`, `python`, `rust`, `simple`, `none` | Release strategy |
|
|
48
48
|
| `npm_publish` | `true` or `false` | Opt into npm publication |
|
|
49
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` | `staging-release` | Branch/release model;
|
|
51
|
-
| `merge_strategy` | `rebase` | Promotion merge method for `staging` → `main`; the staging-release topology
|
|
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
|
|
|
@@ -96,8 +96,12 @@ export function syncRepository(options) {
|
|
|
96
96
|
if (!['auto', 'native', 'mise'].includes(toolchain)) {
|
|
97
97
|
throw new Error(`Unsupported toolchain: ${toolchain}; use auto, native, or mise.`)
|
|
98
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
|
+
}
|
|
99
103
|
const mergeStrategy = configured(config.merge_strategy, 'rebase')
|
|
100
|
-
if (mergeStrategy !== 'rebase') {
|
|
104
|
+
if (workflow === 'staging-release' && mergeStrategy !== 'rebase') {
|
|
101
105
|
throw new Error(`Unsupported merge_strategy: ${mergeStrategy}; the staging-release topology requires rebase for staging to main promotions.`)
|
|
102
106
|
}
|
|
103
107
|
const releaseMergeStrategy = configured(config.release_merge_strategy, '')
|
|
@@ -141,6 +145,12 @@ export function syncRepository(options) {
|
|
|
141
145
|
if (file.endsWith('.yml') && file.startsWith('.github/workflows/')) {
|
|
142
146
|
content = Buffer.from(renderWorkflow(content.toString('utf8'), config, runtimeRepository, runtimeRef, rustCodeql))
|
|
143
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
|
+
}
|
|
144
154
|
if (file === '.gitignore' && existsSync(destination)) {
|
|
145
155
|
content = Buffer.from(mergeGitignore(content.toString('utf8'), readFileSync(destination, 'utf8')))
|
|
146
156
|
}
|
|
@@ -165,6 +175,18 @@ export function syncRepository(options) {
|
|
|
165
175
|
}
|
|
166
176
|
}
|
|
167
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
|
+
|
|
168
190
|
const releaseManifest = buildReleaseManifest(target, mergeReleaseConfig(target, sourcePath(source, 'release-please-config.json')))
|
|
169
191
|
if (releaseManifest) {
|
|
170
192
|
const manifestPath = join(target, '.release-please-manifest.json')
|
|
@@ -267,6 +289,9 @@ function shouldInclude(file, languages, features, config) {
|
|
|
267
289
|
if (file === '.github/dependabot.yml') return includesValue(features, 'dependabot')
|
|
268
290
|
if (file === '.github/workflows/opencode-security.yml') return ['true', 'auto'].includes(config.opencode_security ?? 'false')
|
|
269
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
|
|
270
295
|
// The tiered validation caller supersedes the legacy ci/test/security/codeql
|
|
271
296
|
// event callers, so legacy feature names keep selecting it.
|
|
272
297
|
if (workflow === 'validation') {
|
|
@@ -318,6 +343,18 @@ function renderWorkflow(content, config, repository, ref, rustCodeql) {
|
|
|
318
343
|
release: config.release_runner ?? config.runner,
|
|
319
344
|
}
|
|
320
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
|
+
}
|
|
321
358
|
const runner = workflow ? runners[workflow] : undefined
|
|
322
359
|
if (runner) rendered = rendered.replace(/^(\s+runner:)\s+.*$/m, `$1 ${runner}`)
|
|
323
360
|
if (workflow === 'test' && config.unit_runner) {
|
|
@@ -348,6 +385,122 @@ function renderWorkflow(content, config, repository, ref, rustCodeql) {
|
|
|
348
385
|
return rendered
|
|
349
386
|
}
|
|
350
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
|
+
'| Branch | Purpose | Contribution rule |\n| -------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------- |\n| `main` | Protected release branch | Merge through the `staging` → `main` release PR. No direct pushes. |\n| `staging` | Integration branch | Target normal pull requests here. Required checks must pass before merge. |\n| `feat/*`, `fix/*`, `chore/*`, `refactor/*`, `docs/*`, `test/*` | Focused work | Branch from `staging`; keep changes small and reviewable. |\n',
|
|
449
|
+
'| Branch | Purpose | Contribution rule |\n| -------------------------------------------------------------- | ------------------------ | ------------------------------------------------------ |\n| `main` | Protected release branch | Merge through pull requests only. No direct pushes. |\n| `feat/*`, `fix/*`, `chore/*`, `refactor/*`, `docs/*`, `test/*` | Focused work | Branch from `main`; keep changes small and reviewable. |\n',
|
|
450
|
+
],
|
|
451
|
+
[
|
|
452
|
+
'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.',
|
|
453
|
+
'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.',
|
|
454
|
+
],
|
|
455
|
+
[
|
|
456
|
+
'git switch staging\ngit pull --ff-only origin staging',
|
|
457
|
+
'git switch main\ngit pull --ff-only origin main',
|
|
458
|
+
],
|
|
459
|
+
[
|
|
460
|
+
'1. Start from an up-to-date `staging` branch.',
|
|
461
|
+
'1. Start from an up-to-date `main` branch.',
|
|
462
|
+
],
|
|
463
|
+
[
|
|
464
|
+
'8. Push the branch and open a pull request into `staging`.',
|
|
465
|
+
'8. Push the branch and open a pull request into `main`.',
|
|
466
|
+
],
|
|
467
|
+
[
|
|
468
|
+
'10. Merge with a squash after required checks pass and the change is ready; feature PRs land on `staging` with squash merges.',
|
|
469
|
+
'10. Merge with a squash after required checks pass and the change is ready; feature PRs land on `main` with squash merges.',
|
|
470
|
+
],
|
|
471
|
+
[
|
|
472
|
+
'3. Branch from the upstream `staging` branch.',
|
|
473
|
+
'3. Branch from the upstream `main` branch.',
|
|
474
|
+
],
|
|
475
|
+
[
|
|
476
|
+
'7. Push to the fork and open a pull request targeting `staging`.',
|
|
477
|
+
'7. Push to the fork and open a pull request targeting `main`.',
|
|
478
|
+
],
|
|
479
|
+
[
|
|
480
|
+
'| Event | Expected automation |\n| --- | --- |\n| Pull request targeting `staging` | Fast validation: CI plus unit tests, ending in `Validation / Gate` |\n| Ordinary pull request targeting `main` | Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Exact Release Please pull request targeting `main` | Release-policy validation only, ending in `Validation / Gate` |\n| Scheduled or manual validation | Full audit tier |\n| Push to a working branch | Draft PR workflow |\n| Push to `staging` | Promotion PR workflow; canonical validation waits for the PR event |\n| Push to `main` | Release workflow; canonical validation already ran on the merged PR |\n',
|
|
481
|
+
'| Event | Expected automation |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------- |\n| Pull request targeting `main` | Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Exact Release Please pull request targeting `main` | Release-policy validation only, ending in `Validation / Gate` |\n| Scheduled or manual validation | Full audit tier |\n| Push to a working branch | Draft PR workflow |\n| Push to `main` | Release workflow; canonical validation already ran on the merged PR |\n',
|
|
482
|
+
],
|
|
483
|
+
[
|
|
484
|
+
'| Change | Target | Merge method | Merge gate |\n| ----------------- | --------- | ------------------------------------------------------- | --------------------------------------------------------- |\n| Working branch | `staging` | Squash | All applicable required checks pass |\n| `staging` → `main` promotion | `main` | Rebase (`merge_strategy`) | Current staging checks, release review, and rollout notes |\n| Release Please version PR | `main` | Rebase (`release_merge_strategy`, fails closed) | Validation gate and release policy pass |\n',
|
|
485
|
+
'| Change | Target | Merge method | Merge gate |\n| ------------------------- | ------ | ----------------------------------------------- | --------------------------------------- |\n| Working branch | `main` | Squash | All applicable required checks pass |\n| Release Please version PR | `main` | Rebase (`release_merge_strategy`, fails closed) | Validation gate and release policy pass |\n',
|
|
486
|
+
],
|
|
487
|
+
[
|
|
488
|
+
'1. Create a focused branch from `staging`.',
|
|
489
|
+
'1. Create a focused branch from `main`.',
|
|
490
|
+
],
|
|
491
|
+
],
|
|
492
|
+
'.github/SECURITY.md': [
|
|
493
|
+
[
|
|
494
|
+
'The latest commit on `staging` receives security patches. Patches are promoted to `main` through the next release cycle.',
|
|
495
|
+
'The latest commit on `main` receives security patches.',
|
|
496
|
+
],
|
|
497
|
+
[
|
|
498
|
+
'| `staging` | ✅ |\n',
|
|
499
|
+
'',
|
|
500
|
+
],
|
|
501
|
+
],
|
|
502
|
+
}
|
|
503
|
+
|
|
351
504
|
/** @param {Record<string,string>} config */
|
|
352
505
|
function validateRustCodeqlConfig(config) {
|
|
353
506
|
const threads = configured(config.codeql_rust_threads, '1')
|
|
@@ -475,7 +628,7 @@ function createDefaultConfig(root, source) {
|
|
|
475
628
|
post_release: 'false', post_release_workflow: '', post_release_mode: 'auto',
|
|
476
629
|
opencode_security: 'false',
|
|
477
630
|
sync_mode: 'overlay', custom_workflows: 'preserve',
|
|
478
|
-
license: existsSync(join(root, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later', git_workflow: '
|
|
631
|
+
license: existsSync(join(root, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later', git_workflow: 'direct', merge_strategy: 'rebase', release_merge_strategy: 'rebase',
|
|
479
632
|
}
|
|
480
633
|
}
|
|
481
634
|
|
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
|
+
}
|