@logicseek/hello-cc 0.1.2 → 0.1.3
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 +86 -3
- package/README.md +4 -1
- package/README.zh-CN.md +5 -1
- package/bin/hcc.mjs +2307 -279
- package/docs/README.md +5 -3
- package/docs/README.zh-CN.md +5 -3
- package/docs/commands.md +38 -7
- package/docs/commands.zh-CN.md +31 -6
- package/docs/design.md +12 -4
- package/docs/guide.md +55 -10
- package/docs/guide.zh-CN.md +47 -9
- package/docs/implementation.md +21 -1
- package/package.json +3 -1
- package/scripts/github-release.mjs +190 -0
- package/scripts/regression.mjs +804 -32
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
|
|
7
|
+
const repoRoot = path.resolve(import.meta.dirname, '..');
|
|
8
|
+
const githubApi = 'https://api.github.com';
|
|
9
|
+
|
|
10
|
+
function usage() {
|
|
11
|
+
console.log(`github-release
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
node scripts/github-release.mjs [--version VERSION] [--repo OWNER/REPO] [--dry-run]
|
|
15
|
+
|
|
16
|
+
Creates or updates the GitHub Release for vVERSION using the matching
|
|
17
|
+
CHANGELOG.md section as the release body.
|
|
18
|
+
|
|
19
|
+
Authentication:
|
|
20
|
+
Set GH_TOKEN or GITHUB_TOKEN to a token with repo release permissions.
|
|
21
|
+
`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readJson(file) {
|
|
25
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const opts = { dryRun: false, help: false, repo: null, version: null };
|
|
30
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
31
|
+
const arg = argv[i];
|
|
32
|
+
if (arg === '--help' || arg === '-h') {
|
|
33
|
+
opts.help = true;
|
|
34
|
+
} else if (arg === '--dry-run') {
|
|
35
|
+
opts.dryRun = true;
|
|
36
|
+
} else if (arg === '--repo') {
|
|
37
|
+
opts.repo = argv[++i];
|
|
38
|
+
if (opts.repo === undefined) throw new Error('Missing value for --repo');
|
|
39
|
+
} else if (arg.startsWith('--repo=')) {
|
|
40
|
+
opts.repo = arg.slice('--repo='.length);
|
|
41
|
+
} else if (arg === '--version') {
|
|
42
|
+
opts.version = argv[++i];
|
|
43
|
+
if (opts.version === undefined) throw new Error('Missing value for --version');
|
|
44
|
+
} else if (arg.startsWith('--version=')) {
|
|
45
|
+
opts.version = arg.slice('--version='.length);
|
|
46
|
+
} else {
|
|
47
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return opts;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeVersion(version) {
|
|
54
|
+
return String(version || '').replace(/^v/, '');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function releaseSection(markdown, version) {
|
|
58
|
+
const lines = markdown.split(/\r?\n/);
|
|
59
|
+
const heading = `## ${version}`;
|
|
60
|
+
const start = lines.findIndex((line) => line.trim() === heading);
|
|
61
|
+
if (start < 0) return null;
|
|
62
|
+
|
|
63
|
+
let end = lines.length;
|
|
64
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
65
|
+
if (/^##\s+/.test(lines[i])) {
|
|
66
|
+
end = i;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return lines.slice(start, end).join('\n').trimEnd();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateSection(section, version) {
|
|
74
|
+
const problems = [];
|
|
75
|
+
if (!section) {
|
|
76
|
+
problems.push(`CHANGELOG.md is missing section ## ${version}`);
|
|
77
|
+
return problems;
|
|
78
|
+
}
|
|
79
|
+
if (!section.includes('### Summary')) problems.push(`## ${version} is missing ### Summary`);
|
|
80
|
+
if (!section.includes('### Highlights')) problems.push(`## ${version} is missing ### Highlights`);
|
|
81
|
+
if (section.includes('TODO') || section.includes('TBD')) problems.push(`## ${version} still contains TODO/TBD text`);
|
|
82
|
+
const bulletCount = (section.match(/^- /gm) || []).length;
|
|
83
|
+
if (bulletCount < 2) problems.push(`## ${version} should include at least two release-note bullets`);
|
|
84
|
+
return problems;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function repoFromPackage(pkg) {
|
|
88
|
+
const repo = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
|
|
89
|
+
if (!repo) return null;
|
|
90
|
+
const match = repo.match(/github\.com[:/](.+?\/.+?)(?:\.git)?$/);
|
|
91
|
+
return match ? match[1] : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function envToken() {
|
|
95
|
+
return process.env.GH_TOKEN || process.env.GITHUB_TOKEN || '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function githubRequest(method, route, token, body = null) {
|
|
99
|
+
const response = await fetch(`${githubApi}${route}`, {
|
|
100
|
+
method,
|
|
101
|
+
headers: {
|
|
102
|
+
Accept: 'application/vnd.github+json',
|
|
103
|
+
Authorization: `Bearer ${token}`,
|
|
104
|
+
'Content-Type': 'application/json',
|
|
105
|
+
'User-Agent': 'hello-cc-release-script'
|
|
106
|
+
},
|
|
107
|
+
body: body ? JSON.stringify(body) : undefined
|
|
108
|
+
});
|
|
109
|
+
const text = await response.text();
|
|
110
|
+
const data = text ? JSON.parse(text) : null;
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const detail = data?.message ? `: ${data.message}` : '';
|
|
113
|
+
const error = new Error(`GitHub API ${method} ${route} failed with ${response.status}${detail}`);
|
|
114
|
+
error.status = response.status;
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
return data;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function main() {
|
|
121
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
122
|
+
if (opts.help) {
|
|
123
|
+
usage();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const pkg = readJson(path.join(repoRoot, 'package.json'));
|
|
128
|
+
const version = normalizeVersion(opts.version || pkg.version);
|
|
129
|
+
const tag = `v${version}`;
|
|
130
|
+
const repo = opts.repo || process.env.GITHUB_REPOSITORY || repoFromPackage(pkg);
|
|
131
|
+
if (!repo) throw new Error('Missing repository; pass --repo OWNER/REPO');
|
|
132
|
+
|
|
133
|
+
const changelog = fs.readFileSync(path.join(repoRoot, 'CHANGELOG.md'), 'utf8');
|
|
134
|
+
const body = releaseSection(changelog, version);
|
|
135
|
+
const problems = validateSection(body, version);
|
|
136
|
+
if (problems.length) {
|
|
137
|
+
console.error(problems.join('\n'));
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const payload = {
|
|
143
|
+
tag_name: tag,
|
|
144
|
+
name: tag,
|
|
145
|
+
body,
|
|
146
|
+
draft: false,
|
|
147
|
+
prerelease: false
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (opts.dryRun) {
|
|
151
|
+
console.log(JSON.stringify({
|
|
152
|
+
ok: true,
|
|
153
|
+
dry_run: true,
|
|
154
|
+
repo,
|
|
155
|
+
tag,
|
|
156
|
+
name: payload.name,
|
|
157
|
+
body_length: body.length,
|
|
158
|
+
body_preview: body.split(/\r?\n/).slice(0, 8).join('\n')
|
|
159
|
+
}, null, 2));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const token = envToken();
|
|
164
|
+
if (!token) throw new Error('Missing GH_TOKEN or GITHUB_TOKEN');
|
|
165
|
+
|
|
166
|
+
let existing = null;
|
|
167
|
+
try {
|
|
168
|
+
existing = await githubRequest('GET', `/repos/${repo}/releases/tags/${tag}`, token);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (err.status !== 404) throw err;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = existing
|
|
174
|
+
? await githubRequest('PATCH', `/repos/${repo}/releases/${existing.id}`, token, payload)
|
|
175
|
+
: await githubRequest('POST', `/repos/${repo}/releases`, token, payload);
|
|
176
|
+
|
|
177
|
+
console.log(JSON.stringify({
|
|
178
|
+
ok: true,
|
|
179
|
+
action: existing ? 'updated' : 'created',
|
|
180
|
+
repo,
|
|
181
|
+
tag,
|
|
182
|
+
url: result.html_url,
|
|
183
|
+
body_length: body.length
|
|
184
|
+
}, null, 2));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
main().catch((err) => {
|
|
188
|
+
console.error(err.message || String(err));
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
});
|