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.
Files changed (59) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +4 -2
  3. package/bunfig.toml +4 -0
  4. package/eslint.config.mjs +1 -0
  5. package/package.json +4 -4
  6. package/src/lib/args-parser.js +9 -0
  7. package/src/lib/cgroup-memory.js +95 -23
  8. package/src/lib/command-builder.js +4 -0
  9. package/src/lib/container-removal.js +4 -5
  10. package/src/lib/detached-finalize.js +1 -0
  11. package/src/lib/docker-init-option.js +65 -0
  12. package/src/lib/docker-inspect-batch.js +176 -0
  13. package/src/lib/docker-resource-options.js +21 -2
  14. package/src/lib/docker-runtime-args.js +12 -3
  15. package/src/lib/execution-control.js +8 -6
  16. package/src/lib/execution-recovery.js +4 -1
  17. package/src/lib/execution-resume.js +4 -1
  18. package/src/lib/execution-store.js +19 -1
  19. package/src/lib/exit-evidence.js +139 -7
  20. package/src/lib/output-blocks.js +11 -0
  21. package/src/lib/resume-resources.js +2 -2
  22. package/src/lib/status-formatter.js +34 -130
  23. package/src/lib/status-probe.js +149 -0
  24. package/src/lib/store-decode-cache.js +141 -0
  25. package/src/lib/usage.js +3 -0
  26. package/test/cargo-lock-version.mjs +121 -0
  27. package/test/check-rust-coverage.mjs +71 -0
  28. package/test/check-web-archive.mjs +154 -0
  29. package/test/ci-workflow-invariants.js +204 -0
  30. package/test/command-builder-docker-cleanup.js +24 -0
  31. package/test/command-exit-codes.mjs +91 -0
  32. package/test/docker-runtime-options.js +2 -0
  33. package/test/docker-snapshot-safety.js +39 -0
  34. package/test/execution-resume.js +2 -0
  35. package/test/fixtures/fake-npm.sh +64 -0
  36. package/test/fixtures/lychee-report.md +39 -0
  37. package/test/javascript-first-ci.js +69 -0
  38. package/test/links-workflow.mjs +127 -0
  39. package/test/lint-gate.mjs +56 -0
  40. package/test/npm-publish.mjs +181 -0
  41. package/test/recheck-broken-links.mjs +452 -0
  42. package/test/regression-176.js +2 -0
  43. package/test/regression-187.js +2 -0
  44. package/test/regression-191.js +4 -0
  45. package/test/regression-193.js +2 -0
  46. package/test/regression-198.js +2 -0
  47. package/test/regression-207.js +8 -2
  48. package/test/regression-224.js +2 -0
  49. package/test/regression-236.js +250 -0
  50. package/test/regression-238.js +129 -0
  51. package/test/regression-241.js +254 -0
  52. package/test/regression-242.js +241 -0
  53. package/test/regression-243.js +493 -0
  54. package/test/regression-244.js +471 -0
  55. package/test/release-push-retry.mjs +172 -0
  56. package/test/run-js-tests.mjs +15 -3
  57. package/test/setup/private-snapshot-lock.js +17 -0
  58. package/test/shared-golden.js +29 -2
  59. package/test/transient-links.mjs +139 -0
@@ -37,18 +37,45 @@ describe('shared JavaScript and Rust golden contracts', () => {
37
37
  );
38
38
  });
39
39
  }
40
+ // The fixtures describe the defaults, so a host-wide docker init default
41
+ // (issue #244) must not leak into them.
42
+ function parseWithoutHostDefaults(argv) {
43
+ const saved = process.env.START_COMMAND_DOCKER_INIT;
44
+ delete process.env.START_COMMAND_DOCKER_INIT;
45
+ try {
46
+ return parseArgs(argv);
47
+ } finally {
48
+ if (saved !== undefined) {
49
+ process.env.START_COMMAND_DOCKER_INIT = saved;
50
+ }
51
+ }
52
+ }
40
53
  for (const fixture of fixtures['cli-parsing']) {
41
54
  it(`CLI parsing: ${fixture.name}`, () => {
42
55
  if (fixture.error) {
43
- assert.throws(() => parseArgs(fixture.argv), new RegExp(fixture.error));
56
+ assert.throws(
57
+ () => parseWithoutHostDefaults(fixture.argv),
58
+ new RegExp(fixture.error)
59
+ );
44
60
  return;
45
61
  }
46
- const parsed = parseArgs(fixture.argv);
62
+ const parsed = parseWithoutHostDefaults(fixture.argv);
47
63
  assert.equal(parsed.command, fixture.command);
48
64
  assert.deepEqual(parsed.rawCommand, fixture.rawCommand);
49
65
  for (const [key, value] of Object.entries(fixture.options)) {
50
66
  assert.deepEqual(parsed.wrapperOptions[key], value, key);
51
67
  }
68
+ if (fixture.dockerResourceLimits) {
69
+ const {
70
+ resolveResourceOptions,
71
+ } = require('../src/lib/docker-resource-options');
72
+ assert.deepEqual(
73
+ buildDockerRuntimeArgs(
74
+ resolveResourceOptions(parsed.wrapperOptions)
75
+ ).sort(),
76
+ [...fixture.dockerResourceLimits].sort()
77
+ );
78
+ }
52
79
  });
53
80
  }
54
81
  for (const fixture of fixtures['isolation-arguments']) {
@@ -0,0 +1,139 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { describe, expect, it } from 'bun:test';
5
+ import {
6
+ parseLycheeFailures,
7
+ recheckUnanswered,
8
+ } from '../../scripts/recheck-broken-links.mjs';
9
+
10
+ const root = path.resolve(
11
+ path.dirname(fileURLToPath(import.meta.url)),
12
+ '../..'
13
+ );
14
+
15
+ const url = 'https://github.com/owner/repo/blob/main/src/index.js';
16
+ const base = { accept: '200..=299', userAgent: 'lychee' };
17
+
18
+ describe('transient link failures', () => {
19
+ it('classifies cached 503 and rejected 429 as retryable, preserving 404', () => {
20
+ const failures = parseLycheeFailures(
21
+ [
22
+ `- [503] <${url}> | Error (cached)`,
23
+ `- [ERROR] <${url}> | Rejected status code: 429 Too Many Requests`,
24
+ `- [404] <${url}> | Not Found`,
25
+ ].join('\n')
26
+ );
27
+ expect(failures.map((item) => item.retryable)).toEqual([true, true, false]);
28
+ });
29
+
30
+ it('backs off between transient answers and recovers only on success', async () => {
31
+ let now = 0;
32
+ const waits = [];
33
+ const statuses = [503, 429, 200];
34
+ const result = await recheckUnanswered([url], {
35
+ ...base,
36
+ nowFn: () => now,
37
+ sleepFn: async (ms) => {
38
+ waits.push(ms);
39
+ now += ms;
40
+ },
41
+ fetchImpl: async () => ({ status: statuses.shift() }),
42
+ });
43
+ expect(result.recovered).toEqual([url]);
44
+ expect(waits).toEqual([5000, 10000]);
45
+ });
46
+
47
+ it('uses authenticated contents GET for GitHub blob and tree throttling', async () => {
48
+ for (const kind of ['blob', 'tree']) {
49
+ const page = `https://github.com/owner/repo/${kind}/release%2Fv1/src/a%20b.js`;
50
+ const calls = [];
51
+ const result = await recheckUnanswered([page], {
52
+ ...base,
53
+ githubToken: 'test-token',
54
+ fetchImpl: async (target, init) => {
55
+ calls.push({ target, init });
56
+ return { status: calls.length === 1 ? 503 : 200 };
57
+ },
58
+ });
59
+ expect(result.recovered).toEqual([page]);
60
+ expect(calls[0].init.headers.authorization).toBe(undefined);
61
+ expect(calls[1].target).toBe(
62
+ 'https://api.github.com/repos/owner/repo/contents/src/a%20b.js?ref=release%2Fv1'
63
+ );
64
+ expect(calls[1].init.method).toBe('GET');
65
+ expect(calls[1].init.headers.authorization).toBe('Bearer test-token');
66
+ expect(calls[1].init.redirect).toBe('error');
67
+ }
68
+ });
69
+
70
+ it('does not recover on an API denial and keeps credentials off unrelated hosts', async () => {
71
+ let now = 0;
72
+ const calls = [];
73
+ const result = await recheckUnanswered(
74
+ [url, 'https://other.example/page'],
75
+ {
76
+ ...base,
77
+ githubToken: 'test-token',
78
+ budgetSeconds: 1,
79
+ nowFn: () => now,
80
+ fetchImpl: async (target, init) => {
81
+ calls.push({ target, init });
82
+ now += 100;
83
+ return {
84
+ status: target.startsWith('https://api.github.com/') ? 401 : 503,
85
+ };
86
+ },
87
+ }
88
+ );
89
+ expect(result.recovered).toEqual([]);
90
+ expect(result.stillBroken.length).toBe(2);
91
+ const other = calls.find((call) =>
92
+ call.target.startsWith('https://other.example/')
93
+ );
94
+ expect(other.init.headers.authorization).toBe(undefined);
95
+ });
96
+
97
+ it('never retries a definitive 404 or uses an API answer to hide it', async () => {
98
+ let calls = 0;
99
+ const result = await recheckUnanswered([url], {
100
+ ...base,
101
+ githubToken: 'test-token',
102
+ fetchImpl: async () => {
103
+ calls++;
104
+ return { status: 404 };
105
+ },
106
+ });
107
+ expect(calls).toBe(1);
108
+ expect(result.recovered).toEqual([]);
109
+ });
110
+
111
+ it('bounds persistent 503 errors by the shared wall-clock budget', async () => {
112
+ let now = 0;
113
+ let calls = 0;
114
+ const result = await recheckUnanswered([url], {
115
+ ...base,
116
+ budgetSeconds: 16,
117
+ nowFn: () => now,
118
+ sleepFn: async (ms) => {
119
+ now += ms;
120
+ },
121
+ fetchImpl: async () => {
122
+ calls++;
123
+ return { status: 503 };
124
+ },
125
+ });
126
+ expect(calls).toBe(3);
127
+ expect(result.recovered).toEqual([]);
128
+ expect(result.stillBroken.length).toBe(1);
129
+ });
130
+
131
+ it('pins every Bun setup to the supported major line', () => {
132
+ const workflow = readFileSync(
133
+ path.join(root, '.github/workflows/js.yml'),
134
+ 'utf8'
135
+ );
136
+ expect(workflow).not.toContain('bun-version: latest');
137
+ expect(workflow).toContain("bun-version: '1.x'");
138
+ });
139
+ });