start-command 0.27.0 → 0.27.1
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 +6 -0
- package/package.json +1 -1
- package/test/create-github-release.mjs +118 -0
- package/test/isolation-cleanup.js +120 -109
- package/test/publish-to-crates.mjs +194 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const testDir = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const repoRoot = resolve(testDir, '../..');
|
|
10
|
+
const scriptPath = resolve(repoRoot, 'scripts/create-github-release.mjs');
|
|
11
|
+
|
|
12
|
+
function createFakeGhBin(tempDir) {
|
|
13
|
+
const fakeGhJs = join(tempDir, 'fake-gh.cjs');
|
|
14
|
+
writeFileSync(
|
|
15
|
+
fakeGhJs,
|
|
16
|
+
`
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
|
|
19
|
+
let input = '';
|
|
20
|
+
process.stdin.setEncoding('utf8');
|
|
21
|
+
process.stdin.on('data', (chunk) => {
|
|
22
|
+
input += chunk;
|
|
23
|
+
});
|
|
24
|
+
process.stdin.on('end', () => {
|
|
25
|
+
fs.writeFileSync(process.env.FAKE_GH_PAYLOAD_PATH, input);
|
|
26
|
+
|
|
27
|
+
if (process.env.FAKE_GH_MODE === 'already_exists') {
|
|
28
|
+
console.error('gh: Validation Failed (HTTP 422)');
|
|
29
|
+
console.error(JSON.stringify({
|
|
30
|
+
message: 'Validation Failed',
|
|
31
|
+
errors: [{ resource: 'Release', code: 'already_exists', field: 'tag_name' }],
|
|
32
|
+
}));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (process.env.FAKE_GH_MODE === 'failure') {
|
|
37
|
+
console.error('gh: server exploded');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(JSON.stringify({ html_url: 'https://github.test/release' }));
|
|
42
|
+
process.exit(0);
|
|
43
|
+
});
|
|
44
|
+
`
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return fakeGhJs;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function runScript(mode) {
|
|
51
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'create-release-test-'));
|
|
52
|
+
const payloadPath = join(tempDir, 'payload.json');
|
|
53
|
+
const changelogPath = join(tempDir, 'CHANGELOG.md');
|
|
54
|
+
writeFileSync(
|
|
55
|
+
changelogPath,
|
|
56
|
+
'# Changelog\n\n## [1.2.3] - 2026-05-03\n\n- Release automation fix.\n'
|
|
57
|
+
);
|
|
58
|
+
const fakeGhJs = createFakeGhBin(tempDir);
|
|
59
|
+
|
|
60
|
+
const result = spawnSync(
|
|
61
|
+
'node',
|
|
62
|
+
[
|
|
63
|
+
scriptPath,
|
|
64
|
+
'--release-version',
|
|
65
|
+
'1.2.3',
|
|
66
|
+
'--repository',
|
|
67
|
+
'owner/repo',
|
|
68
|
+
'--prefix',
|
|
69
|
+
'rust-',
|
|
70
|
+
'--changelog-file',
|
|
71
|
+
changelogPath,
|
|
72
|
+
'--badge-type',
|
|
73
|
+
'crates',
|
|
74
|
+
'--package-name',
|
|
75
|
+
'start-command',
|
|
76
|
+
],
|
|
77
|
+
{
|
|
78
|
+
encoding: 'utf8',
|
|
79
|
+
env: {
|
|
80
|
+
...process.env,
|
|
81
|
+
FAKE_GH_MODE: mode,
|
|
82
|
+
FAKE_GH_PAYLOAD_PATH: payloadPath,
|
|
83
|
+
START_GH_COMMAND: process.execPath,
|
|
84
|
+
START_GH_COMMAND_ARGS: JSON.stringify([fakeGhJs]),
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const payload = JSON.parse(readFileSync(payloadPath, 'utf8'));
|
|
90
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
91
|
+
return { result, payload };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('create-github-release script', () => {
|
|
95
|
+
it('treats an existing GitHub release as an idempotent skip', () => {
|
|
96
|
+
const { result, payload } = runScript('already_exists');
|
|
97
|
+
|
|
98
|
+
expect(result.status).toBe(0);
|
|
99
|
+
expect(result.stdout).toContain(
|
|
100
|
+
'GitHub release already exists: rust-v1.2.3'
|
|
101
|
+
);
|
|
102
|
+
expect(result.stdout).not.toContain('Created GitHub release');
|
|
103
|
+
expect(payload).toMatchObject({
|
|
104
|
+
tag_name: 'rust-v1.2.3',
|
|
105
|
+
name: '[Rust] 1.2.3',
|
|
106
|
+
});
|
|
107
|
+
expect(payload.body).toContain('Release automation fix.');
|
|
108
|
+
expect(payload.body).toContain('crates.io');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('fails when gh api returns an unexpected error', () => {
|
|
112
|
+
const { result } = runScript('failure');
|
|
113
|
+
|
|
114
|
+
expect(result.status).not.toBe(0);
|
|
115
|
+
expect(result.stderr).toContain('server exploded');
|
|
116
|
+
expect(result.stderr).toContain('GitHub release creation failed');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -241,29 +241,116 @@ describe('Isolation Resource Cleanup Verification', () => {
|
|
|
241
241
|
// Use the canRunLinuxDockerImages function from isolation module
|
|
242
242
|
// to properly detect if Linux containers can run (handles Windows containers mode)
|
|
243
243
|
const { canRunLinuxDockerImages } = require('../src/lib/isolation');
|
|
244
|
+
const DOCKER_TEST_TIMEOUT = process.platform === 'win32' ? 30000 : 20000;
|
|
245
|
+
const DOCKER_STATE_WAIT_TIMEOUT =
|
|
246
|
+
process.platform === 'win32' ? 20000 : 10000;
|
|
247
|
+
|
|
248
|
+
it(
|
|
249
|
+
'should show docker container as exited after command completes (auto-exit by default)',
|
|
250
|
+
{ timeout: DOCKER_TEST_TIMEOUT },
|
|
251
|
+
async () => {
|
|
252
|
+
if (!canRunLinuxDockerImages()) {
|
|
253
|
+
console.log(
|
|
254
|
+
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
255
|
+
);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
244
258
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
259
|
+
const containerName = `test-cleanup-docker-${Date.now()}`;
|
|
260
|
+
|
|
261
|
+
// Run a quick command in detached mode
|
|
262
|
+
const result = await runInDocker('echo "test" && sleep 0.1', {
|
|
263
|
+
image: 'alpine:latest',
|
|
264
|
+
session: containerName,
|
|
265
|
+
detached: true,
|
|
266
|
+
keepAlive: false,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
assert.strictEqual(result.success, true);
|
|
270
|
+
|
|
271
|
+
// Wait for the container to exit
|
|
272
|
+
const containerExited = await waitFor(() => {
|
|
273
|
+
try {
|
|
274
|
+
const status = execSync(
|
|
275
|
+
`docker inspect -f '{{.State.Status}}' ${containerName}`,
|
|
276
|
+
{
|
|
277
|
+
encoding: 'utf8',
|
|
278
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
279
|
+
}
|
|
280
|
+
).trim();
|
|
281
|
+
return status === 'exited';
|
|
282
|
+
} catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}, DOCKER_STATE_WAIT_TIMEOUT);
|
|
286
|
+
|
|
287
|
+
assert.ok(
|
|
288
|
+
containerExited,
|
|
289
|
+
'Docker container should be in exited state after command completes (auto-exit by default)'
|
|
249
290
|
);
|
|
250
|
-
|
|
291
|
+
|
|
292
|
+
// Verify with docker ps -a that container is exited (not running)
|
|
293
|
+
try {
|
|
294
|
+
const allContainers = execSync('docker ps -a', {
|
|
295
|
+
encoding: 'utf8',
|
|
296
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
297
|
+
});
|
|
298
|
+
assert.ok(
|
|
299
|
+
allContainers.includes(containerName),
|
|
300
|
+
'Container should appear in docker ps -a'
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const runningContainers = execSync('docker ps', {
|
|
304
|
+
encoding: 'utf8',
|
|
305
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
306
|
+
});
|
|
307
|
+
assert.ok(
|
|
308
|
+
!runningContainers.includes(containerName),
|
|
309
|
+
'Container should NOT appear in docker ps (not running)'
|
|
310
|
+
);
|
|
311
|
+
console.log(
|
|
312
|
+
' ✓ Docker container auto-exited and stopped (resources released, filesystem preserved)'
|
|
313
|
+
);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
assert.fail(`Failed to verify container status: ${err.message}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Clean up
|
|
319
|
+
try {
|
|
320
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
321
|
+
} catch {
|
|
322
|
+
// Ignore cleanup errors
|
|
323
|
+
}
|
|
251
324
|
}
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
it(
|
|
328
|
+
'should keep docker container running when keepAlive is true',
|
|
329
|
+
{ timeout: DOCKER_TEST_TIMEOUT },
|
|
330
|
+
async () => {
|
|
331
|
+
if (!canRunLinuxDockerImages()) {
|
|
332
|
+
console.log(
|
|
333
|
+
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
334
|
+
);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
252
337
|
|
|
253
|
-
|
|
338
|
+
const containerName = `test-keepalive-docker-${Date.now()}`;
|
|
254
339
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
340
|
+
// Run command with keepAlive enabled
|
|
341
|
+
const result = await runInDocker('echo "test"', {
|
|
342
|
+
image: 'alpine:latest',
|
|
343
|
+
session: containerName,
|
|
344
|
+
detached: true,
|
|
345
|
+
keepAlive: true,
|
|
346
|
+
});
|
|
262
347
|
|
|
263
|
-
|
|
348
|
+
assert.strictEqual(result.success, true);
|
|
349
|
+
|
|
350
|
+
// Wait a bit for the command to complete
|
|
351
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
264
352
|
|
|
265
|
-
|
|
266
|
-
const containerExited = await waitFor(() => {
|
|
353
|
+
// Container should still be running
|
|
267
354
|
try {
|
|
268
355
|
const status = execSync(
|
|
269
356
|
`docker inspect -f '{{.State.Status}}' ${containerName}`,
|
|
@@ -272,101 +359,25 @@ describe('Isolation Resource Cleanup Verification', () => {
|
|
|
272
359
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
273
360
|
}
|
|
274
361
|
).trim();
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
362
|
+
assert.strictEqual(
|
|
363
|
+
status,
|
|
364
|
+
'running',
|
|
365
|
+
'Container should still be running with keepAlive=true'
|
|
366
|
+
);
|
|
367
|
+
console.log(
|
|
368
|
+
' ✓ Docker container kept running as expected with --keep-alive'
|
|
369
|
+
);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
assert.fail(`Failed to verify container is running: ${err.message}`);
|
|
278
372
|
}
|
|
279
|
-
}, 10000);
|
|
280
|
-
|
|
281
|
-
assert.ok(
|
|
282
|
-
containerExited,
|
|
283
|
-
'Docker container should be in exited state after command completes (auto-exit by default)'
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
// Verify with docker ps -a that container is exited (not running)
|
|
287
|
-
try {
|
|
288
|
-
const allContainers = execSync('docker ps -a', {
|
|
289
|
-
encoding: 'utf8',
|
|
290
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
291
|
-
});
|
|
292
|
-
assert.ok(
|
|
293
|
-
allContainers.includes(containerName),
|
|
294
|
-
'Container should appear in docker ps -a'
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
const runningContainers = execSync('docker ps', {
|
|
298
|
-
encoding: 'utf8',
|
|
299
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
300
|
-
});
|
|
301
|
-
assert.ok(
|
|
302
|
-
!runningContainers.includes(containerName),
|
|
303
|
-
'Container should NOT appear in docker ps (not running)'
|
|
304
|
-
);
|
|
305
|
-
console.log(
|
|
306
|
-
' ✓ Docker container auto-exited and stopped (resources released, filesystem preserved)'
|
|
307
|
-
);
|
|
308
|
-
} catch (err) {
|
|
309
|
-
assert.fail(`Failed to verify container status: ${err.message}`);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Clean up
|
|
313
|
-
try {
|
|
314
|
-
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
315
|
-
} catch {
|
|
316
|
-
// Ignore cleanup errors
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it('should keep docker container running when keepAlive is true', async () => {
|
|
321
|
-
if (!canRunLinuxDockerImages()) {
|
|
322
|
-
console.log(
|
|
323
|
-
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
324
|
-
);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const containerName = `test-keepalive-docker-${Date.now()}`;
|
|
329
|
-
|
|
330
|
-
// Run command with keepAlive enabled
|
|
331
|
-
const result = await runInDocker('echo "test"', {
|
|
332
|
-
image: 'alpine:latest',
|
|
333
|
-
session: containerName,
|
|
334
|
-
detached: true,
|
|
335
|
-
keepAlive: true,
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
assert.strictEqual(result.success, true);
|
|
339
|
-
|
|
340
|
-
// Wait a bit for the command to complete
|
|
341
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
342
373
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
350
|
-
}
|
|
351
|
-
).trim();
|
|
352
|
-
assert.strictEqual(
|
|
353
|
-
status,
|
|
354
|
-
'running',
|
|
355
|
-
'Container should still be running with keepAlive=true'
|
|
356
|
-
);
|
|
357
|
-
console.log(
|
|
358
|
-
' ✓ Docker container kept running as expected with --keep-alive'
|
|
359
|
-
);
|
|
360
|
-
} catch (err) {
|
|
361
|
-
assert.fail(`Failed to verify container is running: ${err.message}`);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Clean up
|
|
365
|
-
try {
|
|
366
|
-
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
367
|
-
} catch {
|
|
368
|
-
// Ignore cleanup errors
|
|
374
|
+
// Clean up
|
|
375
|
+
try {
|
|
376
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
377
|
+
} catch {
|
|
378
|
+
// Ignore cleanup errors
|
|
379
|
+
}
|
|
369
380
|
}
|
|
370
|
-
|
|
381
|
+
);
|
|
371
382
|
});
|
|
372
383
|
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import {
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { createServer } from 'node:http';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { dirname, join, resolve } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const testDir = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const repoRoot = resolve(testDir, '../..');
|
|
17
|
+
const scriptPath = resolve(repoRoot, 'scripts/publish-to-crates.mjs');
|
|
18
|
+
|
|
19
|
+
function createCargoPackage(tempDir) {
|
|
20
|
+
const packageDir = join(tempDir, 'crate');
|
|
21
|
+
const cargoTomlPath = join(packageDir, 'Cargo.toml');
|
|
22
|
+
mkdirSync(packageDir);
|
|
23
|
+
writeFileSync(
|
|
24
|
+
cargoTomlPath,
|
|
25
|
+
'[package]\nname = "example-crate"\nversion = "1.2.3"\nedition = "2021"\n'
|
|
26
|
+
);
|
|
27
|
+
return packageDir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createFakeCargoBin(tempDir) {
|
|
31
|
+
const fakeCargoJs = join(tempDir, 'fake-cargo.cjs');
|
|
32
|
+
writeFileSync(
|
|
33
|
+
fakeCargoJs,
|
|
34
|
+
`
|
|
35
|
+
const fs = require('node:fs');
|
|
36
|
+
fs.writeFileSync(process.env.FAKE_CARGO_ARGS_PATH, JSON.stringify(process.argv.slice(2)));
|
|
37
|
+
process.exit(Number(process.env.FAKE_CARGO_EXIT || 0));
|
|
38
|
+
`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return fakeCargoJs;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function withCratesServer(status, callback) {
|
|
45
|
+
const requests = [];
|
|
46
|
+
const server = createServer((request, response) => {
|
|
47
|
+
requests.push(request.url);
|
|
48
|
+
response.writeHead(status, { 'content-type': 'application/json' });
|
|
49
|
+
response.end(JSON.stringify({ ok: status === 200 }));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await new Promise((resolveListen) =>
|
|
53
|
+
server.listen(0, '127.0.0.1', resolveListen)
|
|
54
|
+
);
|
|
55
|
+
const { port } = server.address();
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return await callback(`http://127.0.0.1:${port}/api/v1`, requests);
|
|
59
|
+
} finally {
|
|
60
|
+
await new Promise((resolveClose) => server.close(resolveClose));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function runScript({
|
|
65
|
+
cargoToken = '',
|
|
66
|
+
cratesIoBaseUrl,
|
|
67
|
+
fakeCargo = false,
|
|
68
|
+
packageDir,
|
|
69
|
+
tempDir,
|
|
70
|
+
}) {
|
|
71
|
+
const outputPath = join(tempDir, 'github-output.txt');
|
|
72
|
+
const cargoArgsPath = join(tempDir, 'cargo-args.json');
|
|
73
|
+
const fakeCargoJs = fakeCargo ? createFakeCargoBin(tempDir) : '';
|
|
74
|
+
|
|
75
|
+
const env = {
|
|
76
|
+
...process.env,
|
|
77
|
+
CARGO_REGISTRY_TOKEN: cargoToken,
|
|
78
|
+
CRATES_IO_BASE_URL: cratesIoBaseUrl,
|
|
79
|
+
CRATES_PUBLISH_RETRY_DELAY_MS: '1',
|
|
80
|
+
FAKE_CARGO_ARGS_PATH: cargoArgsPath,
|
|
81
|
+
GITHUB_OUTPUT: outputPath,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (fakeCargoJs) {
|
|
85
|
+
env.START_CARGO_COMMAND = process.execPath;
|
|
86
|
+
env.START_CARGO_COMMAND_ARGS = JSON.stringify([fakeCargoJs]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return new Promise((resolveRun, rejectRun) => {
|
|
90
|
+
const child = spawn('node', [scriptPath, '--working-dir', packageDir], {
|
|
91
|
+
cwd: repoRoot,
|
|
92
|
+
env,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
let stdout = '';
|
|
96
|
+
let stderr = '';
|
|
97
|
+
child.stdout.setEncoding('utf8');
|
|
98
|
+
child.stderr.setEncoding('utf8');
|
|
99
|
+
child.stdout.on('data', (chunk) => {
|
|
100
|
+
stdout += chunk;
|
|
101
|
+
});
|
|
102
|
+
child.stderr.on('data', (chunk) => {
|
|
103
|
+
stderr += chunk;
|
|
104
|
+
});
|
|
105
|
+
child.on('error', rejectRun);
|
|
106
|
+
child.on('close', (status, signal) => {
|
|
107
|
+
resolveRun({
|
|
108
|
+
cargoArgsPath,
|
|
109
|
+
outputPath,
|
|
110
|
+
result: {
|
|
111
|
+
signal,
|
|
112
|
+
status,
|
|
113
|
+
stderr,
|
|
114
|
+
stdout,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
describe('publish-to-crates script', () => {
|
|
122
|
+
it('reports success without a token when the version is already published', async () => {
|
|
123
|
+
await withCratesServer(200, async (cratesIoBaseUrl, requests) => {
|
|
124
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'publish-crates-test-'));
|
|
125
|
+
const packageDir = createCargoPackage(tempDir);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const { outputPath, result } = await runScript({
|
|
129
|
+
cratesIoBaseUrl,
|
|
130
|
+
packageDir,
|
|
131
|
+
tempDir,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result.status).toBe(0);
|
|
135
|
+
expect(result.stdout).toContain('already_published=true');
|
|
136
|
+
expect(readFileSync(outputPath, 'utf8')).toContain('published=true');
|
|
137
|
+
expect(requests).toEqual(['/api/v1/crates/example-crate/1.2.3']);
|
|
138
|
+
} finally {
|
|
139
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('fails clearly when a missing version needs a crates token', async () => {
|
|
145
|
+
await withCratesServer(404, async (cratesIoBaseUrl) => {
|
|
146
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'publish-crates-test-'));
|
|
147
|
+
const packageDir = createCargoPackage(tempDir);
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const { result } = await runScript({
|
|
151
|
+
cratesIoBaseUrl,
|
|
152
|
+
packageDir,
|
|
153
|
+
tempDir,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(result.status).not.toBe(0);
|
|
157
|
+
expect(result.stderr).toContain('Missing crates.io token');
|
|
158
|
+
} finally {
|
|
159
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('publishes a missing version with cargo publish', async () => {
|
|
165
|
+
await withCratesServer(404, async (cratesIoBaseUrl) => {
|
|
166
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'publish-crates-test-'));
|
|
167
|
+
const packageDir = createCargoPackage(tempDir);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const { cargoArgsPath, outputPath, result } = await runScript({
|
|
171
|
+
cargoToken: 'test-token',
|
|
172
|
+
cratesIoBaseUrl,
|
|
173
|
+
fakeCargo: true,
|
|
174
|
+
packageDir,
|
|
175
|
+
tempDir,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(result.status).toBe(0);
|
|
179
|
+
expect(result.stdout).toContain('publish_result=published');
|
|
180
|
+
expect(readFileSync(outputPath, 'utf8')).toContain('published=true');
|
|
181
|
+
expect(JSON.parse(readFileSync(cargoArgsPath, 'utf8'))).toEqual([
|
|
182
|
+
'publish',
|
|
183
|
+
'--allow-dirty',
|
|
184
|
+
'--manifest-path',
|
|
185
|
+
join(packageDir, 'Cargo.toml'),
|
|
186
|
+
'--token',
|
|
187
|
+
'test-token',
|
|
188
|
+
]);
|
|
189
|
+
} finally {
|
|
190
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|