code-foundry 1.43.0 → 1.43.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/workflows/release.yml +20 -3
- package/.github/workflows/release_self-ci.yml +0 -47
- package/CHANGELOG.md +7 -0
- package/docs/CONFIGURATION.md +23 -23
- package/package.json +1 -1
- package/src/commands/sync.mjs +122 -59
|
@@ -33,6 +33,11 @@ on:
|
|
|
33
33
|
required: false
|
|
34
34
|
type: string
|
|
35
35
|
default: release-please-config.json
|
|
36
|
+
prerelease:
|
|
37
|
+
description: Publish a batched draft release as a prerelease; manual hotfixes use false.
|
|
38
|
+
required: false
|
|
39
|
+
type: boolean
|
|
40
|
+
default: false
|
|
36
41
|
defer-publication:
|
|
37
42
|
description: Create a root draft release for an external qualified publisher; skip legacy publication and post-release jobs.
|
|
38
43
|
required: false
|
|
@@ -139,6 +144,7 @@ jobs:
|
|
|
139
144
|
}
|
|
140
145
|
if (releaseType === 'none' || !fs.existsSync('package.json')) npmPublish = 'false'
|
|
141
146
|
let legacyReleaseType = ''
|
|
147
|
+
const batchedPublication = Boolean(config.release_batching_schedule) && config.release_batching_prerelease !== 'false' && process.env.DEFER_PUBLICATION !== 'true'
|
|
142
148
|
if (releaseType !== 'none') {
|
|
143
149
|
const sourcePath = process.env.RELEASE_CONFIG_FILE || 'release-please-config.json'
|
|
144
150
|
if (!/^(?:[A-Za-z0-9_.-]+\/)*[A-Za-z0-9_.-]+\.json$/.test(sourcePath) || sourcePath.split('/').includes('..')) {
|
|
@@ -151,12 +157,12 @@ jobs:
|
|
|
151
157
|
throw new Error('config-file must be a file inside the repository')
|
|
152
158
|
}
|
|
153
159
|
const releaseConfig = JSON.parse(fs.readFileSync(sourcePath, 'utf8'))
|
|
154
|
-
if (process.env.DEFER_PUBLICATION === 'true') {
|
|
160
|
+
if (process.env.DEFER_PUBLICATION === 'true' || batchedPublication) {
|
|
155
161
|
const rootPackage = releaseConfig.packages?.['.']
|
|
156
162
|
if (!rootPackage || Object.keys(releaseConfig.packages).length !== 1 ||
|
|
157
163
|
(rootPackage.draft ?? releaseConfig.draft) !== true ||
|
|
158
164
|
(rootPackage['force-tag-creation'] ?? releaseConfig['force-tag-creation']) !== true) {
|
|
159
|
-
throw new Error('
|
|
165
|
+
throw new Error('Deferred or batched publication requires one root package with draft and force-tag-creation enabled')
|
|
160
166
|
}
|
|
161
167
|
}
|
|
162
168
|
if (!releaseConfig.packages && !releaseConfig['release-type']) {
|
|
@@ -167,7 +173,7 @@ jobs:
|
|
|
167
173
|
}
|
|
168
174
|
fs.appendFileSync(
|
|
169
175
|
process.env.GITHUB_OUTPUT,
|
|
170
|
-
`
|
|
176
|
+
`batched_publication=${batchedPublication}\nrelease_type=${releaseType}\nlegacy_release_type=${legacyReleaseType}\nnpm_publish=${npmPublish}\nrelease_merge_strategy=${releaseMergeStrategy}\n`,
|
|
171
177
|
)
|
|
172
178
|
NODE
|
|
173
179
|
- name: Detect release credentials
|
|
@@ -243,6 +249,17 @@ jobs:
|
|
|
243
249
|
echo "prs_created=$WORKFLOW_PRS_CREATED"
|
|
244
250
|
} >> "$GITHUB_OUTPUT"
|
|
245
251
|
fi
|
|
252
|
+
- name: Publish batched draft with atomic visibility
|
|
253
|
+
if: steps.profile.outputs.batched_publication == 'true' && steps.release.outputs.release_created == 'true'
|
|
254
|
+
env:
|
|
255
|
+
GH_TOKEN: ${{ steps.credentials.outputs.token_source == 'configured' && secrets.CODE_FOUNDRY_TOKEN || github.token }}
|
|
256
|
+
RELEASE_TAG: ${{ steps.release.outputs.tag_name }}
|
|
257
|
+
PRERELEASE: ${{ inputs.prerelease }}
|
|
258
|
+
run: |
|
|
259
|
+
set -euo pipefail
|
|
260
|
+
latest=false
|
|
261
|
+
if [ "$PRERELEASE" != true ]; then latest=true; fi
|
|
262
|
+
gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false --prerelease="$PRERELEASE" --latest="$latest"
|
|
246
263
|
- name: Normalize generated release PR draft state
|
|
247
264
|
if: steps.release.outcome == 'success'
|
|
248
265
|
env:
|
|
@@ -3,12 +3,6 @@ name: Code Foundry Release
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
5
|
branches: [main]
|
|
6
|
-
# Release batching (release_batching_schedule in code-foundry.yml). The
|
|
7
|
-
# window gates the pipeline to the version-PR squash so ordinary merges
|
|
8
|
-
# accumulate; sync strips this block and the mark-prerelease job below for
|
|
9
|
-
# repositories without the config, and rewrites the cron when configured.
|
|
10
|
-
schedule:
|
|
11
|
-
- cron: '53 12 * * *'
|
|
12
6
|
workflow_dispatch:
|
|
13
7
|
inputs:
|
|
14
8
|
release-while-paused:
|
|
@@ -264,44 +258,3 @@ jobs:
|
|
|
264
258
|
billing-pause-bypass: ${{ github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true }}
|
|
265
259
|
secrets:
|
|
266
260
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
267
|
-
|
|
268
|
-
mark-prerelease:
|
|
269
|
-
name: Mark the release a pre-release
|
|
270
|
-
# Release batching (release_batching_schedule): the window's release
|
|
271
|
-
# publishes as a GitHub pre-release so the stable feed (releases/latest)
|
|
272
|
-
# never sees it until a promotion workflow flips the flag. Runs in
|
|
273
|
-
# parallel with the pipeline, not after it: a failed (unqualified) build
|
|
274
|
-
# is exactly when a release must not be stable-visible. A dispatched run
|
|
275
|
-
# has no head commit to parse, so a hand-dispatched release — the hotfix
|
|
276
|
-
# path — stays stable immediately. Sync strips this job for repositories
|
|
277
|
-
# without the batching config.
|
|
278
|
-
if: >-
|
|
279
|
-
(vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)) &&
|
|
280
|
-
github.event_name == 'push' && startsWith(github.event.head_commit.message, 'chore(main): release ')
|
|
281
|
-
runs-on: ubuntu-latest
|
|
282
|
-
timeout-minutes: 25
|
|
283
|
-
permissions:
|
|
284
|
-
contents: write
|
|
285
|
-
steps:
|
|
286
|
-
- name: Flag the freshly published release
|
|
287
|
-
env:
|
|
288
|
-
GH_TOKEN: ${{ github.token }}
|
|
289
|
-
COMMIT_SUBJECT: ${{ github.event.head_commit.message }}
|
|
290
|
-
run: |
|
|
291
|
-
set -euo pipefail
|
|
292
|
-
version="$(sed -n 's/^chore(main): release \([0-9][0-9.]*\).*$/\1/p' <<<"$COMMIT_SUBJECT")"
|
|
293
|
-
if [[ -z "$version" ]]; then
|
|
294
|
-
echo "No release version in the head commit subject; nothing to mark." >&2
|
|
295
|
-
exit 0
|
|
296
|
-
fi
|
|
297
|
-
tag="v${version}"
|
|
298
|
-
for _ in $(seq 1 50); do
|
|
299
|
-
if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
|
300
|
-
gh release edit "$tag" --repo "$GITHUB_REPOSITORY" --prerelease
|
|
301
|
-
echo "Marked $tag as a pre-release."
|
|
302
|
-
exit 0
|
|
303
|
-
fi
|
|
304
|
-
sleep 30
|
|
305
|
-
done
|
|
306
|
-
echo "::error::release $tag did not appear within 25 minutes; it is stable-visible — flip it manually or investigate the release job" >&2
|
|
307
|
-
exit 1
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.43.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.43.0...v1.43.1) (2026-10-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **release:** make batched publication and stable promotion reliable ([#703](https://github.com/0xPlayerOne/code-foundry/issues/703)) ([271731d](https://github.com/0xPlayerOne/code-foundry/commit/271731d8fac3bff42a99b3d5f4a774eccc0a7788))
|
|
9
|
+
|
|
3
10
|
## [1.43.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.42.0...v1.43.0) (2026-10-03)
|
|
4
11
|
|
|
5
12
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -115,29 +115,29 @@ See [Merge queue validation](merge-queues.md) before enabling it.
|
|
|
115
115
|
|
|
116
116
|
## Validation and quality
|
|
117
117
|
|
|
118
|
-
| Key | Values | Purpose
|
|
119
|
-
| ----------------------------- | ---------------------------------------------- |
|
|
120
|
-
| `performance` | `auto`, `true`, `false` | Discover, require, or disable performance checks.
|
|
121
|
-
| `performance_command` | JSON argv array or array of argv arrays | Ordered commands for a non-package performance harness.
|
|
122
|
-
| `performance_profile` | empty or `node-package` | Shared package import, memory, archive, and dependency audit.
|
|
123
|
-
| `performance_budget_file` | repository-relative path | Budget file for `node-package`; defaults to `performance-package-budgets.json`.
|
|
124
|
-
| `eval` | `auto`, `true`, `false` | Discover, require, or disable the deterministic eval tier.
|
|
125
|
-
| `eval_command` | JSON argv array | Explicit harness command when there is no `eval` package script.
|
|
126
|
-
| `eval_report_file` | repository-relative path | Report validated against the eval contract; defaults to `eval-results/result.json`.
|
|
127
|
-
| `eval_budget_file` | repository-relative path | Optional budget file; defaults to `eval-budgets.json`.
|
|
128
|
-
| `eval_runner` | runner label | Runner for the `Validation / Eval` lane; defaults to the repository runner. Browser evals need a Chrome-capable runner such as `ubuntu-latest`.
|
|
129
|
-
| `required_capabilities` | comma-separated task names | Fail closed when a required task or coverage evidence is unavailable.
|
|
130
|
-
| `coverage_enforcement` | `auto`, `required`, `off` | Shared coverage-report policy.
|
|
131
|
-
| `coverage_minimum` | `0`–`100` | Minimum percentage; defaults to `80`.
|
|
132
|
-
| `coverage_metrics` | `lines`, `functions`, `branches`, `statements` | Metrics checked by the coverage gate.
|
|
133
|
-
| `coverage_report` | comma-separated repository paths | Istanbul JSON summary or LCOV evidence files.
|
|
134
|
-
| `filter_<task>` | comma-separated repository path globs | Skip a lane when no changed path matches; see [Lane path filters](#lane-path-filters).
|
|
135
|
-
| `rust_sccache` | `true`, `false` | Install sccache and wrap rustc so compile artifacts survive manifest/lockfile churn.
|
|
136
|
-
| `rust_nextest` | `true`, `false` | Install cargo-nextest and prefer `cargo nextest run` over `cargo test` in Rust test lanes.
|
|
137
|
-
| `e2e_shards` | `1`–`8` | Fan the `Validation / Test / E2E` job into one runner per shard. The repository's `test:e2e`/`e2e` script receives `E2E_SHARD_INDEX` and `E2E_TOTAL_SHARDS` and partitions itself (Playwright takes `--shard=$E2E_SHARD_INDEX/$E2E_TOTAL_SHARDS`); each shard owns a runner-local test database, so shards stay isolated.
|
|
138
|
-
| `release_batching_schedule` | cron
|
|
139
|
-
| `release_batching_prerelease` | `true`, `false` | Defaults to `true`
|
|
140
|
-
| `release_batching_soak_hours` | `1`–`336` | Generate a daily
|
|
118
|
+
| Key | Values | Purpose |
|
|
119
|
+
| ----------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
120
|
+
| `performance` | `auto`, `true`, `false` | Discover, require, or disable performance checks. |
|
|
121
|
+
| `performance_command` | JSON argv array or array of argv arrays | Ordered commands for a non-package performance harness. |
|
|
122
|
+
| `performance_profile` | empty or `node-package` | Shared package import, memory, archive, and dependency audit. |
|
|
123
|
+
| `performance_budget_file` | repository-relative path | Budget file for `node-package`; defaults to `performance-package-budgets.json`. |
|
|
124
|
+
| `eval` | `auto`, `true`, `false` | Discover, require, or disable the deterministic eval tier. |
|
|
125
|
+
| `eval_command` | JSON argv array | Explicit harness command when there is no `eval` package script. |
|
|
126
|
+
| `eval_report_file` | repository-relative path | Report validated against the eval contract; defaults to `eval-results/result.json`. |
|
|
127
|
+
| `eval_budget_file` | repository-relative path | Optional budget file; defaults to `eval-budgets.json`. |
|
|
128
|
+
| `eval_runner` | runner label | Runner for the `Validation / Eval` lane; defaults to the repository runner. Browser evals need a Chrome-capable runner such as `ubuntu-latest`. |
|
|
129
|
+
| `required_capabilities` | comma-separated task names | Fail closed when a required task or coverage evidence is unavailable. |
|
|
130
|
+
| `coverage_enforcement` | `auto`, `required`, `off` | Shared coverage-report policy. |
|
|
131
|
+
| `coverage_minimum` | `0`–`100` | Minimum percentage; defaults to `80`. |
|
|
132
|
+
| `coverage_metrics` | `lines`, `functions`, `branches`, `statements` | Metrics checked by the coverage gate. |
|
|
133
|
+
| `coverage_report` | comma-separated repository paths | Istanbul JSON summary or LCOV evidence files. |
|
|
134
|
+
| `filter_<task>` | comma-separated repository path globs | Skip a lane when no changed path matches; see [Lane path filters](#lane-path-filters). |
|
|
135
|
+
| `rust_sccache` | `true`, `false` | Install sccache and wrap rustc so compile artifacts survive manifest/lockfile churn. |
|
|
136
|
+
| `rust_nextest` | `true`, `false` | Install cargo-nextest and prefer `cargo nextest run` over `cargo test` in Rust test lanes. |
|
|
137
|
+
| `e2e_shards` | `1`–`8` | Fan the `Validation / Test / E2E` job into one runner per shard. The repository's `test:e2e`/`e2e` script receives `E2E_SHARD_INDEX` and `E2E_TOTAL_SHARDS` and partitions itself (Playwright takes `--shard=$E2E_SHARD_INDEX/$E2E_TOTAL_SHARDS`); each shard owns a runner-local test database, so shards stay isolated. |
|
|
138
|
+
| `release_batching_schedule` | five-field numeric cron | Batch Release Please into scheduled windows. Ordinary pushes skip the producer; release-PR squashes and manual dispatches still run. Batched releases are created privately as drafts and published with prerelease visibility atomically. Manual dispatches publish stable hotfixes. Absent, releases publish per merge. Requires a single root Release Please package. |
|
|
139
|
+
| `release_batching_prerelease` | `true`, `false` | Defaults to `true` with a schedule. Sync enables `draft` and `force-tag-creation` in Release Please; the producer publishes the draft with the selected visibility. `false` keeps scheduled releases stable-visible. |
|
|
140
|
+
| `release_batching_soak_hours` | `1`–`336` | Generate a daily promoter that promotes the newest prerelease when the oldest candidate since the last stable has soaked this many hours, stitching batch notes. Requires a batching schedule and prerelease publication. Set `STABLE_PROMOTION_HELD=true` in repository Actions variables to hold scheduled and manual promotion; `force` overrides the hold or soak, and `version` selects a plain semantic version. |
|
|
141
141
|
|
|
142
142
|
Supported task capabilities are `format`, `lint`, `type_check`, `build`, `unit`,
|
|
143
143
|
`integration`, `e2e`, `smoke`, `eval`, and `performance`. `coverage` is a policy
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.43.
|
|
3
|
+
"version": "1.43.1",
|
|
4
4
|
"description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
|
|
5
5
|
"homepage": "https://github.com/0xPlayerOne/code-foundry#readme",
|
|
6
6
|
"bugs": {
|
package/src/commands/sync.mjs
CHANGED
|
@@ -135,6 +135,21 @@ function synchronize(options) {
|
|
|
135
135
|
const changed = []
|
|
136
136
|
if (!Object.keys(existingConfig).length && !options.init)
|
|
137
137
|
throw new Error('Missing .github/code-foundry.yml; run init first.')
|
|
138
|
+
validateReleaseBatchingConfig(existingConfig)
|
|
139
|
+
if (
|
|
140
|
+
existingConfig.release_batching_schedule &&
|
|
141
|
+
existingConfig.release_batching_prerelease !== 'false'
|
|
142
|
+
) {
|
|
143
|
+
const releasePath = join(target, 'release-please-config.json')
|
|
144
|
+
if (existsSync(releasePath)) {
|
|
145
|
+
const packages = JSON.parse(readFileSync(releasePath, 'utf8')).packages
|
|
146
|
+
if (packages && (Object.keys(packages).length !== 1 || !packages['.'])) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
'Unsupported release_batching_schedule: batched publication requires a single root Release Please package.'
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
138
153
|
const defaults = createDefaultConfig(target, source, existingConfig.git_workflow)
|
|
139
154
|
let config = { ...defaults, ...existingConfig }
|
|
140
155
|
const workflow = gitWorkflow(config.git_workflow)
|
|
@@ -269,7 +284,7 @@ function synchronize(options) {
|
|
|
269
284
|
if (file === '.github/CODEOWNERS' && existsSync(destination)) continue
|
|
270
285
|
let content = readFileSync(sourceFile)
|
|
271
286
|
if (file === 'release-please-config.json') {
|
|
272
|
-
content = Buffer.from(renderReleaseConfig(target, sourceFile))
|
|
287
|
+
content = Buffer.from(renderReleaseConfig(target, sourceFile, config))
|
|
273
288
|
}
|
|
274
289
|
if (file.endsWith('.yml') && file.startsWith('.github/workflows/')) {
|
|
275
290
|
content = Buffer.from(
|
|
@@ -625,9 +640,34 @@ function mergeReleaseConfig(target, sourceFile) {
|
|
|
625
640
|
return buildReleaseConfig(target, merged)
|
|
626
641
|
}
|
|
627
642
|
|
|
628
|
-
/** @param {string} target @param {string} sourceFile @returns {string} */
|
|
629
|
-
function renderReleaseConfig(target, sourceFile) {
|
|
630
|
-
|
|
643
|
+
/** @param {string} target @param {string} sourceFile @param {Record<string, string>} config @returns {string} */
|
|
644
|
+
function renderReleaseConfig(target, sourceFile, config) {
|
|
645
|
+
const merged = mergeReleaseConfig(target, sourceFile)
|
|
646
|
+
if (
|
|
647
|
+
config.release_batching_schedule &&
|
|
648
|
+
configured(config.release_batching_prerelease, 'true') === 'true'
|
|
649
|
+
) {
|
|
650
|
+
// Stage privately so publication can set visibility atomically. A polling
|
|
651
|
+
// flagger cannot prevent clients seeing an initially stable release.
|
|
652
|
+
merged.draft = true
|
|
653
|
+
merged['force-tag-creation'] = true
|
|
654
|
+
for (const value of Object.values(merged.packages ?? {})) {
|
|
655
|
+
value.draft = true
|
|
656
|
+
value['force-tag-creation'] = true
|
|
657
|
+
}
|
|
658
|
+
} else {
|
|
659
|
+
const caller = join(target, '.github/workflows/release.yml')
|
|
660
|
+
const previouslyBatched =
|
|
661
|
+
existsSync(caller) &&
|
|
662
|
+
readFileSync(caller, 'utf8').includes(
|
|
663
|
+
"prerelease: ${{ github.event_name != 'workflow_dispatch' }}"
|
|
664
|
+
)
|
|
665
|
+
if (previouslyBatched) {
|
|
666
|
+
merged.draft = false
|
|
667
|
+
for (const value of Object.values(merged.packages ?? {})) value.draft = false
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return `${JSON.stringify(merged, null, 2)}\n`
|
|
631
671
|
}
|
|
632
672
|
|
|
633
673
|
/** @param {string} file @param {string} languages @param {string} features @param {Record<string, string>} config */
|
|
@@ -711,14 +751,9 @@ on:
|
|
|
711
751
|
required: false
|
|
712
752
|
type: boolean
|
|
713
753
|
default: false
|
|
714
|
-
hold:
|
|
715
|
-
description: Set (true) or clear (false) the promotion hold; nothing promotes on schedule while held.
|
|
716
|
-
required: false
|
|
717
|
-
type: boolean
|
|
718
754
|
|
|
719
755
|
permissions:
|
|
720
756
|
contents: write
|
|
721
|
-
actions: write
|
|
722
757
|
|
|
723
758
|
concurrency:
|
|
724
759
|
group: promote-stable
|
|
@@ -736,29 +771,22 @@ jobs:
|
|
|
736
771
|
SOAK_HOURS: '__SOAK_HOURS__'
|
|
737
772
|
VERSION: \${{ inputs.version }}
|
|
738
773
|
FORCE: \${{ inputs.force }}
|
|
739
|
-
|
|
774
|
+
STABLE_PROMOTION_HELD: \${{ vars.STABLE_PROMOTION_HELD }}
|
|
740
775
|
run: |
|
|
741
776
|
set -euo pipefail
|
|
742
777
|
repo="$GITHUB_REPOSITORY"
|
|
743
778
|
summary="$GITHUB_STEP_SUMMARY"
|
|
744
779
|
|
|
745
|
-
# The
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if gh api "repos/$repo/actions/variables/STABLE_PROMOTION_HELD" >/dev/null 2>&1; then
|
|
750
|
-
gh api -X PATCH "repos/$repo/actions/variables/STABLE_PROMOTION_HELD" -f value="$wanted" >/dev/null
|
|
751
|
-
else
|
|
752
|
-
gh api -X POST "repos/$repo/actions/variables" -f name=STABLE_PROMOTION_HELD -f value="$wanted" >/dev/null
|
|
753
|
-
fi
|
|
754
|
-
echo "Promotion hold set to $wanted." | tee -a "$summary"
|
|
780
|
+
# The repository variable applies equally to scheduled and manual runs.
|
|
781
|
+
if [ "$STABLE_PROMOTION_HELD" = "true" ] && [ "$FORCE" != "true" ]; then
|
|
782
|
+
echo "Stable promotion is held." | tee -a "$summary"
|
|
783
|
+
exit 0
|
|
755
784
|
fi
|
|
756
785
|
|
|
757
|
-
# A manually named version wins over everything but the hold.
|
|
758
786
|
if [ -n "$VERSION" ]; then
|
|
759
|
-
if [ "$
|
|
760
|
-
echo "
|
|
761
|
-
exit
|
|
787
|
+
if ! [[ "$VERSION" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]; then
|
|
788
|
+
echo "::error::version must be a plain semantic version" >&2
|
|
789
|
+
exit 1
|
|
762
790
|
fi
|
|
763
791
|
gh release edit "v$VERSION" --repo "$repo" --prerelease=false --latest
|
|
764
792
|
echo "Promoted v$VERSION to stable by name." | tee -a "$summary"
|
|
@@ -766,22 +794,25 @@ jobs:
|
|
|
766
794
|
fi
|
|
767
795
|
|
|
768
796
|
releases="$(mktemp)"
|
|
769
|
-
|
|
797
|
+
trap 'rm -f "$releases"' EXIT
|
|
798
|
+
gh api --paginate --slurp "repos/$repo/releases?per_page=100" | jq 'add' > "$releases"
|
|
770
799
|
|
|
771
800
|
# The last promoted stable anchors the candidate batch.
|
|
772
801
|
last_stable_published="$(jq -r '
|
|
773
|
-
[ .[] | select(.draft == false and .prerelease == false)
|
|
802
|
+
[ .[] | select(.draft == false and .prerelease == false)
|
|
803
|
+
| select(.tag_name | test("^v[0-9]+[.][0-9]+[.][0-9]+$")) ]
|
|
804
|
+
| sort_by(.published_at) | .[-1].published_at // "null"
|
|
774
805
|
' "$releases")"
|
|
775
806
|
echo "Last stable published: $last_stable_published"
|
|
776
807
|
|
|
777
808
|
# The batch: every non-draft pre-release published since then, with
|
|
778
809
|
# the plain release tag shape (dev builds carry a -dev suffix and
|
|
779
810
|
# live on their own channel forever; they are never promoted).
|
|
780
|
-
batch="$(jq -r '
|
|
811
|
+
batch="$(jq -r --arg last_stable_published "$last_stable_published" '
|
|
781
812
|
[ .[]
|
|
782
813
|
| select(.draft == false and .prerelease == true)
|
|
783
|
-
| select(.tag_name | test("^v[0-9]
|
|
784
|
-
| select(
|
|
814
|
+
| select(.tag_name | test("^v[0-9]+[.][0-9]+[.][0-9]+$"))
|
|
815
|
+
| select($last_stable_published == "null" or .published_at > $last_stable_published)
|
|
785
816
|
| { tag: .tag_name, published_at: .published_at, body: .body } ]
|
|
786
817
|
| sort_by(.published_at)
|
|
787
818
|
' "$releases")"
|
|
@@ -810,6 +841,58 @@ jobs:
|
|
|
810
841
|
echo "Promoted $tip to stable: the batch of $count had soaked \${age_hours}h." | tee -a "$summary"
|
|
811
842
|
`
|
|
812
843
|
|
|
844
|
+
/** @param {Record<string, string>} config */
|
|
845
|
+
function validateReleaseBatchingConfig(config) {
|
|
846
|
+
const schedule = String(config.release_batching_schedule ?? '').trim()
|
|
847
|
+
if (schedule) {
|
|
848
|
+
const limits = [
|
|
849
|
+
[0, 59],
|
|
850
|
+
[0, 23],
|
|
851
|
+
[1, 31],
|
|
852
|
+
[1, 12],
|
|
853
|
+
[0, 6],
|
|
854
|
+
]
|
|
855
|
+
const fields = schedule.split(/\s+/)
|
|
856
|
+
const valid =
|
|
857
|
+
fields.length === 5 &&
|
|
858
|
+
fields.every((field, index) => {
|
|
859
|
+
const [min, max] = limits[index]
|
|
860
|
+
return field.split(',').every((part) => {
|
|
861
|
+
const match = part.match(/^(\*|\d+(?:-\d+)?)(?:\/(\d+))?$/)
|
|
862
|
+
if (!match || (match[2] && (Number(match[2]) < 1 || Number(match[2]) > max))) return false
|
|
863
|
+
if (match[1] === '*') return true
|
|
864
|
+
const [start, end = start] = match[1].split('-').map(Number)
|
|
865
|
+
return start >= min && end <= max && start <= end
|
|
866
|
+
})
|
|
867
|
+
})
|
|
868
|
+
if (!valid)
|
|
869
|
+
throw new Error(
|
|
870
|
+
`Unsupported release_batching_schedule: ${schedule}; use a five-field numeric cron expression.`
|
|
871
|
+
)
|
|
872
|
+
}
|
|
873
|
+
if (
|
|
874
|
+
config.release_batching_prerelease &&
|
|
875
|
+
!['true', 'false'].includes(config.release_batching_prerelease)
|
|
876
|
+
) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
`Unsupported release_batching_prerelease: ${config.release_batching_prerelease}; use true or false.`
|
|
879
|
+
)
|
|
880
|
+
}
|
|
881
|
+
if (config.release_batching_soak_hours) {
|
|
882
|
+
const hours = Number(config.release_batching_soak_hours)
|
|
883
|
+
if (!Number.isInteger(hours) || hours < 1 || hours > 336) {
|
|
884
|
+
throw new Error(
|
|
885
|
+
`Unsupported release_batching_soak_hours: ${config.release_batching_soak_hours}; use an integer between 1 and 336.`
|
|
886
|
+
)
|
|
887
|
+
}
|
|
888
|
+
if (!schedule || config.release_batching_prerelease === 'false') {
|
|
889
|
+
throw new Error(
|
|
890
|
+
'Unsupported release_batching_soak_hours: configure a batching schedule with prerelease publication.'
|
|
891
|
+
)
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
813
896
|
/** Render the batch-soak promoter for a repository, or null when the
|
|
814
897
|
* repository has not configured a soak.
|
|
815
898
|
*
|
|
@@ -923,43 +1006,23 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
|
|
|
923
1006
|
/^ if: github\.ref == 'refs\/heads\/main' && \(vars\.CI_BILLING_PAUSED != 'true' \|\| \(github\.event_name == 'workflow_dispatch' && inputs\['release-while-paused'\] == true\)\)\n/m,
|
|
924
1007
|
" if: vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)\n"
|
|
925
1008
|
)
|
|
926
|
-
//
|
|
927
|
-
// pipeline to the `chore(main): release …` squash so ordinary merges
|
|
928
|
-
// accumulate into the configured window, and the mark-prerelease job
|
|
929
|
-
// flags each window release for the pre-release channel. Without the
|
|
930
|
-
// config the caller renders the classic per-merge form and the batching
|
|
931
|
-
// blocks are stripped. `release_batching_prerelease: false` opts out of
|
|
932
|
-
// the flagging job while keeping the window.
|
|
1009
|
+
// Gate ordinary pushes while allowing the release squash and manual hotfixes.
|
|
933
1010
|
const batchingSchedule = String(config.release_batching_schedule ?? '').trim()
|
|
934
1011
|
if (batchingSchedule) {
|
|
935
|
-
// The minute field must be numeric or a wildcard: that single rule
|
|
936
|
-
// rejects prose ('every morning') while accepting every real cron form
|
|
937
|
-
// the field needs, including lists, steps, and ranges.
|
|
938
|
-
if (
|
|
939
|
-
!/^[\d*]+([/,][\d*]+)*( \S+){0,4}$/.test(batchingSchedule) ||
|
|
940
|
-
batchingSchedule.length > 64
|
|
941
|
-
) {
|
|
942
|
-
throw new Error(
|
|
943
|
-
`Unsupported release_batching_schedule: ${config.release_batching_schedule}; use a cron expression.`
|
|
944
|
-
)
|
|
945
|
-
}
|
|
946
1012
|
rendered = rendered.replace(
|
|
947
|
-
|
|
948
|
-
` - cron: '${batchingSchedule}'\n
|
|
1013
|
+
' workflow_dispatch:',
|
|
1014
|
+
` schedule:\n - cron: '${batchingSchedule}'\n workflow_dispatch:`
|
|
949
1015
|
)
|
|
950
1016
|
rendered = rendered.replace(
|
|
951
1017
|
" if: vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)\n",
|
|
952
|
-
" if: vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true) && (github.event_name != 'push' || startsWith(github.event.head_commit.message, 'chore(main): release '))\n"
|
|
953
|
-
)
|
|
954
|
-
} else {
|
|
955
|
-
rendered = rendered.replace(
|
|
956
|
-
/^ # Release batching \(release_batching_schedule[^\n]*\n( #[^\n]*\n)* schedule:\n - cron: '53 12 \* \* \*'\n/m,
|
|
957
|
-
''
|
|
1018
|
+
" if: >-\n (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)) && (github.event_name != 'push' || startsWith(github.event.head_commit.message, 'chore(main): release '))\n"
|
|
958
1019
|
)
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1020
|
+
if (configured(config.release_batching_prerelease, 'true') === 'true') {
|
|
1021
|
+
rendered = rendered.replace(
|
|
1022
|
+
' git-workflow:',
|
|
1023
|
+
" prerelease: ${{ github.event_name != 'workflow_dispatch' }}\n git-workflow:"
|
|
1024
|
+
)
|
|
1025
|
+
}
|
|
963
1026
|
}
|
|
964
1027
|
rendered = rendered.replace(
|
|
965
1028
|
/(\n secrets:\n CODE_FOUNDRY_TOKEN: \$\{\{ secrets\.CODE_FOUNDRY_TOKEN \}\}\n)/,
|