start-command 0.29.1 → 0.30.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 +12 -0
- package/package.json +1 -1
- package/src/bin/cli.js +7 -0
- package/src/lib/args-parser.js +57 -13
- package/src/lib/command-builder.js +13 -2
- package/src/lib/docker-cleanup.js +145 -0
- package/src/lib/docker-utils.js +133 -6
- package/src/lib/isolation.js +52 -29
- package/src/lib/usage.js +4 -1
- package/test/args-parser-docker-cleanup.js +143 -0
- package/test/args-parser.js +0 -69
- package/test/command-builder-docker-cleanup.js +35 -0
- package/test/docker-autoremove.js +204 -29
- package/test/regression-138.js +199 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Regression tests for issue #138:
|
|
4
|
+
* "Detached docker session log omits the image-preparation phase
|
|
5
|
+
* (docker pull / dind boot) — $ does not preserve the full log in one file"
|
|
6
|
+
*
|
|
7
|
+
* When a command is launched with `--isolated docker`, the image-preparation
|
|
8
|
+
* phase (the `docker pull`) used to be printed to the console only and never
|
|
9
|
+
* written to the session log file. An operator tailing the session log (e.g.
|
|
10
|
+
* via `$ --upload-log <uuid>`) during a multi-GB pull would see only the
|
|
11
|
+
* header — the minutes spent pulling left no trace in the log.
|
|
12
|
+
*
|
|
13
|
+
* The fix tees the pull output into the session log and brackets it with
|
|
14
|
+
* `Preparing image …` / `Image ready (<duration>)` markers so the single
|
|
15
|
+
* session-log file is a gap-free record of everything that ran.
|
|
16
|
+
*
|
|
17
|
+
* Reference: https://github.com/link-foundation/start/issues/138
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
21
|
+
const assert = require('assert');
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
dockerPullImage,
|
|
28
|
+
dockerImageExists,
|
|
29
|
+
isDockerAvailable,
|
|
30
|
+
} = require('../src/lib/docker-utils');
|
|
31
|
+
|
|
32
|
+
function makeTempLog() {
|
|
33
|
+
const logPath = path.join(
|
|
34
|
+
os.tmpdir(),
|
|
35
|
+
`start-138-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.log`
|
|
36
|
+
);
|
|
37
|
+
fs.writeFileSync(logPath, '=== Start Command Log ===\n');
|
|
38
|
+
return logPath;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('docker pull is recorded in the session log (issue #138)', () => {
|
|
42
|
+
// Silence the virtual-command console output during these tests.
|
|
43
|
+
let originalLog;
|
|
44
|
+
let originalError;
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
originalLog = console.log;
|
|
47
|
+
originalError = console.error;
|
|
48
|
+
console.log = () => {};
|
|
49
|
+
console.error = () => {};
|
|
50
|
+
});
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
console.log = originalLog;
|
|
53
|
+
console.error = originalError;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('writes prep markers and the docker error into the log on a failed pull', () => {
|
|
57
|
+
if (!isDockerAvailable()) {
|
|
58
|
+
console.log = originalLog;
|
|
59
|
+
console.log(' Skipping: docker daemon not available');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const logPath = makeTempLog();
|
|
64
|
+
try {
|
|
65
|
+
// An invalid reference format fails fast without any network access.
|
|
66
|
+
const result = dockerPullImage('invalid..badname', logPath);
|
|
67
|
+
const contents = fs.readFileSync(logPath, 'utf8');
|
|
68
|
+
|
|
69
|
+
assert.strictEqual(
|
|
70
|
+
result.success,
|
|
71
|
+
false,
|
|
72
|
+
'pull of invalid ref must fail'
|
|
73
|
+
);
|
|
74
|
+
assert.ok(
|
|
75
|
+
contents.includes('Preparing image invalid..badname'),
|
|
76
|
+
'log must contain the "Preparing image …" start marker'
|
|
77
|
+
);
|
|
78
|
+
assert.ok(
|
|
79
|
+
contents.includes('Image preparation failed'),
|
|
80
|
+
'log must contain the failure marker with elapsed duration'
|
|
81
|
+
);
|
|
82
|
+
// The docker error itself (teed pull output) must be in the log, not just
|
|
83
|
+
// on the console — this is the core of issue #138.
|
|
84
|
+
assert.ok(
|
|
85
|
+
contents.includes('invalid reference format'),
|
|
86
|
+
'log must capture the teed docker pull error output'
|
|
87
|
+
);
|
|
88
|
+
} finally {
|
|
89
|
+
fs.rmSync(logPath, { force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('tees real pull output and an "Image ready" marker into the log', () => {
|
|
94
|
+
if (!isDockerAvailable()) {
|
|
95
|
+
console.log = originalLog;
|
|
96
|
+
console.log(' Skipping: docker daemon not available');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (process.platform === 'win32') {
|
|
100
|
+
// Windows Docker runners pull Linux images slowly/unreliably and the
|
|
101
|
+
// capture path is exercised by the failed-pull test above; skip to avoid
|
|
102
|
+
// network-dependent timeouts. The real-pull streaming path is Unix-only.
|
|
103
|
+
console.log = originalLog;
|
|
104
|
+
console.log(' Skipping: real pull is unreliable on Windows Docker');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Use a tiny image and force a real pull by removing it first.
|
|
109
|
+
const image = 'hello-world:latest';
|
|
110
|
+
try {
|
|
111
|
+
require('child_process').execSync(`docker rmi ${image}`, {
|
|
112
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
113
|
+
});
|
|
114
|
+
} catch {
|
|
115
|
+
// image not present locally — that's fine, the pull below still runs
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const logPath = makeTempLog();
|
|
119
|
+
try {
|
|
120
|
+
const result = dockerPullImage(image, logPath);
|
|
121
|
+
if (!result.success) {
|
|
122
|
+
// No network in this environment — cannot exercise the success path.
|
|
123
|
+
console.log = originalLog;
|
|
124
|
+
console.log(' Skipping: docker pull failed (no registry access?)');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const contents = fs.readFileSync(logPath, 'utf8');
|
|
129
|
+
assert.ok(
|
|
130
|
+
contents.includes(`Preparing image ${image}`),
|
|
131
|
+
'log must contain the "Preparing image …" start marker'
|
|
132
|
+
);
|
|
133
|
+
assert.ok(
|
|
134
|
+
/Image ready \(\d+\.\d+s\)/.test(contents),
|
|
135
|
+
'log must contain "Image ready (<duration>)" marker'
|
|
136
|
+
);
|
|
137
|
+
assert.ok(
|
|
138
|
+
contents.includes('Pulling from') ||
|
|
139
|
+
contents.includes('Status:') ||
|
|
140
|
+
contents.includes('Pull complete'),
|
|
141
|
+
'log must capture the teed docker pull progress output'
|
|
142
|
+
);
|
|
143
|
+
assert.ok(
|
|
144
|
+
dockerImageExists(image),
|
|
145
|
+
'image should exist locally after a successful pull'
|
|
146
|
+
);
|
|
147
|
+
} finally {
|
|
148
|
+
fs.rmSync(logPath, { force: true });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('does not write prep markers when no logPath is given (backward compat)', () => {
|
|
153
|
+
if (!isDockerAvailable()) {
|
|
154
|
+
console.log = originalLog;
|
|
155
|
+
console.log(' Skipping: docker daemon not available');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Without a logPath, dockerPullImage must still return the {success, output}
|
|
160
|
+
// shape and must not throw. We only assert the contract here.
|
|
161
|
+
const result = dockerPullImage('invalid..badname');
|
|
162
|
+
assert.strictEqual(result.success, false);
|
|
163
|
+
assert.strictEqual(typeof result.output, 'string');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('runInDocker threads logPath into dockerPullImage (issue #138)', () => {
|
|
168
|
+
it('source passes options.logPath to dockerPullImage', () => {
|
|
169
|
+
const isolationSrc = fs.readFileSync(
|
|
170
|
+
path.join(__dirname, '../src/lib/isolation.js'),
|
|
171
|
+
'utf8'
|
|
172
|
+
);
|
|
173
|
+
assert.ok(
|
|
174
|
+
/dockerPullImage\(options\.image,\s*options\.logPath\)/.test(
|
|
175
|
+
isolationSrc
|
|
176
|
+
),
|
|
177
|
+
'runInDocker must pass options.logPath to dockerPullImage'
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('dockerPullImage tees output and writes prep markers in source', () => {
|
|
182
|
+
const dockerUtilsSrc = fs.readFileSync(
|
|
183
|
+
path.join(__dirname, '../src/lib/docker-utils.js'),
|
|
184
|
+
'utf8'
|
|
185
|
+
);
|
|
186
|
+
assert.ok(
|
|
187
|
+
dockerUtilsSrc.includes('Preparing image'),
|
|
188
|
+
'docker-utils must write a "Preparing image …" marker'
|
|
189
|
+
);
|
|
190
|
+
assert.ok(
|
|
191
|
+
dockerUtilsSrc.includes('Image ready'),
|
|
192
|
+
'docker-utils must write an "Image ready (<duration>)" marker'
|
|
193
|
+
);
|
|
194
|
+
assert.ok(
|
|
195
|
+
dockerUtilsSrc.includes('tee -a'),
|
|
196
|
+
'docker-utils must tee docker pull output into the log file'
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
});
|