1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
import { afterEach, describe, expect, test } from 'bun:test';
import { execFileSync } from 'node:child_process';
import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
const script = resolve(import.meta.dir, 'versions.mjs');
const temporaryDirectories: string[] = [];
// The fixture shells out to node and git many times. A healthy Windows runner
// finishes in seconds, but a degraded one can run an order of magnitude slower,
// so keep a generous ceiling instead of making the suite a load-sensitive flake.
const fixtureTimeoutMs = process.platform === 'win32' ? 120_000 : 30_000;
afterEach(async () => {
await Promise.all(
temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })),
);
});
describe('documentation release publishing', () => {
test('publishes immutable tagged snapshots and validates maintained corrections', async () => {
const root = await mkdtemp(join(tmpdir(), 'herdr-docs-'));
temporaryDirectories.push(root);
await write(root, 'distribution/latest.json', '{"version":"0.9.0"}\n');
await write(root, 'README.md', 'stable readme\n');
await write(root, 'README.zh-CN.md', 'stable readme zh-cn\n');
const nextDocs = '---\ntitle: Documentation\n---\n\nnext docs\n';
await write(root, 'docs/next/website/src/content/docs/index.mdx', nextDocs);
await write(root, 'docs/next/website/src/data/config-reference.json', '{"next":true}\n');
await write(root, 'docs/next/README.md', 'next readme\n');
await write(root, 'docs/next/README.zh-CN.md', 'next readme zh-cn\n');
git(root, ['init', '-q']);
git(root, ['config', 'user.email', 'test@example.com']);
git(root, ['config', 'user.name', 'Test']);
git(root, ['add', '.']);
git(root, ['commit', '-qm', 'release fixture']);
git(root, ['tag', 'v0.9.0']);
git(root, ['tag', 'v1.0.0']);
runScript(root, ['publish', 'v1.0.0']);
expect(await read(root, 'README.md')).toBe('next readme\n');
expect(await read(root, 'README.zh-CN.md')).toBe('next readme zh-cn\n');
expect(await read(root, 'docs/versions/1.0.0/website/src/content/docs/index.mdx')).toBe(
nextDocs,
);
const manifest = JSON.parse(await read(root, 'docs/versions/manifest.json'));
expect(manifest.current).toBe('1.0.0');
expect(manifest.stable_source).toBe('snapshot');
expect(manifest.versions[0]).toMatchObject({
version: '1.0.0',
tag: 'v1.0.0',
commit: expect.stringMatching(/^[0-9a-f]{40}$/),
source: 'docs/next/website/src/content/docs',
});
await write(root, 'distribution/latest.json', '{"version":"1.0.0"}\n');
runScript(root, ['check']);
const correctedDocs = '---\ntitle: Documentation\n---\n\ncorrected docs\n';
await write(root, 'docs/versions/1.0.0/website/src/content/docs/index.mdx', correctedDocs);
await write(
root,
'docs/versions/1.0.0/website/src/data/config-reference.json',
'{"corrected":true}\n',
);
runScript(root, ['check']);
await write(root, 'README.md', 'post-release correction\n');
runScript(root, ['publish', 'v1.0.0']);
expect(await read(root, 'README.md')).toBe('post-release correction\n');
runScript(root, ['publish', 'v0.9.0']);
const archivedManifest = JSON.parse(await read(root, 'docs/versions/manifest.json'));
expect(archivedManifest.current).toBe('1.0.0');
expect(archivedManifest.versions.map(({ version }) => version)).toEqual(['1.0.0', '0.9.0']);
expect(await read(root, 'README.md')).toBe('post-release correction\n');
runScript(root, ['check']);
const correctedArchivedDocs = '---\ntitle: Documentation\n---\n\ncorrected archived docs\n';
await write(
root,
'docs/versions/0.9.0/website/src/content/docs/index.mdx',
correctedArchivedDocs,
);
runScript(root, ['publish', 'v0.9.0']);
expect(await read(root, 'docs/versions/0.9.0/website/src/content/docs/index.mdx')).toBe(
correctedArchivedDocs,
);
const archivedDocsRoot = resolve(root, 'docs/versions/0.9.0/website/src/content/docs');
await rm(archivedDocsRoot, { recursive: true, force: true });
expect(() => runScript(root, ['check'])).toThrow();
await write(
root,
'docs/versions/0.9.0/website/src/content/docs/index.mdx',
correctedArchivedDocs,
);
const linkedPage = resolve(archivedDocsRoot, 'linked.mdx');
await symlink('index.mdx', linkedPage);
expect(() => runScript(root, ['check'])).toThrow();
await rm(linkedPage);
runScript(root, ['check']);
delete archivedManifest.versions[0].commit;
await write(root, 'docs/versions/manifest.json', `${JSON.stringify(archivedManifest)}\n`);
expect(() => runScript(root, ['check'])).toThrow();
}, fixtureTimeoutMs);
});
async function write(root: string, path: string, content: string) {
const destination = resolve(root, path);
await mkdir(resolve(destination, '..'), { recursive: true });
await writeFile(destination, content, 'utf8');
}
async function read(root: string, path: string) {
return readFile(resolve(root, path), 'utf8');
}
function git(root: string, args: string[]) {
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: 'pipe' });
}
function runScript(root: string, args: string[]) {
execFileSync('node', [script, ...args], {
cwd: root,
env: { ...process.env, HERDR_DOCS_REPO_ROOT: root },
stdio: 'pipe',
});
}
|