start-command 0.37.3 → 0.37.5
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/test/publish-to-crates.mjs +8 -0
- package/test/release-git.mjs +154 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.37.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1f6c17c: CI/CD: the release script checks the exit code of every git command and verifies the version commit reached main before publishing. The Rust bump skips versions crates.io already has (#211).
|
|
8
|
+
|
|
9
|
+
## 0.37.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 519ce86: CI/CD: the Rust release jobs receive the release deploy key again (the called workflow inherits secrets), and a rejected version push now fails the release instead of reporting success (#211).
|
|
14
|
+
|
|
3
15
|
## 0.37.3
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -135,6 +135,11 @@ describe('publish-to-crates script', () => {
|
|
|
135
135
|
expect(result.status).toBe(0);
|
|
136
136
|
expect(result.stdout).toContain('already_published=true');
|
|
137
137
|
expect(readFileSync(outputPath, 'utf8')).toContain('published=true');
|
|
138
|
+
// rust.yml fails the release when the version it just committed was
|
|
139
|
+
// already on crates.io before the publish step (#211).
|
|
140
|
+
expect(readFileSync(outputPath, 'utf8')).toContain(
|
|
141
|
+
'published_before_run=true'
|
|
142
|
+
);
|
|
138
143
|
expect(requests).toEqual(['/api/v1/crates/example-crate/1.2.3']);
|
|
139
144
|
} finally {
|
|
140
145
|
rmSync(tempDir, { force: true, recursive: true });
|
|
@@ -179,6 +184,9 @@ describe('publish-to-crates script', () => {
|
|
|
179
184
|
expect(result.status).toBe(0);
|
|
180
185
|
expect(result.stdout).toContain('publish_result=published');
|
|
181
186
|
expect(readFileSync(outputPath, 'utf8')).toContain('published=true');
|
|
187
|
+
expect(readFileSync(outputPath, 'utf8')).not.toContain(
|
|
188
|
+
'published_before_run'
|
|
189
|
+
);
|
|
182
190
|
expect(JSON.parse(readFileSync(cargoArgsPath, 'utf8'))).toEqual([
|
|
183
191
|
'publish',
|
|
184
192
|
'--allow-dirty',
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { $ } from 'command-stream';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
bumpVersion,
|
|
12
|
+
checked,
|
|
13
|
+
isCrateVersionPublished,
|
|
14
|
+
nextUnpublishedVersion,
|
|
15
|
+
verifyCommitOnRemote,
|
|
16
|
+
} from '../../scripts/release-git.mjs';
|
|
17
|
+
|
|
18
|
+
function git(cwd, ...args) {
|
|
19
|
+
return execFileSync('git', args, { cwd, stdio: 'pipe' }).toString().trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A clone of a bare `main` remote. With `reject`, the remote's pre-receive
|
|
24
|
+
* hook declines every push, the way ruleset 24848039 declined the
|
|
25
|
+
* GITHUB_TOKEN push of the Rust Auto Release job (#211).
|
|
26
|
+
*/
|
|
27
|
+
async function withRepo({ reject }, callback) {
|
|
28
|
+
const dir = mkdtempSync(join(tmpdir(), 'release-git-'));
|
|
29
|
+
const previousCwd = process.cwd();
|
|
30
|
+
try {
|
|
31
|
+
const remote = join(dir, 'remote.git');
|
|
32
|
+
const work = join(dir, 'work');
|
|
33
|
+
git(dir, 'init', '--bare', '-b', 'main', remote);
|
|
34
|
+
git(dir, 'clone', remote, work);
|
|
35
|
+
git(work, 'config', 'user.name', 'test');
|
|
36
|
+
git(work, 'config', 'user.email', 'test@example.com');
|
|
37
|
+
git(work, 'commit', '--allow-empty', '-m', 'initial');
|
|
38
|
+
git(work, 'push', 'origin', 'main');
|
|
39
|
+
if (reject) {
|
|
40
|
+
const hook = join(remote, 'hooks', 'pre-receive');
|
|
41
|
+
writeFileSync(
|
|
42
|
+
hook,
|
|
43
|
+
'#!/bin/sh\necho "error: GH013: Repository rule violations found for refs/heads/main." >&2\nexit 1\n'
|
|
44
|
+
);
|
|
45
|
+
chmodSync(hook, 0o755);
|
|
46
|
+
}
|
|
47
|
+
git(work, 'commit', '--allow-empty', '-m', 'rust-v0.25.0');
|
|
48
|
+
process.chdir(work);
|
|
49
|
+
return await callback(work);
|
|
50
|
+
} finally {
|
|
51
|
+
process.chdir(previousCwd);
|
|
52
|
+
rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function withCratesServer(publishedVersions, callback) {
|
|
57
|
+
const server = createServer((request, response) => {
|
|
58
|
+
const version = request.url.split('/').pop();
|
|
59
|
+
if (version === 'broken') {
|
|
60
|
+
response.writeHead(503);
|
|
61
|
+
} else {
|
|
62
|
+
response.writeHead(publishedVersions.includes(version) ? 200 : 404);
|
|
63
|
+
}
|
|
64
|
+
response.end('{}');
|
|
65
|
+
});
|
|
66
|
+
await new Promise((resolveListen) =>
|
|
67
|
+
server.listen(0, '127.0.0.1', resolveListen)
|
|
68
|
+
);
|
|
69
|
+
const previous = process.env.CRATES_IO_BASE_URL;
|
|
70
|
+
process.env.CRATES_IO_BASE_URL = `http://127.0.0.1:${server.address().port}/api/v1`;
|
|
71
|
+
try {
|
|
72
|
+
return await callback();
|
|
73
|
+
} finally {
|
|
74
|
+
if (previous === undefined) {
|
|
75
|
+
delete process.env.CRATES_IO_BASE_URL;
|
|
76
|
+
} else {
|
|
77
|
+
process.env.CRATES_IO_BASE_URL = previous;
|
|
78
|
+
}
|
|
79
|
+
await new Promise((resolveClose) => server.close(resolveClose));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const quiet = { capture: true, mirror: false };
|
|
84
|
+
|
|
85
|
+
describe('release-git (#211)', () => {
|
|
86
|
+
it('reproduces the bug: command-stream resolves on a rejected push', async () => {
|
|
87
|
+
await withRepo({ reject: true }, async () => {
|
|
88
|
+
const result = await $`git push origin main`.run(quiet);
|
|
89
|
+
expect(result.code).not.toBe(0);
|
|
90
|
+
expect(result.stderr).toContain('GH013');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('checked() throws when the push is rejected', async () => {
|
|
95
|
+
await withRepo({ reject: true }, async () => {
|
|
96
|
+
await expect(
|
|
97
|
+
checked($`git push origin main`.run(quiet), 'git push origin main')
|
|
98
|
+
).rejects.toThrow('git push origin main failed with exit code 1');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('verifyCommitOnRemote() fails when the commit never reached main', async () => {
|
|
103
|
+
await withRepo({ reject: true }, async () => {
|
|
104
|
+
await $`git push origin main`.run(quiet);
|
|
105
|
+
await expect(verifyCommitOnRemote($)).rejects.toThrow(
|
|
106
|
+
'did not reach main'
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('verifyCommitOnRemote() accepts a pushed commit, even after main moves on', async () => {
|
|
112
|
+
await withRepo({ reject: false }, async (work) => {
|
|
113
|
+
await checked($`git push origin main`.run(quiet), 'git push origin main');
|
|
114
|
+
const pushed = git(work, 'rev-parse', 'HEAD');
|
|
115
|
+
// Another writer lands a commit on main right after our push.
|
|
116
|
+
git(work, 'commit', '--allow-empty', '-m', 'later');
|
|
117
|
+
git(work, 'push', 'origin', 'main');
|
|
118
|
+
git(work, 'reset', '--hard', pushed);
|
|
119
|
+
expect(await verifyCommitOnRemote($)).toBe(pushed);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('bumpVersion() follows semver', () => {
|
|
124
|
+
expect(bumpVersion('0.23.0', 'minor')).toBe('0.24.0');
|
|
125
|
+
expect(bumpVersion('0.23.4', 'patch')).toBe('0.23.5');
|
|
126
|
+
expect(bumpVersion('0.23.4', 'major')).toBe('1.0.0');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('skips 0.24.0, which crates.io had from an older tree, and picks 0.25.0', async () => {
|
|
130
|
+
await withCratesServer(['0.23.0', '0.24.0'], async () => {
|
|
131
|
+
const version = await nextUnpublishedVersion('0.23.0', 'minor', (v) =>
|
|
132
|
+
isCrateVersionPublished('start-command', v)
|
|
133
|
+
);
|
|
134
|
+
expect(version).toBe('0.25.0');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('keeps the plain bump when the next version is free', async () => {
|
|
139
|
+
await withCratesServer(['0.23.0'], async () => {
|
|
140
|
+
const version = await nextUnpublishedVersion('0.23.0', 'patch', (v) =>
|
|
141
|
+
isCrateVersionPublished('start-command', v)
|
|
142
|
+
);
|
|
143
|
+
expect(version).toBe('0.23.1');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('fails closed when crates.io answers neither 200 nor 404', async () => {
|
|
148
|
+
await withCratesServer([], async () => {
|
|
149
|
+
await expect(
|
|
150
|
+
isCrateVersionPublished('start-command', 'broken')
|
|
151
|
+
).rejects.toThrow('Unexpected crates.io response 503');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|