browser-commander 0.17.0 → 0.17.2
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/.changeset/config.json +1 -0
- package/.jscpd-baseline.json +12 -4
- package/CHANGELOG.md +26 -1
- package/package.json +1 -1
- package/scripts/check-release-format.mjs +80 -0
- package/scripts/create-github-release.mjs +27 -6
- package/scripts/version-and-commit.mjs +25 -2
- package/tests/unit/scripts/cargo-lock-sync.test.js +110 -0
- package/tests/unit/scripts/changelog-collection-order.test.js +144 -0
- package/tests/unit/scripts/changeset-formatter.test.js +108 -0
- package/tests/unit/scripts/check-release-format.test.js +134 -0
- package/tests/unit/scripts/ci-workflow-policy.test.js +103 -0
- package/tests/unit/scripts/command-error-text.test.js +83 -0
- package/tests/unit/scripts/command-stream-errexit.test.js +132 -0
- package/tests/unit/scripts/pre-commit-config.test.js +5 -0
- package/tests/unit/scripts/push-failure-classifier.test.js +153 -0
- package/tests/unit/scripts/push-with-rebase-retry.test.js +302 -0
- package/tests/unit/scripts/release-tags.test.js +96 -0
- package/tests/unit/scripts/required-docs.test.js +145 -0
- package/tests/unit/scripts/rust-changelog.test.js +147 -0
- package/.changeset/merged-loud-river.md +0 -7
- package/deno.json +0 -7
package/.changeset/config.json
CHANGED
package/.jscpd-baseline.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"fingerprints": {
|
|
4
4
|
"00d381dd611ae8ac": 1,
|
|
5
5
|
"029a62ef1f48e856": 1,
|
|
6
|
+
"03241c1f33f40d14": 1,
|
|
6
7
|
"038170e72e0af1f5": 1,
|
|
7
8
|
"04b28d01d0bb47dc": 1,
|
|
8
9
|
"04f605be138bc350": 1,
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
"0caa402c4eae32d8": 1,
|
|
19
20
|
"0fd23be8410c2c95": 1,
|
|
20
21
|
"0fe4f3b5e5e939b3": 1,
|
|
21
|
-
"
|
|
22
|
+
"1019ebbe7f07a5ad": 1,
|
|
22
23
|
"1273365073edd790": 1,
|
|
23
24
|
"1296adc3c3128048": 1,
|
|
24
25
|
"13c4b58d3ea0719b": 1,
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"1ac9fbd87efdb313": 1,
|
|
31
32
|
"1ad4dc7e19a77f68": 1,
|
|
32
33
|
"1c1baf1bc8ea0eae": 1,
|
|
34
|
+
"1cbd39b416a2b94b": 1,
|
|
33
35
|
"1d61f5c49f12d2dc": 1,
|
|
34
36
|
"1ef570563d4e5a08": 1,
|
|
35
37
|
"1f2104c950b12a30": 1,
|
|
@@ -51,6 +53,7 @@
|
|
|
51
53
|
"2c0f8b5c6f28420b": 1,
|
|
52
54
|
"2d2d6fd6c7c0f30d": 1,
|
|
53
55
|
"2e70be24d2268673": 1,
|
|
56
|
+
"2f4c9810eae87d6f": 1,
|
|
54
57
|
"2f9e15467cc2f12a": 1,
|
|
55
58
|
"3101802d3ea65339": 1,
|
|
56
59
|
"33357ce94e07cc4b": 1,
|
|
@@ -60,7 +63,6 @@
|
|
|
60
63
|
"3a9f1672ecc1d55b": 1,
|
|
61
64
|
"3adca52a3f04837c": 1,
|
|
62
65
|
"3c1017def7b3216c": 1,
|
|
63
|
-
"3d7e17ad32108a7f": 1,
|
|
64
66
|
"3da23cbee214a1a5": 1,
|
|
65
67
|
"3dced48baf70eb36": 1,
|
|
66
68
|
"3fa2ebbb416120f0": 1,
|
|
@@ -73,6 +75,7 @@
|
|
|
73
75
|
"46e3f58cfb14a43a": 1,
|
|
74
76
|
"4b0d3cc5c27227e7": 1,
|
|
75
77
|
"4c3b94caa28045d0": 1,
|
|
78
|
+
"4e090d6bb30e6551": 1,
|
|
76
79
|
"4f3c541b3f3e3f23": 1,
|
|
77
80
|
"4fe5663c69a426cb": 1,
|
|
78
81
|
"505d98bd14b63a6c": 1,
|
|
@@ -97,6 +100,7 @@
|
|
|
97
100
|
"5ae9b1430fc38daf": 1,
|
|
98
101
|
"602bfd129bba900e": 1,
|
|
99
102
|
"608e1dd27138ec6f": 1,
|
|
103
|
+
"6098544c77ff17a2": 1,
|
|
100
104
|
"60d0c58a01fc8746": 1,
|
|
101
105
|
"61bcadcd8dc9f63e": 1,
|
|
102
106
|
"63a3b672c8d8b50b": 1,
|
|
@@ -109,6 +113,7 @@
|
|
|
109
113
|
"69d50e6f6a5f2ee9": 1,
|
|
110
114
|
"6a7ed9ad3fc00a39": 1,
|
|
111
115
|
"6af3c307939ef75e": 1,
|
|
116
|
+
"6b35b48512756192": 1,
|
|
112
117
|
"6cd00d6b6a039022": 1,
|
|
113
118
|
"6e2457cd3e1f69e7": 1,
|
|
114
119
|
"6fa62e2642cc73c3": 1,
|
|
@@ -154,6 +159,7 @@
|
|
|
154
159
|
"97c5a112fdde7be8": 1,
|
|
155
160
|
"98cc22079feaa004": 1,
|
|
156
161
|
"990d87f21678fa96": 1,
|
|
162
|
+
"9a72b1c2f22156ca": 1,
|
|
157
163
|
"9b6ef4df04562546": 1,
|
|
158
164
|
"9c055dfd90c369bf": 1,
|
|
159
165
|
"9cb19f1b972e811e": 1,
|
|
@@ -196,6 +202,7 @@
|
|
|
196
202
|
"be0900fb3e87164e": 1,
|
|
197
203
|
"bf452b9431c092ad": 1,
|
|
198
204
|
"c0b027bebd7bd737": 1,
|
|
205
|
+
"c189fe5122a7dc91": 1,
|
|
199
206
|
"c2c155adbcb46f3c": 1,
|
|
200
207
|
"c30737d62069fc02": 1,
|
|
201
208
|
"c3c3569357673af8": 1,
|
|
@@ -203,15 +210,16 @@
|
|
|
203
210
|
"c461a83b51eb7740": 1,
|
|
204
211
|
"c9cf3c28bed33a5b": 1,
|
|
205
212
|
"cbd444e9a6a9072e": 1,
|
|
206
|
-
"cc60029bf62920a3": 1,
|
|
207
213
|
"cd49f39b43d14635": 1,
|
|
208
214
|
"cebff7d1cc87f646": 1,
|
|
209
215
|
"cf6116d195199f8a": 1,
|
|
210
216
|
"cfd68135b744c27b": 1,
|
|
211
217
|
"d097f41a6f43e634": 1,
|
|
218
|
+
"d0da8714c4b7de30": 1,
|
|
212
219
|
"d17201496391f6cc": 1,
|
|
213
220
|
"d17e8db5a2bab966": 1,
|
|
214
221
|
"d1a4a8e6a523b752": 1,
|
|
222
|
+
"d1e5c2961267399a": 1,
|
|
215
223
|
"d2ed3d02dd6a2d8c": 1,
|
|
216
224
|
"d4eec99f58906a63": 1,
|
|
217
225
|
"d6157a86fea41907": 1,
|
|
@@ -233,7 +241,6 @@
|
|
|
233
241
|
"eb9a70c2a21094ff": 1,
|
|
234
242
|
"ebd08637a2d7bcf3": 1,
|
|
235
243
|
"ebe03c5f8c2f5147": 1,
|
|
236
|
-
"edccf5a595ab439f": 1,
|
|
237
244
|
"ee19c8b9b332ee98": 1,
|
|
238
245
|
"eeca0ccf8d821f21": 1,
|
|
239
246
|
"ef2fc2e92ac07c6e": 1,
|
|
@@ -244,6 +251,7 @@
|
|
|
244
251
|
"f46509ed693574df": 1,
|
|
245
252
|
"f4bb6f4c83950fcd": 1,
|
|
246
253
|
"f4bfa7e503443cd1": 1,
|
|
254
|
+
"f6678d6748e9c902": 1,
|
|
247
255
|
"f67a07a611f39cb2": 1,
|
|
248
256
|
"f911821aa7dda47f": 1,
|
|
249
257
|
"f97bf515bfd95829": 1,
|
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 539ae64: Recover a release push that lost the race to `main` instead of failing the run.
|
|
8
|
+
|
|
9
|
+
The `main-writer` concurrency group serialises the three release jobs correctly,
|
|
10
|
+
but `actions/checkout` checks out `github.sha`, so every writer after the first
|
|
11
|
+
holds a tree one commit behind `main` and its push is rejected as
|
|
12
|
+
non-fast-forward. Serialisation buys ordering, not freshness. Every push to
|
|
13
|
+
`main` now rebases and retries — after classifying the rejection, because a
|
|
14
|
+
GH006/GH013 ruleset rejection also prints "rejected" and can never be rebased
|
|
15
|
+
away.
|
|
16
|
+
|
|
17
|
+
## 0.17.1
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 43708ff: Make the release scripts fail when the commands they run fail.
|
|
22
|
+
|
|
23
|
+
`command-stream`'s `$` resolves rather than rejects on a non-zero exit, so every
|
|
24
|
+
`try/catch` around it in the release scripts was dead code. `loadCommandStream()`
|
|
25
|
+
now turns on `errexit`, the GitHub release tags are namespaced per language, and
|
|
26
|
+
the release job checks the formatting of the commit it is about to push.
|
|
27
|
+
|
|
3
28
|
## 0.17.0
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
|
6
31
|
|
|
7
32
|
- Restore the release pipeline. `const { $ } = await use('command-stream')` returned `undefined` on the Node 24 the release jobs pin, because Node 23 added a synthetic `'module.exports'` named export for CommonJS namespaces that use-m's unwrapping does not recognise, so every JS and Rust release died on its first shell call with `TypeError: $ is not a function`. Dependencies now load through `scripts/use-module.mjs`, which detects that namespace shape and unwraps it. Manifest fields are read by TOML table through `scripts/read-manifest.mjs` instead of a line-anchored `grep`, which matched duplicate keys in other tables and wrote two lines into `$GITHUB_OUTPUT`. The duplication gate had `format` set to a reporter name, so jscpd scanned zero files and passed unconditionally; it now scans the tree against a recorded baseline.
|
|
8
|
-
|
|
33
|
+
|
|
9
34
|
Add the `fingerprint` subsystem, which removes the differences between a browser this library controls and one started by hand. `navigator.webdriver` stays false because the `AutomationControlled` Blink feature is disabled at launch, and `launchBrowser()` no longer passes the engine defaults a real Chrome does not carry. `resolveFingerprintProfile()` validates the 19 environment fields a page can read -- user agent and client hints, languages and locale, time zone, platform, cores, memory, screen, viewport, touch, WebGL strings, geolocation and the media preferences -- `createFingerprintPreset()` builds internally consistent Windows, macOS, Linux and Android machines, and `applyFingerprint()` installs a profile over CDP for both Playwright and Puppeteer. `FINGERPRINT_LIMITATIONS` documents what still cannot be made identical, and `relevantFingerprintLimitations()` narrows it to the entries a given profile and browser actually hit. The excluded defaults include Playwright's unconditional `--enable-unsafe-swiftshader`, which would otherwise give an automated browser a SwiftShader WebGL context on a machine where a hand-started Chrome has none.
|
|
10
35
|
|
|
11
36
|
## 0.16.1
|
package/package.json
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard the release commit against the formatting gate that never sees it.
|
|
3
|
+
*
|
|
4
|
+
* `version-and-commit.mjs` pushes the bump with `GITHUB_TOKEN`, and a push
|
|
5
|
+
* authenticated with `GITHUB_TOKEN` does not start a workflow run. Nothing
|
|
6
|
+
* therefore inspects the files the bump generated -- CHANGELOG.md,
|
|
7
|
+
* package.json, package-lock.json. When `changeset version` wrote a
|
|
8
|
+
* CHANGELOG.md that Prettier rejects, main's `format:check` went red and
|
|
9
|
+
* stayed red, because the only runs that could have reported it were the ones
|
|
10
|
+
* the push never triggered.
|
|
11
|
+
*
|
|
12
|
+
* This module has no side effects on import: the shell is passed in, so the
|
|
13
|
+
* behaviour is testable without running a release.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const FORMAT_CHECK_COMMAND = 'npm run format:check';
|
|
17
|
+
|
|
18
|
+
export const FORMAT_FAILURE_TITLE = 'Release commit is not formatted';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the annotation shown when the generated files fail the gate.
|
|
22
|
+
*
|
|
23
|
+
* A workflow command ends at the first newline, so the message is a single
|
|
24
|
+
* line and the captured output is printed separately.
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function formatFailureAnnotation() {
|
|
28
|
+
return (
|
|
29
|
+
`::error title=${FORMAT_FAILURE_TITLE}::The version bump wrote files that ` +
|
|
30
|
+
'Prettier rejects. Pushing them would leave main failing format:check, ' +
|
|
31
|
+
'and the push does not trigger a run that would report it. Fix the ' +
|
|
32
|
+
'generator (see .changeset/config.json "format") rather than reformatting ' +
|
|
33
|
+
'by hand.'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract the reason a command-stream failure carries.
|
|
39
|
+
*
|
|
40
|
+
* With `errexit` on, `error.message` is only "Command failed with exit code
|
|
41
|
+
* N"; the diagnosis is in the captured streams.
|
|
42
|
+
* @param {unknown} error
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function formatFailureDetails(error) {
|
|
46
|
+
const streams = error && typeof error === 'object' ? error : {};
|
|
47
|
+
return [streams.stdout, streams.stderr]
|
|
48
|
+
.map((stream) => String(stream ?? '').trim())
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run the repository's formatting gate over the generated release commit.
|
|
55
|
+
*
|
|
56
|
+
* Rethrows on failure so the release stops before the push.
|
|
57
|
+
* @param {() => Promise<unknown>} runCheck
|
|
58
|
+
* @param {{log?: (message: string) => void, error?: (message: string) => void}} [console_]
|
|
59
|
+
* @returns {Promise<void>}
|
|
60
|
+
*/
|
|
61
|
+
export async function assertGeneratedFilesAreFormatted(
|
|
62
|
+
runCheck,
|
|
63
|
+
console_ = console
|
|
64
|
+
) {
|
|
65
|
+
const log = console_.log ?? (() => {});
|
|
66
|
+
const error = console_.error ?? (() => {});
|
|
67
|
+
|
|
68
|
+
log('Checking that the release commit is formatted...');
|
|
69
|
+
try {
|
|
70
|
+
await runCheck();
|
|
71
|
+
} catch (failure) {
|
|
72
|
+
error(formatFailureAnnotation());
|
|
73
|
+
const details = formatFailureDetails(failure);
|
|
74
|
+
if (details) {
|
|
75
|
+
error(details);
|
|
76
|
+
}
|
|
77
|
+
throw failure;
|
|
78
|
+
}
|
|
79
|
+
log('Release commit is formatted');
|
|
80
|
+
}
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
import { readFileSync } from 'fs';
|
|
16
16
|
|
|
17
|
+
import { releaseTag } from '../../scripts/release-tags.mjs';
|
|
17
18
|
import {
|
|
19
|
+
commandErrorText,
|
|
18
20
|
loadCommandStream,
|
|
19
21
|
loadLinoArguments,
|
|
20
22
|
} from '../../scripts/use-module.mjs';
|
|
@@ -49,7 +51,9 @@ if (!version || !repository) {
|
|
|
49
51
|
process.exit(1);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
// `v<version>` is this package's namespace only; the crate uses `rust-v` and
|
|
55
|
+
// the wheel `python-v`. See scripts/release-tags.mjs.
|
|
56
|
+
const tag = releaseTag('js', version);
|
|
53
57
|
|
|
54
58
|
console.log(`Creating GitHub release for ${tag}...`);
|
|
55
59
|
|
|
@@ -68,7 +72,14 @@ try {
|
|
|
68
72
|
releaseNotes = match[0].replace(`## ${version}`, '').trim();
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
// Falling back to a bare "Release <version>" body is a silent downgrade:
|
|
76
|
+
// the release still goes out and the job still passes, so this annotation is
|
|
77
|
+
// what makes an empty release body visible in the run summary.
|
|
71
78
|
if (!releaseNotes) {
|
|
79
|
+
console.warn(
|
|
80
|
+
`::warning::Version ${version} has no "## ${version}" section in ` +
|
|
81
|
+
'CHANGELOG.md, releasing without notes'
|
|
82
|
+
);
|
|
72
83
|
releaseNotes = `Release ${version}`;
|
|
73
84
|
}
|
|
74
85
|
|
|
@@ -81,11 +92,21 @@ try {
|
|
|
81
92
|
body: releaseNotes,
|
|
82
93
|
});
|
|
83
94
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
try {
|
|
96
|
+
await $`gh api repos/${repository}/releases -X POST --input -`.run({
|
|
97
|
+
stdin: payload,
|
|
98
|
+
});
|
|
99
|
+
console.log(`\u2705 Created GitHub release: ${tag}`);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
// Re-running a release over a tag that already has one is not a failure.
|
|
102
|
+
// The rust script has always tolerated it; the JS one did not need to
|
|
103
|
+
// while a failed `gh` call resolved instead of rejecting.
|
|
104
|
+
if (commandErrorText(error).includes('already exists')) {
|
|
105
|
+
console.log(`Release ${tag} already exists, skipping`);
|
|
106
|
+
} else {
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
89
110
|
} catch (error) {
|
|
90
111
|
console.error('Error creating release:', error.message);
|
|
91
112
|
process.exit(1);
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
|
|
15
15
|
import { readFileSync, appendFileSync, readdirSync } from 'fs';
|
|
16
16
|
|
|
17
|
+
import { assertGeneratedFilesAreFormatted } from './check-release-format.mjs';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
createGitRunner,
|
|
21
|
+
pushWithRebaseRetry,
|
|
22
|
+
} from '../../scripts/push-with-rebase-retry.mjs';
|
|
17
23
|
import {
|
|
18
24
|
loadCommandStream,
|
|
19
25
|
loadLinoArguments,
|
|
@@ -205,6 +211,16 @@ async function main() {
|
|
|
205
211
|
console.log(`New version: ${newVersion}`);
|
|
206
212
|
setOutput('new_version', newVersion);
|
|
207
213
|
|
|
214
|
+
// Re-run the formatting gate over the files the bump just generated.
|
|
215
|
+
//
|
|
216
|
+
// The release commit is pushed with GITHUB_TOKEN, and a GITHUB_TOKEN push
|
|
217
|
+
// does not start a workflow run, so no job ever inspects what the bump
|
|
218
|
+
// wrote. `changeset version` once produced a CHANGELOG.md that Prettier
|
|
219
|
+
// rejects; main's format:check went red and stayed red, because the only
|
|
220
|
+
// runs that could have caught it were the ones the push never triggered.
|
|
221
|
+
// Checking here stops a malformed release commit before it lands.
|
|
222
|
+
await assertGeneratedFilesAreFormatted(() => $`npm run format:check`);
|
|
223
|
+
|
|
208
224
|
// Check if there are changes to commit
|
|
209
225
|
const statusResult = await $`git status --porcelain`.run({ capture: true });
|
|
210
226
|
const status = statusResult.stdout.trim();
|
|
@@ -220,8 +236,15 @@ async function main() {
|
|
|
220
236
|
const escapedMessage = commitMessage.replace(/"/g, '\\"');
|
|
221
237
|
await $`git commit -m "${escapedMessage}"`;
|
|
222
238
|
|
|
223
|
-
// Push directly to main
|
|
224
|
-
|
|
239
|
+
// Push directly to main.
|
|
240
|
+
//
|
|
241
|
+
// The pre-emptive rebase above runs before the version bump, so it
|
|
242
|
+
// cannot cover a writer that lands between the bump and this push.
|
|
243
|
+
// pushWithRebaseRetry closes that window and, just as importantly,
|
|
244
|
+
// distinguishes a lost race from a branch-protection rejection instead
|
|
245
|
+
// of retrying one as if it were the other.
|
|
246
|
+
const git = await createGitRunner();
|
|
247
|
+
await pushWithRebaseRetry({ git, branch: 'main' });
|
|
225
248
|
|
|
226
249
|
console.log('\u2705 Version bump committed and pushed to main');
|
|
227
250
|
setOutput('version_committed', 'true');
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard for the lockfile half of RC-C.
|
|
3
|
+
*
|
|
4
|
+
* `rust/scripts/version-and-commit.mjs` writes the new version into
|
|
5
|
+
* Cargo.toml. Cargo.lock carries the workspace member's own version too, so a
|
|
6
|
+
* bump that touches only the manifest leaves the two disagreeing, and every
|
|
7
|
+
* job that builds the release commit with `cargo build --locked` fails with
|
|
8
|
+
* "the lock file needs to be updated but --locked was passed".
|
|
9
|
+
*
|
|
10
|
+
* The drift was invisible while RC-C was live: the release commit was never
|
|
11
|
+
* pushed, so main kept a Cargo.toml and a Cargo.lock that still agreed.
|
|
12
|
+
* Fixing RC-C is what makes this reachable.
|
|
13
|
+
*
|
|
14
|
+
* Analysis: dev/log/issues/83/pulls/84/analysis/root-causes.md, RC-C
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { describe, it } from 'node:test';
|
|
22
|
+
|
|
23
|
+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read the version of a named package out of a Cargo.lock.
|
|
27
|
+
*
|
|
28
|
+
* The line separator is `\r?\n` because a Windows runner checks the file out
|
|
29
|
+
* with CRLF: `git config core.autocrlf` is `true` on GitHub's windows-latest
|
|
30
|
+
* image, so a pattern anchored on a bare `\n` matches nothing there and the
|
|
31
|
+
* lookup silently returns `undefined` -- a guard that passes on two operating
|
|
32
|
+
* systems and fails on the third for a reason that has nothing to do with what
|
|
33
|
+
* it guards.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} lock
|
|
36
|
+
* @param {string} name
|
|
37
|
+
* @returns {string | undefined}
|
|
38
|
+
*/
|
|
39
|
+
function lockedVersion(lock, name) {
|
|
40
|
+
const pattern = new RegExp(
|
|
41
|
+
`\\[\\[package\\]\\]\\r?\\nname = "${name}"\\r?\\nversion = "([^"]+)"`
|
|
42
|
+
);
|
|
43
|
+
return pattern.exec(lock)?.[1];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('Cargo.lock stays in step with the release bump', () => {
|
|
47
|
+
const cargoToml = readFileSync(join(repoRoot, 'rust/Cargo.toml'), 'utf8');
|
|
48
|
+
const cargoLock = readFileSync(join(repoRoot, 'rust/Cargo.lock'), 'utf8');
|
|
49
|
+
const script = readFileSync(
|
|
50
|
+
join(repoRoot, 'rust/scripts/version-and-commit.mjs'),
|
|
51
|
+
'utf8'
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
it('records the manifest version in the lockfile today', () => {
|
|
55
|
+
const manifestVersion = /^\[package\][\s\S]*?^version = "([^"]+)"/m.exec(
|
|
56
|
+
cargoToml
|
|
57
|
+
)?.[1];
|
|
58
|
+
assert.ok(manifestVersion, 'Cargo.toml has no [package] version');
|
|
59
|
+
assert.equal(
|
|
60
|
+
lockedVersion(cargoLock, 'browser-commander'),
|
|
61
|
+
manifestVersion,
|
|
62
|
+
'a build with --locked would refuse this tree'
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('reads a lockfile that was checked out with CRLF line endings', () => {
|
|
67
|
+
const crlf = cargoLock.replace(/\r?\n/g, '\r\n');
|
|
68
|
+
assert.equal(
|
|
69
|
+
lockedVersion(crlf, 'browser-commander'),
|
|
70
|
+
lockedVersion(cargoLock, 'browser-commander'),
|
|
71
|
+
'the same lockfile must read the same on a Windows checkout'
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('refreshes the lockfile as part of the bump', () => {
|
|
76
|
+
assert.match(
|
|
77
|
+
script,
|
|
78
|
+
/cargo update --workspace/,
|
|
79
|
+
'bumping Cargo.toml without refreshing Cargo.lock breaks --locked builds'
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('refreshes the lockfile after the manifest, not before', () => {
|
|
84
|
+
const bump = script.indexOf('updateCargoToml(newVersion)');
|
|
85
|
+
const refresh = script.indexOf('cargo update --workspace');
|
|
86
|
+
assert.ok(bump > 0 && refresh > 0);
|
|
87
|
+
assert.ok(
|
|
88
|
+
bump < refresh,
|
|
89
|
+
'refreshing before the bump would re-lock the old version'
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('stages the lockfile in the release commit', () => {
|
|
94
|
+
const add = /git add -A ([^`]+)`/.exec(script)?.[1] ?? '';
|
|
95
|
+
assert.ok(
|
|
96
|
+
add.split(/\s+/).includes('Cargo.lock'),
|
|
97
|
+
`Cargo.lock is refreshed but never committed (staged: ${add.trim()})`
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('is what the release job later builds with --locked', () => {
|
|
102
|
+
// If the workflow stopped passing --locked the drift would go unnoticed
|
|
103
|
+
// again, so the guard is only meaningful while this flag is there.
|
|
104
|
+
const workflow = readFileSync(
|
|
105
|
+
join(repoRoot, '.github/workflows/rust.yml'),
|
|
106
|
+
'utf8'
|
|
107
|
+
);
|
|
108
|
+
assert.match(workflow, /cargo build --locked/);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard for RC-I: collecting the changelog before the version bump.
|
|
3
|
+
*
|
|
4
|
+
* Both manual-release jobs used to collect fragments in a workflow step that
|
|
5
|
+
* ran ahead of the bump, and both named the section wrongly as a result:
|
|
6
|
+
*
|
|
7
|
+
* rust.yml `node scripts/collect-changelog.mjs` reads the version out of
|
|
8
|
+
* Cargo.toml, which at that point still held the version that
|
|
9
|
+
* was already released, so the notes were filed under it and
|
|
10
|
+
* the version being released got none.
|
|
11
|
+
* python.yml `scriv collect --version "$BUMP_TYPE"`. `--version` names the
|
|
12
|
+
* new changelog section rather than selecting a bump, so a
|
|
13
|
+
* release wrote a section titled "patch".
|
|
14
|
+
*
|
|
15
|
+
* Either way the release notes went missing: create-github-release looks for
|
|
16
|
+
* the version it is releasing and falls back to a bare "Release <version>".
|
|
17
|
+
*
|
|
18
|
+
* Collection now happens inside the version-and-commit scripts, after the
|
|
19
|
+
* bump has computed the version. These assertions are on the workflows,
|
|
20
|
+
* because that is where the ordering lived.
|
|
21
|
+
*
|
|
22
|
+
* Analysis: dev/log/issues/83/pulls/84/analysis/root-causes.md
|
|
23
|
+
* Reproduction: experiments/scriv-collect-version/run.sh
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import assert from 'node:assert/strict';
|
|
27
|
+
import { readFileSync } from 'node:fs';
|
|
28
|
+
import { dirname, join } from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import { describe, it } from 'node:test';
|
|
31
|
+
|
|
32
|
+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read a workflow with its comment lines removed.
|
|
36
|
+
*
|
|
37
|
+
* The comments explain the very mistakes these assertions look for, so
|
|
38
|
+
* matching against the raw file would find the explanation and report the bug
|
|
39
|
+
* as still present.
|
|
40
|
+
* @param {string} name
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
function workflow(name) {
|
|
44
|
+
return readFileSync(join(repoRoot, '.github/workflows', name), 'utf8')
|
|
45
|
+
.split('\n')
|
|
46
|
+
.filter((line) => !/^\s*#/.test(line))
|
|
47
|
+
.join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('changelog fragments are collected after the version bump', () => {
|
|
51
|
+
it('never passes a bump type where a version name is expected', () => {
|
|
52
|
+
// `scriv collect --version` is documented as "The version name to use for
|
|
53
|
+
// this entry", so "$BUMP_TYPE" lands in the changelog verbatim.
|
|
54
|
+
assert.doesNotMatch(
|
|
55
|
+
workflow('python.yml'),
|
|
56
|
+
/scriv collect --version "\$BUMP_TYPE"/,
|
|
57
|
+
'scriv would head the release section with the word "patch"'
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('has no workflow step that collects ahead of the bump', () => {
|
|
62
|
+
for (const [name, collector] of [
|
|
63
|
+
['rust.yml', 'collect-changelog.mjs'],
|
|
64
|
+
['python.yml', 'scriv collect'],
|
|
65
|
+
]) {
|
|
66
|
+
const content = workflow(name);
|
|
67
|
+
const collect = content.indexOf(collector);
|
|
68
|
+
if (collect < 0) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const bump = content.indexOf('name: Version and commit');
|
|
72
|
+
assert.ok(
|
|
73
|
+
bump < 0 || collect > bump,
|
|
74
|
+
`${name} collects the changelog before the bump names the version`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('collects inside the scripts that know the new version', () => {
|
|
80
|
+
const rust = readFileSync(
|
|
81
|
+
join(repoRoot, 'rust/scripts/version-and-commit.mjs'),
|
|
82
|
+
'utf8'
|
|
83
|
+
);
|
|
84
|
+
assert.match(rust, /updateChangelog\('\.', newVersion, fragments\)/);
|
|
85
|
+
|
|
86
|
+
const python = readFileSync(
|
|
87
|
+
join(repoRoot, 'python/scripts/version_and_commit.py'),
|
|
88
|
+
'utf8'
|
|
89
|
+
);
|
|
90
|
+
assert.match(python, /collect_changelog\(new_version, project_root\)/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('orders collection after the bump inside those scripts', () => {
|
|
94
|
+
const rust = readFileSync(
|
|
95
|
+
join(repoRoot, 'rust/scripts/version-and-commit.mjs'),
|
|
96
|
+
'utf8'
|
|
97
|
+
);
|
|
98
|
+
assert.ok(
|
|
99
|
+
rust.indexOf('const newVersion = await findNextAvailableVersion') <
|
|
100
|
+
rust.indexOf('collectFragments()')
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const python = readFileSync(
|
|
104
|
+
join(repoRoot, 'python/scripts/version_and_commit.py'),
|
|
105
|
+
'utf8'
|
|
106
|
+
);
|
|
107
|
+
assert.ok(
|
|
108
|
+
python.indexOf('new_version = bump_version(') <
|
|
109
|
+
python.indexOf('collect_changelog(new_version')
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('writes headings at the level the release scripts read', () => {
|
|
114
|
+
// scriv defaults to level 1 while create_github_release.py extracts from
|
|
115
|
+
// `## <version>`, so the two could never meet.
|
|
116
|
+
const pyproject = readFileSync(
|
|
117
|
+
join(repoRoot, 'python/pyproject.toml'),
|
|
118
|
+
'utf8'
|
|
119
|
+
);
|
|
120
|
+
assert.match(pyproject, /^md_header_level = "2"$/m);
|
|
121
|
+
|
|
122
|
+
const release = readFileSync(
|
|
123
|
+
join(repoRoot, 'python/scripts/create_github_release.py'),
|
|
124
|
+
'utf8'
|
|
125
|
+
);
|
|
126
|
+
assert.match(release, /rf"\^## \{re\.escape\(version\)\}/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('says so in the run summary when the notes are missing anyway', () => {
|
|
130
|
+
// The fallback body is a silent downgrade: the release still goes out and
|
|
131
|
+
// the job still passes.
|
|
132
|
+
for (const path of [
|
|
133
|
+
'js/scripts/create-github-release.mjs',
|
|
134
|
+
'rust/scripts/create-github-release.mjs',
|
|
135
|
+
'python/scripts/create_github_release.py',
|
|
136
|
+
]) {
|
|
137
|
+
assert.match(
|
|
138
|
+
readFileSync(join(repoRoot, path), 'utf8'),
|
|
139
|
+
/::warning::/,
|
|
140
|
+
`${path} drops the release notes without a word`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|