start-command 0.38.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +4 -2
- package/bunfig.toml +4 -0
- package/eslint.config.mjs +1 -0
- package/package.json +4 -4
- package/src/lib/args-parser.js +9 -0
- package/src/lib/cgroup-memory.js +95 -23
- package/src/lib/command-builder.js +4 -0
- package/src/lib/container-removal.js +4 -5
- package/src/lib/detached-finalize.js +1 -0
- package/src/lib/docker-init-option.js +65 -0
- package/src/lib/docker-inspect-batch.js +176 -0
- package/src/lib/docker-resource-options.js +21 -2
- package/src/lib/docker-runtime-args.js +12 -3
- package/src/lib/execution-control.js +8 -6
- package/src/lib/execution-recovery.js +4 -1
- package/src/lib/execution-resume.js +4 -1
- package/src/lib/execution-store.js +19 -1
- package/src/lib/exit-evidence.js +139 -7
- package/src/lib/output-blocks.js +11 -0
- package/src/lib/resume-resources.js +2 -2
- package/src/lib/status-formatter.js +34 -130
- package/src/lib/status-probe.js +149 -0
- package/src/lib/store-decode-cache.js +141 -0
- package/src/lib/usage.js +3 -0
- package/test/cargo-lock-version.mjs +121 -0
- package/test/check-rust-coverage.mjs +71 -0
- package/test/check-web-archive.mjs +154 -0
- package/test/ci-workflow-invariants.js +204 -0
- package/test/command-builder-docker-cleanup.js +24 -0
- package/test/command-exit-codes.mjs +91 -0
- package/test/docker-runtime-options.js +2 -0
- package/test/docker-snapshot-safety.js +39 -0
- package/test/execution-resume.js +2 -0
- package/test/fixtures/fake-npm.sh +64 -0
- package/test/fixtures/lychee-report.md +39 -0
- package/test/javascript-first-ci.js +69 -0
- package/test/links-workflow.mjs +127 -0
- package/test/lint-gate.mjs +56 -0
- package/test/npm-publish.mjs +181 -0
- package/test/recheck-broken-links.mjs +452 -0
- package/test/regression-176.js +2 -0
- package/test/regression-187.js +2 -0
- package/test/regression-191.js +4 -0
- package/test/regression-193.js +2 -0
- package/test/regression-198.js +2 -0
- package/test/regression-207.js +8 -2
- package/test/regression-224.js +2 -0
- package/test/regression-236.js +250 -0
- package/test/regression-238.js +129 -0
- package/test/regression-241.js +254 -0
- package/test/regression-242.js +241 -0
- package/test/regression-243.js +493 -0
- package/test/regression-244.js +471 -0
- package/test/release-push-retry.mjs +172 -0
- package/test/run-js-tests.mjs +15 -3
- package/test/setup/private-snapshot-lock.js +17 -0
- package/test/shared-golden.js +29 -2
- package/test/transient-links.mjs +139 -0
|
@@ -127,6 +127,51 @@ function inRunBlock(workflow, index) {
|
|
|
127
127
|
return false;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** The `needs:` of a job, written inline (`[a, b]`) or as a block list. */
|
|
131
|
+
function parseNeeds(body) {
|
|
132
|
+
const inline = jobKey(body, 'needs');
|
|
133
|
+
if (inline === null) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
if (inline) {
|
|
137
|
+
return inline
|
|
138
|
+
.split(/[,[\]]/)
|
|
139
|
+
.map((entry) => entry.trim())
|
|
140
|
+
.filter(Boolean);
|
|
141
|
+
}
|
|
142
|
+
const block = body.match(/^ {4}needs:\n((?: {6}- .*\n?)+)/m);
|
|
143
|
+
return block
|
|
144
|
+
? block[1]
|
|
145
|
+
.split('\n')
|
|
146
|
+
.map((line) => line.replace(/^ {6}- /, '').trim())
|
|
147
|
+
.filter(Boolean)
|
|
148
|
+
: [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Jobs the stage's pipeline-status gate passes to `--required`. */
|
|
152
|
+
function requiredStageJobs(jobs) {
|
|
153
|
+
const status = jobs.find((job) => job.name === 'pipeline-status');
|
|
154
|
+
const match = status && status.body.match(/--required ([\w,-]+)/);
|
|
155
|
+
return match ? match[1].split(',') : [];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** `workflow_dispatch` inputs as `{ name, body }` blocks. */
|
|
159
|
+
function dispatchInputs(workflow) {
|
|
160
|
+
const section = workflow.match(
|
|
161
|
+
/^ {2}workflow_dispatch:\n {4}inputs:\n((?: {6}.*\n| *\n)+)/m
|
|
162
|
+
);
|
|
163
|
+
if (!section) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
return section[1]
|
|
167
|
+
.split(/^(?= {6}[\w-]+:$)/m)
|
|
168
|
+
.map((block) => {
|
|
169
|
+
const header = block.match(/^ {6}([\w-]+):$/m);
|
|
170
|
+
return header ? { name: header[1], body: block } : null;
|
|
171
|
+
})
|
|
172
|
+
.filter(Boolean);
|
|
173
|
+
}
|
|
174
|
+
|
|
130
175
|
const workflows = listWorkflows();
|
|
131
176
|
|
|
132
177
|
describe('CI workflow invariants', () => {
|
|
@@ -449,6 +494,38 @@ describe('CI workflow invariants', () => {
|
|
|
449
494
|
);
|
|
450
495
|
});
|
|
451
496
|
|
|
497
|
+
it('fails a pull request whose Cargo.lock is out of date', () => {
|
|
498
|
+
// A release bump that rewrote only Cargo.toml left Cargo.lock behind and
|
|
499
|
+
// every later cargo run silently rewrote it (issue #234).
|
|
500
|
+
const lint = parseJobs(readWorkflow('rust.yml')).find(
|
|
501
|
+
(job) => job.name === 'lint'
|
|
502
|
+
);
|
|
503
|
+
assert.ok(lint, 'rust.yml has no lint job');
|
|
504
|
+
assert.match(lint.body, /cargo metadata --locked/);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('reports a failed Rust test as a failed test, not low coverage', () => {
|
|
508
|
+
// Without pipefail a failing test reached the threshold check as an empty
|
|
509
|
+
// value: "Coverage % is below the 50% threshold" (issue #234).
|
|
510
|
+
const coverage = parseJobs(readWorkflow('rust.yml')).find(
|
|
511
|
+
(job) => job.name === 'coverage'
|
|
512
|
+
);
|
|
513
|
+
assert.ok(coverage, 'rust.yml has no coverage job');
|
|
514
|
+
assert.match(coverage.body, /set -o pipefail/);
|
|
515
|
+
assert.match(coverage.body, /scripts\/check-rust-coverage\.mjs/);
|
|
516
|
+
assert.doesNotMatch(
|
|
517
|
+
coverage.body,
|
|
518
|
+
/grep -oP/,
|
|
519
|
+
'parse tarpaulin output through the tested helper'
|
|
520
|
+
);
|
|
521
|
+
assert.ok(
|
|
522
|
+
coverage.body.includes(
|
|
523
|
+
'cargo-tarpaulin-bin-${{ env.TARPAULIN_VERSION }}'
|
|
524
|
+
),
|
|
525
|
+
'an unversioned tarpaulin cache key never picks up a new release'
|
|
526
|
+
);
|
|
527
|
+
});
|
|
528
|
+
|
|
452
529
|
it('parses coverage through the tested helper, not an inline grep', () => {
|
|
453
530
|
const js = readWorkflow('js.yml');
|
|
454
531
|
assert.doesNotMatch(
|
|
@@ -463,4 +540,131 @@ describe('CI workflow invariants', () => {
|
|
|
463
540
|
'an unparsable coverage report must fail, not skip'
|
|
464
541
|
);
|
|
465
542
|
});
|
|
543
|
+
it('releases only after every check its stage requires has passed', () => {
|
|
544
|
+
// Rust Auto Release needed only [lint, test, build] and started in run
|
|
545
|
+
// 39257928573 while the tarpaulin test run was failing (issue #234).
|
|
546
|
+
for (const name of ['js.yml', 'rust.yml']) {
|
|
547
|
+
const jobs = parseJobs(readWorkflow(name));
|
|
548
|
+
const required = requiredStageJobs(jobs);
|
|
549
|
+
assert.ok(required.length > 0, `${name}: no --required list`);
|
|
550
|
+
const releases = jobs.filter(
|
|
551
|
+
(job) => WRITER_JOBS.has(job.name) && job.name !== 'changeset-pr'
|
|
552
|
+
);
|
|
553
|
+
assert.ok(releases.length > 0, `${name}: no release jobs`);
|
|
554
|
+
for (const job of releases) {
|
|
555
|
+
const needs = parseNeeds(job.body);
|
|
556
|
+
for (const check of required) {
|
|
557
|
+
assert.ok(
|
|
558
|
+
needs.includes(check),
|
|
559
|
+
`${name}: ${job.name} does not wait for ${check}`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
// The job-level `needs.*` also sees skipped ancestors (changeset-check
|
|
563
|
+
// and changelog are skipped on push), so a wildcard check skipped
|
|
564
|
+
// every main release; only named results are reliable (issue #240).
|
|
565
|
+
assert.ok(
|
|
566
|
+
!job.body.includes('needs.*'),
|
|
567
|
+
`${name}: ${job.name} checks needs.* instead of named results`
|
|
568
|
+
);
|
|
569
|
+
for (const check of needs) {
|
|
570
|
+
assert.ok(
|
|
571
|
+
job.body.includes(`needs.${check}.result == 'success'`),
|
|
572
|
+
`${name}: ${job.name} may start without a successful ${check}`
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('fails a main run whose release was skipped although its needs succeeded', () => {
|
|
580
|
+
// Nothing was published for days while every run looked green (#240).
|
|
581
|
+
for (const [name, release] of [
|
|
582
|
+
['js.yml', 'release'],
|
|
583
|
+
['rust.yml', 'auto-release'],
|
|
584
|
+
]) {
|
|
585
|
+
const jobs = parseJobs(readWorkflow(name));
|
|
586
|
+
const job = jobs.find((entry) => entry.name === release);
|
|
587
|
+
const gate = jobs.find((entry) => entry.name === 'pipeline-status');
|
|
588
|
+
assert.ok(
|
|
589
|
+
gate.body.includes(
|
|
590
|
+
`--release '${release}:${parseNeeds(job.body).join(',')}'`
|
|
591
|
+
),
|
|
592
|
+
`${name}: pipeline-status does not check that ${release} ran`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('gives every required dispatch input a default', () => {
|
|
598
|
+
// `gh workflow run` cannot fill a form; a required input without a
|
|
599
|
+
// default makes the dispatch API answer HTTP 422 (issue #234).
|
|
600
|
+
for (const name of workflows) {
|
|
601
|
+
for (const input of dispatchInputs(readWorkflow(name))) {
|
|
602
|
+
if (/^ {8}required: true$/m.test(input.body)) {
|
|
603
|
+
assert.match(
|
|
604
|
+
input.body,
|
|
605
|
+
/^ {8}default: /m,
|
|
606
|
+
`${name}: dispatch input "${input.name}" has no default`
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
it('opens release pull requests with a token that triggers their checks', () => {
|
|
613
|
+
// A pull request opened with the built-in GITHUB_TOKEN starts no
|
|
614
|
+
// pull_request workflows, so the changeset PR never ran its checks
|
|
615
|
+
// (issue #234; template issue #192).
|
|
616
|
+
for (const name of workflows) {
|
|
617
|
+
for (const { name: key, body } of parseJobs(readWorkflow(name))) {
|
|
618
|
+
if (!body.includes('peter-evans/create-pull-request@')) {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
assert.ok(
|
|
622
|
+
!/^ {10}token: \$\{\{ secrets\.GITHUB_TOKEN \}\}/m.test(body),
|
|
623
|
+
`${name}: ${key} opens its pull request with GITHUB_TOKEN`
|
|
624
|
+
);
|
|
625
|
+
assert.ok(
|
|
626
|
+
/^ {10}token: \$\{\{ secrets\.RELEASE_PR_TOKEN \}\}/m.test(body) &&
|
|
627
|
+
body.includes('::error::RELEASE_PR_TOKEN is required'),
|
|
628
|
+
`${name}: ${key} must use RELEASE_PR_TOKEN and fail loudly without it`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('re-runs a path-filtered workflow when a script it runs changes', () => {
|
|
635
|
+
// workflows.yml ran only on `.github/**`, so a broken
|
|
636
|
+
// scripts/install-actionlint.sh merged without the linter running.
|
|
637
|
+
for (const name of workflows) {
|
|
638
|
+
const workflow = readWorkflow(name);
|
|
639
|
+
const filters = [
|
|
640
|
+
...workflow.matchAll(/^ {4}paths:\n((?: {6}- .*\n)+)/gm),
|
|
641
|
+
].map((match) =>
|
|
642
|
+
match[1]
|
|
643
|
+
.split('\n')
|
|
644
|
+
.map((line) =>
|
|
645
|
+
line
|
|
646
|
+
.replace(/^ {6}- /, '')
|
|
647
|
+
.replace(/'/g, '')
|
|
648
|
+
.trim()
|
|
649
|
+
)
|
|
650
|
+
.filter(Boolean)
|
|
651
|
+
);
|
|
652
|
+
if (filters.length === 0) {
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const scripts = new Set(
|
|
656
|
+
[...workflow.matchAll(/\bscripts\/[\w.-]+\.(?:sh|mjs|js)\b/g)].map(
|
|
657
|
+
(match) => match[0]
|
|
658
|
+
)
|
|
659
|
+
);
|
|
660
|
+
for (const paths of filters) {
|
|
661
|
+
for (const script of scripts) {
|
|
662
|
+
assert.ok(
|
|
663
|
+
paths.includes(script) || paths.includes('scripts/**'),
|
|
664
|
+
`${name}: ${script} changes do not trigger the workflow`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
});
|
|
466
670
|
});
|
|
@@ -32,4 +32,28 @@ describe('command-builder docker cleanup forwarding', () => {
|
|
|
32
32
|
assert.match(command, /--isolated "tmux"/);
|
|
33
33
|
assert.doesNotMatch(command, /--keep-container/);
|
|
34
34
|
});
|
|
35
|
+
|
|
36
|
+
it('forwards --init only to a remaining docker level (issue #244)', () => {
|
|
37
|
+
const nested = buildNextLevelCommand(
|
|
38
|
+
{
|
|
39
|
+
isolatedStack: ['screen', 'docker'],
|
|
40
|
+
imageStack: [null, 'alpine'],
|
|
41
|
+
init: true,
|
|
42
|
+
},
|
|
43
|
+
'npm test'
|
|
44
|
+
);
|
|
45
|
+
assert.match(nested, /--isolated "docker"/);
|
|
46
|
+
assert.match(nested, / --init(?: |$)/);
|
|
47
|
+
|
|
48
|
+
const consumed = buildNextLevelCommand(
|
|
49
|
+
{
|
|
50
|
+
isolatedStack: ['docker', 'tmux'],
|
|
51
|
+
imageStack: ['alpine', null],
|
|
52
|
+
init: true,
|
|
53
|
+
},
|
|
54
|
+
'npm test'
|
|
55
|
+
);
|
|
56
|
+
assert.match(consumed, /--isolated "tmux"/);
|
|
57
|
+
assert.doesNotMatch(consumed, /--init/);
|
|
58
|
+
});
|
|
35
59
|
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
// command-stream's `$` resolves with a non-zero `code` instead of throwing, so
|
|
7
|
+
// a bare `await $`...`` - or one inside try/catch - silently ignores a failed
|
|
8
|
+
// command. That made a rejected `npm publish` print "✅ Published" and a failed
|
|
9
|
+
// GitHub API call print "✅ Formatted release notes" (issue #234). Every call in
|
|
10
|
+
// scripts/ must either go through checked() or read the result's `.code`.
|
|
11
|
+
|
|
12
|
+
const scriptsDir = join(
|
|
13
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
14
|
+
'../../scripts'
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Find `$` template calls whose exit code nobody looks at.
|
|
19
|
+
* @param {string} source
|
|
20
|
+
* @returns {string[]} offending lines, prefixed with their line number
|
|
21
|
+
*/
|
|
22
|
+
export function findUncheckedCommands(source) {
|
|
23
|
+
const lines = source.split('\n');
|
|
24
|
+
const offenders = [];
|
|
25
|
+
lines.forEach((line, index) => {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
if (
|
|
28
|
+
!trimmed.includes('$`') ||
|
|
29
|
+
trimmed.startsWith('*') ||
|
|
30
|
+
trimmed.startsWith('//')
|
|
31
|
+
) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const previous = index > 0 ? lines[index - 1] : '';
|
|
35
|
+
if (/\bchecked\(/.test(line) || /\bchecked\(\s*$/.test(previous)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const assigned = `${previous}\n${line}`.match(
|
|
39
|
+
/(?:const|let)\s+(\w+)\s*=\s*(?:await\s+)?$|(?:const|let)\s+(\w+)\s*=\s*(?:await\s+)?\$`/m
|
|
40
|
+
);
|
|
41
|
+
const name = assigned && (assigned[1] || assigned[2]);
|
|
42
|
+
const rest = lines.slice(index + 1).join('\n');
|
|
43
|
+
if (name && new RegExp(`\\b${name}\\.code\\b`).test(rest)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
offenders.push(`${index + 1}: ${trimmed}`);
|
|
47
|
+
});
|
|
48
|
+
return offenders;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('command exit codes in release scripts', () => {
|
|
52
|
+
it('flags a bare call and a try/catch-wrapped call', () => {
|
|
53
|
+
const source = [
|
|
54
|
+
'try {',
|
|
55
|
+
' await $`npm run changeset:publish`;',
|
|
56
|
+
'} catch {}',
|
|
57
|
+
'const r = await $`gh api x`.run({ capture: true });',
|
|
58
|
+
'console.log(r.stdout);',
|
|
59
|
+
].join('\n');
|
|
60
|
+
expect(findUncheckedCommands(source)).toEqual([
|
|
61
|
+
'2: await $`npm run changeset:publish`;',
|
|
62
|
+
'4: const r = await $`gh api x`.run({ capture: true });',
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('accepts checked() and an inspected .code', () => {
|
|
67
|
+
const source = [
|
|
68
|
+
'await checked($`git push`, "git push");',
|
|
69
|
+
'await checked(',
|
|
70
|
+
' $`git fetch`,',
|
|
71
|
+
' "git fetch"',
|
|
72
|
+
');',
|
|
73
|
+
'const ancestry =',
|
|
74
|
+
' await $`git merge-base --is-ancestor a b`.run({ capture: true });',
|
|
75
|
+
'if (ancestry.code !== 0) throw new Error("x");',
|
|
76
|
+
'// await $`ignored in a comment`',
|
|
77
|
+
].join('\n');
|
|
78
|
+
expect(findUncheckedCommands(source)).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('every command in scripts/*.mjs has its exit code checked', () => {
|
|
82
|
+
const offenders = readdirSync(scriptsDir)
|
|
83
|
+
.filter((file) => file.endsWith('.mjs'))
|
|
84
|
+
.flatMap((file) =>
|
|
85
|
+
findUncheckedCommands(readFileSync(join(scriptsDir, file), 'utf8')).map(
|
|
86
|
+
(offender) => `scripts/${file}:${offender}`
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
expect(offenders).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -303,6 +303,7 @@ describe('buildDockerRuntimeMetadata', () => {
|
|
|
303
303
|
labels: null,
|
|
304
304
|
commandHandoff: null,
|
|
305
305
|
privileged: null,
|
|
306
|
+
init: null,
|
|
306
307
|
network: null,
|
|
307
308
|
networks: null,
|
|
308
309
|
networkAliases: null,
|
|
@@ -332,6 +333,7 @@ describe('buildDockerRuntimeMetadata', () => {
|
|
|
332
333
|
mounts: ['type=bind,src=/h,dst=/c'],
|
|
333
334
|
env: ['FOO=bar'],
|
|
334
335
|
privileged: true,
|
|
336
|
+
init: null,
|
|
335
337
|
network: 'hive-formal-ai',
|
|
336
338
|
networks: ['hive-formal-ai', 'public-egress'],
|
|
337
339
|
networkAliases: ['formal-ai', 'checker'],
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Own snapshot lock even without the bunfig.toml preload (issue #234).
|
|
2
|
+
require('./setup/private-snapshot-lock');
|
|
1
3
|
const { test } = require('node:test');
|
|
2
4
|
const assert = require('node:assert/strict');
|
|
3
5
|
const fs = require('node:fs');
|
|
@@ -104,3 +106,40 @@ test('snapshot lock is separate from store locks and excludes concurrent process
|
|
|
104
106
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
105
107
|
}
|
|
106
108
|
});
|
|
109
|
+
|
|
110
|
+
test('tests never share the host-wide snapshot lock (issue #234)', () => {
|
|
111
|
+
// bunfig.toml preloads test/setup/private-snapshot-lock.js. Without it a
|
|
112
|
+
// real `start --resume` holding the default lock failed every resume test.
|
|
113
|
+
const lockPath = process.env.START_SNAPSHOT_LOCK;
|
|
114
|
+
assert.ok(lockPath, 'START_SNAPSHOT_LOCK is not set by the test preload');
|
|
115
|
+
assert.notEqual(
|
|
116
|
+
lockPath,
|
|
117
|
+
path.join(
|
|
118
|
+
process.platform === 'win32' ? os.tmpdir() : '/tmp',
|
|
119
|
+
'start-command-docker-snapshot.lock'
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
const lock = acquireSnapshotLock({ holder: 'issue-234' });
|
|
123
|
+
try {
|
|
124
|
+
assert.ok(fs.existsSync(lockPath));
|
|
125
|
+
} finally {
|
|
126
|
+
lock.release();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('every snapshot test requires its private lock itself (issue #234)', () => {
|
|
131
|
+
// `bun test ./js/test/x.js` from the repository root skips js/bunfig.toml,
|
|
132
|
+
// so the preload alone left 18 tests on the host-wide lock.
|
|
133
|
+
const dir = __dirname;
|
|
134
|
+
const offenders = fs
|
|
135
|
+
.readdirSync(dir)
|
|
136
|
+
.filter((name) => name.endsWith('.js'))
|
|
137
|
+
.filter((name) => {
|
|
138
|
+
const source = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
139
|
+
return (
|
|
140
|
+
/\b(resumeExecution|acquireSnapshotLock)\(/.test(source) &&
|
|
141
|
+
!source.includes("require('./setup/private-snapshot-lock')")
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
assert.deepEqual(offenders, []);
|
|
145
|
+
});
|
package/test/execution-resume.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Tests for --resume and --resume-all (issue #162)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
// Own snapshot lock even without the bunfig.toml preload (issue #234).
|
|
6
|
+
require('./setup/private-snapshot-lock');
|
|
5
7
|
const { test, describe } = require('node:test');
|
|
6
8
|
const assert = require('node:assert');
|
|
7
9
|
const {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Fake `npm` for js/test/npm-publish.mjs (issue #234). State lives in
|
|
3
|
+
# $FAKE_NPM_STATE: `published` marks the version as on the registry,
|
|
4
|
+
# `mode` picks how `npm run changeset:publish` behaves, `calls` logs argv.
|
|
5
|
+
# `pending` holds how many more `npm view` calls miss a version that has
|
|
6
|
+
# landed but not yet propagated to the registry's read path.
|
|
7
|
+
# Any other command goes to $REAL_NPM when set (use-m runs `npm root -g`).
|
|
8
|
+
state="${FAKE_NPM_STATE:?}"
|
|
9
|
+
echo "$*" >> "$state/calls"
|
|
10
|
+
case "$1" in
|
|
11
|
+
view)
|
|
12
|
+
if [ -f "$state/outage" ]; then
|
|
13
|
+
echo "npm error code ECONNRESET" >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
if [ -f "$state/pending" ]; then
|
|
17
|
+
left="$(cat "$state/pending")"
|
|
18
|
+
if [ "$left" -le 1 ]; then
|
|
19
|
+
rm "$state/pending"
|
|
20
|
+
echo "$FAKE_NPM_VERSION" > "$state/published"
|
|
21
|
+
else
|
|
22
|
+
echo $((left - 1)) > "$state/pending"
|
|
23
|
+
fi
|
|
24
|
+
echo "npm error code E404" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
if [ -f "$state/published" ]; then
|
|
28
|
+
cat "$state/published"
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
echo "npm error code E404" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
;;
|
|
34
|
+
run)
|
|
35
|
+
mode="$(cat "$state/mode" 2>/dev/null || echo succeed)"
|
|
36
|
+
case "$mode" in
|
|
37
|
+
succeed) echo "$FAKE_NPM_VERSION" > "$state/published"; exit 0 ;;
|
|
38
|
+
fail) echo "npm error code E403" >&2; exit 1 ;;
|
|
39
|
+
exit-0-without-publishing) exit 0 ;;
|
|
40
|
+
fail-after-landing) echo "$FAKE_NPM_VERSION" > "$state/published"; exit 1 ;;
|
|
41
|
+
land-late) echo 20 > "$state/pending"; exit 0 ;;
|
|
42
|
+
time-out-while-landing)
|
|
43
|
+
if [ -f "$state/pending" ] || [ -f "$state/published" ]; then
|
|
44
|
+
echo "npm error code E403" >&2
|
|
45
|
+
echo "npm error You cannot publish over the previously published versions: $FAKE_NPM_VERSION." >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
echo 20 > "$state/pending"
|
|
49
|
+
echo "npm error code ETIMEDOUT" >&2
|
|
50
|
+
exit 1
|
|
51
|
+
;;
|
|
52
|
+
fail-once)
|
|
53
|
+
echo succeed > "$state/mode"
|
|
54
|
+
echo "npm error code E503" >&2
|
|
55
|
+
exit 1
|
|
56
|
+
;;
|
|
57
|
+
esac
|
|
58
|
+
;;
|
|
59
|
+
esac
|
|
60
|
+
if [ -n "${REAL_NPM:-}" ]; then
|
|
61
|
+
exec "$REAL_NPM" "$@"
|
|
62
|
+
fi
|
|
63
|
+
echo "fake npm: unexpected arguments: $*" >&2
|
|
64
|
+
exit 2
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Link Checker Report
|
|
2
|
+
|
|
3
|
+
| Status | Count |
|
|
4
|
+
| -------------- | ----- |
|
|
5
|
+
| 🔍 Total | 120 |
|
|
6
|
+
| ✅ Successful | 108 |
|
|
7
|
+
| ⏳ Timeouts | 0 |
|
|
8
|
+
| 🔀 Redirected | 6 |
|
|
9
|
+
| 👻 Excluded | 2 |
|
|
10
|
+
| ❓ Unknown | 0 |
|
|
11
|
+
| 🚫 Errors | 4 |
|
|
12
|
+
| ⛔ Unsupported | 0 |
|
|
13
|
+
|
|
14
|
+
## Errors per input
|
|
15
|
+
|
|
16
|
+
### Errors in csharp/docs/index.md
|
|
17
|
+
|
|
18
|
+
- [ERROR] <file:///home/runner/work/repo/repo/csharp/docs/api/Foundation.Data.Doublets.Cli.yml> (at 15:12) | File not found. Check if file exists and path is correct
|
|
19
|
+
|
|
20
|
+
### Errors in js/index.html
|
|
21
|
+
|
|
22
|
+
- [ERROR] <error:> (at 10:49) | Cannot resolve root-relative link '/favicon.svg': To resolve root-relative links in local files, provide a root dir
|
|
23
|
+
|
|
24
|
+
### Errors in README.md
|
|
25
|
+
|
|
26
|
+
- [404] <https://example.com/csharp/> (at 48:130) | Rejected status code: 404 Not Found
|
|
27
|
+
- [404] <https://example.com/rust/link_cli/> (at 49:62) | Rejected status code: 404 Not Found
|
|
28
|
+
|
|
29
|
+
## Redirects per input
|
|
30
|
+
|
|
31
|
+
### Redirects in README.md
|
|
32
|
+
|
|
33
|
+
- https://example.com/docs --[302]--> https://example.com/docs/latest/
|
|
34
|
+
- https://example.com/protocols --[301]--> https://example.com/links-notation
|
|
35
|
+
- https://example.com/articles/804617 --[301]--> https://example.com/articles/804617/ --[302]--> https://example.com/companies/articles/804617/
|
|
36
|
+
|
|
37
|
+
### Redirects in rust/README.md
|
|
38
|
+
|
|
39
|
+
- https://example.com/docs --[302]--> https://example.com/docs/latest/
|
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
} = require('../../scripts/check-javascript-first.mjs');
|
|
10
10
|
const {
|
|
11
11
|
checkFeatureChanges,
|
|
12
|
+
checkRegressionPairs,
|
|
12
13
|
} = require('../../scripts/check-feature-parity.mjs');
|
|
13
14
|
|
|
14
15
|
const root = resolve(__dirname, '../..');
|
|
@@ -178,6 +179,44 @@ describe('JavaScript first CI gate', () => {
|
|
|
178
179
|
assert.equal(check.status, 0);
|
|
179
180
|
});
|
|
180
181
|
|
|
182
|
+
const releaseCheck = (isMain, release, test = 'success') =>
|
|
183
|
+
spawnSync(
|
|
184
|
+
process.execPath,
|
|
185
|
+
[
|
|
186
|
+
resolve(root, 'scripts/check-stage-status.mjs'),
|
|
187
|
+
'--required',
|
|
188
|
+
'test',
|
|
189
|
+
'--release',
|
|
190
|
+
'release:test',
|
|
191
|
+
],
|
|
192
|
+
{
|
|
193
|
+
encoding: 'utf8',
|
|
194
|
+
env: {
|
|
195
|
+
...process.env,
|
|
196
|
+
IS_MAIN: String(isMain),
|
|
197
|
+
NEEDS_JSON: JSON.stringify({
|
|
198
|
+
test: { result: test },
|
|
199
|
+
release: { result: release },
|
|
200
|
+
}),
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
it('fails a main push whose release was skipped after its needs passed', () => {
|
|
206
|
+
// Every main release was silently skipped since #234 (issue #240).
|
|
207
|
+
const check = releaseCheck(true, 'skipped');
|
|
208
|
+
assert.equal(check.status, 1);
|
|
209
|
+
assert.match(check.stderr, /release was skipped on a main push/);
|
|
210
|
+
// The terms of the release condition are printed for the next skip.
|
|
211
|
+
assert.match(check.stdout, /needs\.test\.result=success/);
|
|
212
|
+
assert.match(check.stdout, /release: skipped; expected to run: true/);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('accepts a skipped release outside main pushes and a run release on main', () => {
|
|
216
|
+
assert.equal(releaseCheck(false, 'skipped').status, 0);
|
|
217
|
+
assert.equal(releaseCheck(true, 'success').status, 0);
|
|
218
|
+
});
|
|
219
|
+
|
|
181
220
|
it('rejects a change to only one implementation of a feature', () => {
|
|
182
221
|
const features = [
|
|
183
222
|
{
|
|
@@ -194,6 +233,36 @@ describe('JavaScript first CI gate', () => {
|
|
|
194
233
|
);
|
|
195
234
|
});
|
|
196
235
|
|
|
236
|
+
it('keeps both halves of a regression test under one feature', () => {
|
|
237
|
+
const feature = (id, js, rust) => ({
|
|
238
|
+
id,
|
|
239
|
+
javascript: { implementation: [], tests: js },
|
|
240
|
+
rust: { implementation: [], tests: rust },
|
|
241
|
+
});
|
|
242
|
+
assert.equal(
|
|
243
|
+
checkRegressionPairs([
|
|
244
|
+
feature('a', ['js/test/regression-193.js'], []),
|
|
245
|
+
feature('b', [], ['rust/tests/regression_193.rs']),
|
|
246
|
+
]).length,
|
|
247
|
+
1
|
|
248
|
+
);
|
|
249
|
+
assert.deepEqual(
|
|
250
|
+
checkRegressionPairs([
|
|
251
|
+
feature(
|
|
252
|
+
'a',
|
|
253
|
+
['js/test/regression-201.js'],
|
|
254
|
+
['rust/tests/regression_201.rs']
|
|
255
|
+
),
|
|
256
|
+
feature('b', [], ['rust/tests/regression_201.rs']),
|
|
257
|
+
]),
|
|
258
|
+
[]
|
|
259
|
+
);
|
|
260
|
+
const manifest = JSON.parse(
|
|
261
|
+
readFileSync(resolve(root, 'parity/features.json'), 'utf8')
|
|
262
|
+
);
|
|
263
|
+
assert.deepEqual(checkRegressionPairs(manifest.features), []);
|
|
264
|
+
});
|
|
265
|
+
|
|
197
266
|
it('makes Rust callable only after the complete JavaScript stage', () => {
|
|
198
267
|
const files = workflows();
|
|
199
268
|
assert.deepEqual(Object.keys(files['rust.yml'].on), ['workflow_call']);
|