browser-commander 0.26.0 → 0.26.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/.jscpd-baseline.json +6 -4
- package/CHANGELOG.md +30 -0
- package/package.json +2 -2
- package/scripts/check-duplication.mjs +176 -0
- package/scripts/create-manual-changeset.mjs +3 -1
- package/scripts/format-release-notes.mjs +8 -3
- package/scripts/merge-changesets.mjs +4 -1
- package/scripts/validate-changeset.mjs +23 -10
- package/scripts/version-and-commit.mjs +8 -10
- package/src/browser/webdriver.js +69 -3
- package/src/elements/selectors.js +31 -14
- package/src/parity/harness.js +4 -1
- package/src/tests/index.js +12 -1
- package/tests/e2e/engine-matrix.e2e.test.js +2 -1
- package/tests/e2e/safari.e2e.test.js +2 -1
- package/tests/e2e/webdriver.e2e.test.js +2 -1
- package/tests/helpers/fixture-server.js +18 -1
- package/tests/unit/browser/webdriver.test.js +100 -0
- package/tests/unit/elements/selectors.test.js +75 -0
- package/tests/unit/helpers/fixture-server.test.js +60 -0
- package/tests/unit/playwright-protocol-coverage.test.js +9 -6
- package/tests/unit/puppeteer-bindings-coverage.test.js +28 -7
- package/tests/unit/scripts/bot-commit-attribution.test.js +70 -0
- package/tests/unit/scripts/check-duplication.test.js +139 -0
- package/tests/unit/scripts/check-pipeline-status.test.js +264 -52
- package/tests/unit/scripts/check-version-modification.test.js +107 -1
- package/tests/unit/scripts/ci-timeout-budgets.test.js +15 -2
- package/tests/unit/scripts/ci-workflow-policy.test.js +91 -7
- package/tests/unit/scripts/command-stream-errexit.test.js +83 -41
- package/tests/unit/scripts/e2e-suite-coverage.test.js +174 -0
- package/tests/unit/scripts/github-actions-log.test.js +99 -0
- package/tests/unit/scripts/links-workflow.test.js +42 -0
- package/tests/unit/scripts/preflight-credentials.test.js +555 -0
- package/tests/unit/scripts/run-with-budget-warning.test.js +212 -6
- package/tests/unit/scripts/trusted-publishing.test.js +78 -0
- package/tests/unit/scripts/use-module-integration.test.js +2 -9
- package/tests/unit/scripts/use-module.test.js +20 -0
- package/tests/unit/scripts/validate-changeset.test.js +127 -0
- package/tests/unit/scripts/zizmor-version.test.js +82 -0
- package/tests/unit/tests/index.test.js +43 -0
|
@@ -6,9 +6,17 @@
|
|
|
6
6
|
* `cancelled` as well - run 24045269874 of this repository is one such run on
|
|
7
7
|
* `main`. scripts/check-pipeline-status.sh is the only thing that looks at
|
|
8
8
|
* that, so these tests pin the readings it has to get right: a failure is
|
|
9
|
-
* always an error, a cancellation
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* always an error, and a cancellation is an error unless a newer commit has
|
|
10
|
+
* overtaken the run *and* the cancelled job is one a newer run can cancel
|
|
11
|
+
* (`cancel-in-progress: true`). Anything else - a job that queues instead, a
|
|
12
|
+
* run still at the branch head, a value the gate cannot read - is a timeout or
|
|
13
|
+
* a manual stop that nothing else will report.
|
|
14
|
+
*
|
|
15
|
+
* The gate used to warn about every cancellation off the default branch, so a
|
|
16
|
+
* pull request whose test job hit `timeout-minutes` passed its gate (issue
|
|
17
|
+
* #128), and it excused a cancelled release writer on a superseded `main` run
|
|
18
|
+
* although `cancel-in-progress: false` means no newer run can cancel it.
|
|
19
|
+
*
|
|
12
20
|
* Hosted-runner failures can report an undocumented abandoned result; it and
|
|
13
21
|
* other unexpected/missing results must fail on every branch.
|
|
14
22
|
*
|
|
@@ -16,27 +24,65 @@
|
|
|
16
24
|
*/
|
|
17
25
|
|
|
18
26
|
import assert from 'node:assert/strict';
|
|
19
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
27
|
+
import { mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
20
28
|
import { tmpdir } from 'node:os';
|
|
21
29
|
import path from 'node:path';
|
|
22
30
|
import { spawnSync } from 'node:child_process';
|
|
23
|
-
import { describe, it } from 'node:test';
|
|
31
|
+
import { after, before, describe, it } from 'node:test';
|
|
24
32
|
|
|
33
|
+
import { readCancelInProgress } from '../../../../scripts/read-job-cancel-in-progress.mjs';
|
|
25
34
|
import {
|
|
26
35
|
BASH_AVAILABLE,
|
|
27
36
|
REPO_ROOT,
|
|
37
|
+
readRepoText,
|
|
28
38
|
repoPath,
|
|
29
39
|
runBashScript,
|
|
30
40
|
} from '../../helpers/repo.js';
|
|
31
41
|
|
|
32
42
|
const SCRIPT = repoPath('scripts/check-pipeline-status.sh');
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
const FIXTURE_WORKFLOW = `name: Fixture
|
|
45
|
+
|
|
46
|
+
concurrency:
|
|
47
|
+
group: fixture-\${{ github.ref }}
|
|
48
|
+
cancel-in-progress: false
|
|
49
|
+
|
|
50
|
+
jobs:
|
|
51
|
+
test:
|
|
52
|
+
runs-on: ubuntu-24.04
|
|
53
|
+
concurrency:
|
|
54
|
+
group: \${{ github.workflow }}-\${{ github.ref }}-test
|
|
55
|
+
cancel-in-progress: true # superseded runs stop here
|
|
56
|
+
steps:
|
|
57
|
+
- run: echo test
|
|
58
|
+
|
|
59
|
+
release:
|
|
60
|
+
runs-on: ubuntu-24.04
|
|
61
|
+
concurrency:
|
|
62
|
+
group: main-writer
|
|
63
|
+
cancel-in-progress: false
|
|
64
|
+
steps:
|
|
65
|
+
- run: echo release
|
|
66
|
+
|
|
67
|
+
dynamic:
|
|
68
|
+
runs-on: ubuntu-24.04
|
|
69
|
+
concurrency:
|
|
70
|
+
group: dynamic
|
|
71
|
+
cancel-in-progress: \${{ github.event_name == 'pull_request' }}
|
|
72
|
+
steps:
|
|
73
|
+
- run: echo dynamic
|
|
74
|
+
|
|
75
|
+
inherits:
|
|
76
|
+
runs-on: ubuntu-24.04
|
|
77
|
+
steps:
|
|
78
|
+
- run: echo inherits
|
|
79
|
+
|
|
80
|
+
group-only:
|
|
81
|
+
runs-on: ubuntu-24.04
|
|
82
|
+
concurrency: just-a-group
|
|
83
|
+
steps:
|
|
84
|
+
- run: echo group
|
|
85
|
+
`;
|
|
40
86
|
|
|
41
87
|
function results(map) {
|
|
42
88
|
return Object.fromEntries(
|
|
@@ -44,11 +90,76 @@ function results(map) {
|
|
|
44
90
|
);
|
|
45
91
|
}
|
|
46
92
|
|
|
93
|
+
describe('readCancelInProgress', () => {
|
|
94
|
+
const policy = readCancelInProgress(FIXTURE_WORKFLOW, [
|
|
95
|
+
'test',
|
|
96
|
+
'release',
|
|
97
|
+
'dynamic',
|
|
98
|
+
'inherits',
|
|
99
|
+
'group-only',
|
|
100
|
+
'absent',
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
it('reads a literal job-level value, comments and all', () => {
|
|
104
|
+
assert.equal(policy.get('test'), 'true');
|
|
105
|
+
assert.equal(policy.get('release'), 'false');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('reports an expression as unknown instead of guessing', () => {
|
|
109
|
+
assert.equal(policy.get('dynamic'), 'unknown');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('falls back to the workflow-level group, and to false for a bare group', () => {
|
|
113
|
+
assert.equal(policy.get('inherits'), 'false');
|
|
114
|
+
assert.equal(policy.get('group-only'), 'false');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('says when the workflow has no such job', () => {
|
|
118
|
+
assert.equal(policy.get('absent'), 'missing');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('says when neither the job nor the workflow has a group', () => {
|
|
122
|
+
const bare = 'name: Bare\non: push\njobs:\n only:\n runs-on: x\n';
|
|
123
|
+
assert.equal(readCancelInProgress(bare, ['only']).get('only'), 'none');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
47
127
|
describe('check-pipeline-status.sh', { skip: !BASH_AVAILABLE }, () => {
|
|
128
|
+
let directory;
|
|
129
|
+
let workflowFile;
|
|
130
|
+
|
|
131
|
+
before(() => {
|
|
132
|
+
directory = mkdtempSync(path.join(tmpdir(), 'pipeline-status-'));
|
|
133
|
+
workflowFile = path.join(directory, 'fixture.yml');
|
|
134
|
+
writeFileSync(workflowFile, FIXTURE_WORKFLOW);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
after(() => {
|
|
138
|
+
rmSync(directory, { force: true, recursive: true });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// GITHUB_WORKFLOW_REF is set when this suite itself runs in Actions; blank
|
|
142
|
+
// it so only what a test passes decides which workflow the gate reads.
|
|
143
|
+
function runGate(needs, env = {}, options = {}) {
|
|
144
|
+
return runBashScript(SCRIPT, [], {
|
|
145
|
+
cwd: options.cwd,
|
|
146
|
+
env: {
|
|
147
|
+
GITHUB_WORKFLOW_REF: '',
|
|
148
|
+
WORKFLOW_FILE: workflowFile,
|
|
149
|
+
BRANCH_NAME: 'main',
|
|
150
|
+
RUN_SHA: 'abc',
|
|
151
|
+
BRANCH_HEAD_SHA: 'abc',
|
|
152
|
+
NEEDS_JSON: JSON.stringify(needs),
|
|
153
|
+
...env,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const superseded = { BRANCH_HEAD_SHA: 'def' };
|
|
159
|
+
|
|
48
160
|
it('passes when every job succeeded or was legitimately skipped', () => {
|
|
49
161
|
const { status, output } = runGate(
|
|
50
|
-
results({ lint: 'success', release: 'skipped' })
|
|
51
|
-
{ IS_MAIN: 'true', RUN_SHA: 'abc', BRANCH_HEAD_SHA: 'abc' }
|
|
162
|
+
results({ lint: 'success', release: 'skipped' })
|
|
52
163
|
);
|
|
53
164
|
|
|
54
165
|
assert.equal(status, 0);
|
|
@@ -60,19 +171,15 @@ describe('check-pipeline-status.sh', { skip: !BASH_AVAILABLE }, () => {
|
|
|
60
171
|
|
|
61
172
|
it('fails on a failed job and names it', () => {
|
|
62
173
|
const { status, output } = runGate(
|
|
63
|
-
results({ lint: 'failure', test: 'success' })
|
|
64
|
-
{ IS_MAIN: 'true', RUN_SHA: 'abc', BRANCH_HEAD_SHA: 'abc' }
|
|
174
|
+
results({ lint: 'failure', test: 'success' })
|
|
65
175
|
);
|
|
66
176
|
|
|
67
177
|
assert.equal(status, 1);
|
|
68
178
|
assert.match(output, /::error::Pipeline failed\. Failing jobs: lint/);
|
|
69
179
|
});
|
|
70
180
|
|
|
71
|
-
for (const env of [
|
|
72
|
-
{
|
|
73
|
-
{ IS_MAIN: 'true', RUN_SHA: 'abc', BRANCH_HEAD_SHA: 'def' },
|
|
74
|
-
]) {
|
|
75
|
-
it(`fails on an abandoned runner job with IS_MAIN=${env.IS_MAIN}`, () => {
|
|
181
|
+
for (const env of [{}, superseded]) {
|
|
182
|
+
it(`fails on an abandoned runner job (branch head ${env.BRANCH_HEAD_SHA ?? 'abc'})`, () => {
|
|
76
183
|
// Hosted-runner acquisition failure in run 37362314527 left the macOS
|
|
77
184
|
// check cancelled, but GitHub sent "abandoned" to the needs context.
|
|
78
185
|
const { status, output } = runGate(
|
|
@@ -98,58 +205,124 @@ describe('check-pipeline-status.sh', { skip: !BASH_AVAILABLE }, () => {
|
|
|
98
205
|
});
|
|
99
206
|
}
|
|
100
207
|
|
|
101
|
-
it('
|
|
208
|
+
it('fails on a cancellation in a pull request run that nothing overtook', () => {
|
|
209
|
+
// The old gate only warned off the default branch, so a pull request whose
|
|
210
|
+
// test job hit timeout-minutes passed its gate.
|
|
102
211
|
const { status, output } = runGate(results({ test: 'cancelled' }), {
|
|
103
|
-
|
|
104
|
-
RUN_SHA: 'abc',
|
|
105
|
-
BRANCH_HEAD_SHA: 'abc',
|
|
212
|
+
BRANCH_NAME: 'issue-128-feature',
|
|
106
213
|
});
|
|
107
214
|
|
|
215
|
+
assert.equal(status, 1);
|
|
216
|
+
assert.match(output, /This run tests abc; issue-128-feature is at abc/);
|
|
217
|
+
assert.match(
|
|
218
|
+
output,
|
|
219
|
+
/test: the run is still the head of issue-128-feature/
|
|
220
|
+
);
|
|
221
|
+
assert.match(output, /::error::Pipeline has cancelled jobs: test\./);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('fails on a cancellation at the default branch head', () => {
|
|
225
|
+
const { status, output } = runGate(results({ test: 'cancelled' }));
|
|
226
|
+
|
|
227
|
+
assert.equal(status, 1);
|
|
228
|
+
assert.match(output, /::error::Pipeline has cancelled jobs: test\./);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('only warns when a newer commit overtook a job that cancels in progress', () => {
|
|
232
|
+
const { status, output } = runGate(
|
|
233
|
+
results({ test: 'cancelled' }),
|
|
234
|
+
superseded
|
|
235
|
+
);
|
|
236
|
+
|
|
108
237
|
assert.equal(status, 0);
|
|
109
|
-
assert.match(output,
|
|
238
|
+
assert.match(output, /This run tests abc; main is at def/);
|
|
239
|
+
assert.match(output, /test: it sets cancel-in-progress: true/);
|
|
240
|
+
assert.match(
|
|
241
|
+
output,
|
|
242
|
+
/::warning::Cancelled jobs in a superseded run: test\./
|
|
243
|
+
);
|
|
244
|
+
assert.doesNotMatch(output, /::error::/);
|
|
110
245
|
});
|
|
111
246
|
|
|
112
|
-
|
|
247
|
+
for (const [job, reason] of [
|
|
248
|
+
['release', /it sets cancel-in-progress: false, so a newer run queues/],
|
|
249
|
+
['dynamic', /cancel-in-progress is an expression or otherwise unreadable/],
|
|
250
|
+
['inherits', /it sets cancel-in-progress: false, so a newer run queues/],
|
|
251
|
+
['absent', /declares no job by that name/],
|
|
252
|
+
]) {
|
|
253
|
+
it(`fails on a superseded run's cancelled ${job} job, which no supersede explains`, () => {
|
|
254
|
+
const { status, output } = runGate(
|
|
255
|
+
results({ test: 'cancelled', [job]: 'cancelled' }),
|
|
256
|
+
superseded
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
assert.equal(status, 1);
|
|
260
|
+
assert.match(output, reason);
|
|
261
|
+
assert.match(
|
|
262
|
+
output,
|
|
263
|
+
/::warning::Cancelled jobs in a superseded run: test\./
|
|
264
|
+
);
|
|
265
|
+
assert.match(
|
|
266
|
+
output,
|
|
267
|
+
new RegExp(`::error::Pipeline has cancelled jobs: ${job}\\.`)
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
it('fails closed when it cannot tell which workflow it is gating', () => {
|
|
113
273
|
const { status, output } = runGate(results({ test: 'cancelled' }), {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
BRANCH_HEAD_SHA: 'abc',
|
|
274
|
+
...superseded,
|
|
275
|
+
WORKFLOW_FILE: '',
|
|
117
276
|
});
|
|
118
277
|
|
|
119
278
|
assert.equal(status, 1);
|
|
120
|
-
assert.match(output,
|
|
279
|
+
assert.match(output, /could not identify its workflow/);
|
|
121
280
|
});
|
|
122
281
|
|
|
123
|
-
it('
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
282
|
+
it('finds its workflow through GITHUB_WORKFLOW_REF, from the repository root', () => {
|
|
283
|
+
// ci-policy.yml's `check` job cancels in progress; `pipeline-status` reads
|
|
284
|
+
// it from the checkout whichever directory the step runs in.
|
|
285
|
+
const { status, output } = runGate(
|
|
286
|
+
results({ check: 'cancelled' }),
|
|
287
|
+
{
|
|
288
|
+
...superseded,
|
|
289
|
+
WORKFLOW_FILE: '',
|
|
290
|
+
GITHUB_WORKFLOW_REF:
|
|
291
|
+
'link-foundation/browser-commander/.github/workflows/ci-policy.yml@refs/pull/129/merge',
|
|
292
|
+
},
|
|
293
|
+
{ cwd: directory }
|
|
294
|
+
);
|
|
129
295
|
|
|
130
|
-
assert.equal(status, 0);
|
|
131
|
-
assert.match(output, /
|
|
132
|
-
assert.match(output, /::warning::Cancelled jobs: test/);
|
|
296
|
+
assert.equal(status, 0, output);
|
|
297
|
+
assert.match(output, /check: it sets cancel-in-progress: true/);
|
|
133
298
|
});
|
|
134
299
|
|
|
135
300
|
it('fails rather than guesses when the branch head cannot be resolved', () => {
|
|
136
301
|
// A missed supersede costs one noisy warning; a missed overrun on main
|
|
137
302
|
// costs a silent failure, so the unresolvable case has to be the loud one.
|
|
138
|
-
const
|
|
303
|
+
const { status, output } = runGate(
|
|
304
|
+
results({ test: 'cancelled' }),
|
|
305
|
+
{ BRANCH_HEAD_SHA: '', GIT_REMOTE: 'no-such-remote' },
|
|
306
|
+
{ cwd: directory }
|
|
307
|
+
);
|
|
139
308
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
{ cwd: directory }
|
|
145
|
-
);
|
|
309
|
+
assert.equal(status, 1);
|
|
310
|
+
assert.match(output, /assuming it is current/);
|
|
311
|
+
assert.match(output, /::error::Pipeline has cancelled jobs: test\./);
|
|
312
|
+
});
|
|
146
313
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
314
|
+
it('prints the classification trace only when asked', () => {
|
|
315
|
+
const quiet = runGate(results({ test: 'cancelled' }), superseded);
|
|
316
|
+
const verbose = runGate(results({ test: 'cancelled' }), {
|
|
317
|
+
...superseded,
|
|
318
|
+
PIPELINE_STATUS_VERBOSE: '1',
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
assert.doesNotMatch(quiet.output, /\[pipeline-status\]/);
|
|
322
|
+
assert.match(
|
|
323
|
+
verbose.output,
|
|
324
|
+
/\[pipeline-status\] cancelled test: cancel-in-progress=true, superseded=yes/
|
|
325
|
+
);
|
|
153
326
|
});
|
|
154
327
|
|
|
155
328
|
it('refuses to run without the job results', () => {
|
|
@@ -163,3 +336,42 @@ describe('check-pipeline-status.sh', { skip: !BASH_AVAILABLE }, () => {
|
|
|
163
336
|
assert.match(result.stderr, /NEEDS_JSON is required/);
|
|
164
337
|
});
|
|
165
338
|
});
|
|
339
|
+
|
|
340
|
+
describe('workflows that run the gate', () => {
|
|
341
|
+
const workflows = readdirSync(repoPath('.github/workflows'))
|
|
342
|
+
.filter((name) => name.endsWith('.yml'))
|
|
343
|
+
.map((name) => ({ name, source: readRepoText('.github/workflows', name) }))
|
|
344
|
+
.filter(({ source }) => source.includes('check-pipeline-status.sh'));
|
|
345
|
+
|
|
346
|
+
it('exist', () => {
|
|
347
|
+
assert.ok(workflows.length > 0);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
for (const { name, source } of workflows) {
|
|
351
|
+
it(`${name} tells the gate which commit and branch the run tests`, () => {
|
|
352
|
+
// A pull request run's github.sha is a merge commit no branch points at,
|
|
353
|
+
// and github.ref_name is "129/merge"; the head commit and head branch are
|
|
354
|
+
// what a newer push moves.
|
|
355
|
+
assert.match(
|
|
356
|
+
source,
|
|
357
|
+
/RUN_SHA: \$\{\{ github\.event\.pull_request\.head\.sha \|\| github\.sha \}\}/
|
|
358
|
+
);
|
|
359
|
+
assert.match(
|
|
360
|
+
source,
|
|
361
|
+
/BRANCH_NAME: \$\{\{ github\.head_ref \|\| github\.ref_name \}\}/
|
|
362
|
+
);
|
|
363
|
+
assert.doesNotMatch(source, /IS_MAIN:/);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it(`${name} gives every job a cancel-in-progress the gate can read`, () => {
|
|
367
|
+
const jobsBlock = source.slice(source.search(/^jobs:\s*$/m));
|
|
368
|
+
const jobs = [
|
|
369
|
+
...jobsBlock.matchAll(/^ {2}([A-Za-z_][\w.-]*):\s*$/gm),
|
|
370
|
+
].map((match) => match[1]);
|
|
371
|
+
const unreadable = [...readCancelInProgress(source, jobs)]
|
|
372
|
+
.filter(([, value]) => !['true', 'false'].includes(value))
|
|
373
|
+
.map(([job, value]) => `${job}=${value}`);
|
|
374
|
+
assert.deepEqual(unreadable, []);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
});
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
2
6
|
import { describe, it } from 'node:test';
|
|
3
7
|
|
|
4
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
checkVersionModification,
|
|
10
|
+
findManualVersionChanges,
|
|
11
|
+
} from '../../../../scripts/check-version-modification.mjs';
|
|
12
|
+
import { repoPath } from '../../helpers/repo.js';
|
|
13
|
+
|
|
14
|
+
const SCRIPT = repoPath('scripts/check-version-modification.mjs');
|
|
5
15
|
|
|
6
16
|
// The real implementation shells out to git; injecting the diff keeps these
|
|
7
17
|
// cases independent of the branch the suite happens to run on.
|
|
@@ -68,3 +78,99 @@ describe('check-version-modification', () => {
|
|
|
68
78
|
assert.deepEqual(findManualVersionChanges('main', diff), []);
|
|
69
79
|
});
|
|
70
80
|
});
|
|
81
|
+
|
|
82
|
+
describe('check-version-modification when git cannot diff', () => {
|
|
83
|
+
// Issue #128 (link-foundation/rust-ai-driven-development-pipeline-template
|
|
84
|
+
// #174): a failed `git diff` used to read as "nothing changed", so a missing
|
|
85
|
+
// base ref turned the check into a pass.
|
|
86
|
+
function failingDiff() {
|
|
87
|
+
throw new Error("fatal: ambiguous argument 'origin/main...HEAD'");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
it('fails rather than reporting no change', () => {
|
|
91
|
+
assert.throws(
|
|
92
|
+
() => findManualVersionChanges('main', failingDiff),
|
|
93
|
+
/Could not diff js\/package\.json against origin\/main/
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('fetches the base once and retries before giving up', () => {
|
|
98
|
+
let fetched = 0;
|
|
99
|
+
const diff = (_baseRef, path) => {
|
|
100
|
+
if (fetched === 0) {
|
|
101
|
+
failingDiff();
|
|
102
|
+
}
|
|
103
|
+
return path === 'rust/Cargo.toml' ? '+version = "0.9.1"' : '';
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
assert.deepEqual(
|
|
107
|
+
checkVersionModification('main', {
|
|
108
|
+
diff,
|
|
109
|
+
fetchBase: () => {
|
|
110
|
+
fetched += 1;
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
['rust/Cargo.toml']
|
|
114
|
+
);
|
|
115
|
+
assert.equal(fetched, 1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('reports the failure when the retry fails too', () => {
|
|
119
|
+
let fetched = 0;
|
|
120
|
+
|
|
121
|
+
assert.throws(
|
|
122
|
+
() =>
|
|
123
|
+
checkVersionModification('main', {
|
|
124
|
+
diff: failingDiff,
|
|
125
|
+
fetchBase: () => {
|
|
126
|
+
fetched += 1;
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
/Could not diff/
|
|
130
|
+
);
|
|
131
|
+
assert.equal(fetched, 1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('exits 1 with an error annotation from the command line', () => {
|
|
135
|
+
const directory = mkdtempSync(path.join(tmpdir(), 'version-check-'));
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
// A repository with no origin: the base ref cannot exist, and neither
|
|
139
|
+
// can the fetch that would bring it in.
|
|
140
|
+
spawnSync('git', ['init', '-q', directory]);
|
|
141
|
+
spawnSync(
|
|
142
|
+
'git',
|
|
143
|
+
[
|
|
144
|
+
'-C',
|
|
145
|
+
directory,
|
|
146
|
+
'-c',
|
|
147
|
+
'user.name=t',
|
|
148
|
+
'-c',
|
|
149
|
+
'user.email=t@example.com',
|
|
150
|
+
'commit',
|
|
151
|
+
'-q',
|
|
152
|
+
'--allow-empty',
|
|
153
|
+
'-m',
|
|
154
|
+
'init',
|
|
155
|
+
],
|
|
156
|
+
{ encoding: 'utf8' }
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const result = spawnSync(process.execPath, [SCRIPT], {
|
|
160
|
+
cwd: directory,
|
|
161
|
+
encoding: 'utf8',
|
|
162
|
+
env: {
|
|
163
|
+
...process.env,
|
|
164
|
+
GITHUB_BASE_REF: 'main',
|
|
165
|
+
GITHUB_HEAD_REF: 'feature',
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
170
|
+
assert.match(result.stderr, /::error::Could not diff/);
|
|
171
|
+
assert.doesNotMatch(result.stdout, /No manual version changes/);
|
|
172
|
+
} finally {
|
|
173
|
+
rmSync(directory, { force: true, recursive: true });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -199,18 +199,31 @@ describe('CI execution budgets', () => {
|
|
|
199
199
|
'parity.yml cli CLI and API coverage suites',
|
|
200
200
|
'parity.yml cli Cross-language CLI contract',
|
|
201
201
|
'parity.yml cli Rust CLI build',
|
|
202
|
+
'parity.yml engine-suites Engine lifecycle suites',
|
|
203
|
+
'parity.yml fixture-suites React fixture suites',
|
|
202
204
|
'parity.yml parity Fingerprint parity suite',
|
|
203
205
|
'parity.yml parity WebDriver suite',
|
|
206
|
+
'parity.yml snapshots Python headful real-browser launches',
|
|
204
207
|
'parity.yml snapshots Python native snapshot launches',
|
|
208
|
+
'parity.yml snapshots Rust launch and connect smokes',
|
|
205
209
|
'parity.yml snapshots Rust native snapshots and parity',
|
|
206
210
|
'parity.yml storage-state Rust storage state transfer',
|
|
207
211
|
'python.yml test pytest suite',
|
|
208
212
|
'rust.yml coverage Rust code coverage',
|
|
209
|
-
'rust.yml test Rust doc tests',
|
|
210
213
|
'rust.yml test Rust test suite',
|
|
211
214
|
]);
|
|
212
215
|
});
|
|
213
216
|
|
|
217
|
+
it('runs the Rust doc tests once, inside the main test suite', () => {
|
|
218
|
+
// `cargo test --all-features` already runs them. A second `cargo test
|
|
219
|
+
// --doc` with the default features rebuilt the crate and, on Windows,
|
|
220
|
+
// stalled silently until its budget killed it (issue #128).
|
|
221
|
+
const block = getJobBlock(readWorkflow('rust.yml'), 'test');
|
|
222
|
+
|
|
223
|
+
assert.match(block, /cargo test --all-features/);
|
|
224
|
+
assert.doesNotMatch(block, /cargo test --doc/);
|
|
225
|
+
});
|
|
226
|
+
|
|
214
227
|
it('warns at the same share of a budget that this invariant allows', () => {
|
|
215
228
|
const wrapper = readFileSync(
|
|
216
229
|
repoPath('scripts/run-with-budget-warning.sh'),
|
|
@@ -313,7 +326,7 @@ describe('pipeline status gate', () => {
|
|
|
313
326
|
|
|
314
327
|
assert.match(
|
|
315
328
|
block,
|
|
316
|
-
/RUN_SHA: \$\{\{ github\.sha \}\}/,
|
|
329
|
+
/RUN_SHA: \$\{\{ github\.event\.pull_request\.head\.sha \|\| github\.sha \}\}/,
|
|
317
330
|
`${fileName}: ${GATE_JOB} cannot tell a supersede from an overrun without RUN_SHA`
|
|
318
331
|
);
|
|
319
332
|
}
|