@oorabona/release-it-preset 0.3.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/LICENSE +21 -0
- package/README.md +833 -0
- package/bin/cli.js +180 -0
- package/bin/run-script.js +72 -0
- package/config/changelog-only.js +41 -0
- package/config/default.js +54 -0
- package/config/helpers.js +52 -0
- package/config/hotfix.js +51 -0
- package/config/manual-changelog.js +64 -0
- package/config/no-changelog.js +40 -0
- package/config/republish.js +60 -0
- package/config/retry-publish.js +40 -0
- package/dist/scripts/check-config.js +285 -0
- package/dist/scripts/check-pr-status.js +164 -0
- package/dist/scripts/extract-changelog.js +66 -0
- package/dist/scripts/init-project.js +191 -0
- package/dist/scripts/lib/commit-parser.js +67 -0
- package/dist/scripts/lib/git-utils.js +33 -0
- package/dist/scripts/lib/semver-utils.js +26 -0
- package/dist/scripts/lib/string-utils.js +12 -0
- package/dist/scripts/populate-unreleased-changelog.js +236 -0
- package/dist/scripts/republish-changelog.js +187 -0
- package/dist/scripts/retry-publish.js +98 -0
- package/dist/scripts/validate-release.js +288 -0
- package/dist/types/check-config.d.ts +65 -0
- package/dist/types/check-pr-status.d.ts +51 -0
- package/dist/types/extract-changelog.d.ts +23 -0
- package/dist/types/init-project.d.ts +37 -0
- package/dist/types/lib/commit-parser.d.ts +44 -0
- package/dist/types/lib/git-utils.d.ts +20 -0
- package/dist/types/lib/semver-utils.d.ts +18 -0
- package/dist/types/lib/string-utils.d.ts +10 -0
- package/dist/types/populate-unreleased-changelog.d.ts +56 -0
- package/dist/types/republish-changelog.d.ts +39 -0
- package/dist/types/retry-publish.d.ts +33 -0
- package/dist/types/validate-release.d.ts +43 -0
- package/package.json +93 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Check and display release configuration and project status
|
|
4
|
+
*
|
|
5
|
+
* This script displays:
|
|
6
|
+
* - Environment variables and their values
|
|
7
|
+
* - Repository information
|
|
8
|
+
* - Git tags and latest version
|
|
9
|
+
* - Commits since last tag
|
|
10
|
+
* - Configuration files status
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* tsx check-config.ts
|
|
14
|
+
*/
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { getGitHubRepoUrl } from './lib/git-utils.js';
|
|
18
|
+
export function safeExec(command, deps) {
|
|
19
|
+
try {
|
|
20
|
+
return deps.execSync(command, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function getEnvVar(name, deps, defaultValue) {
|
|
27
|
+
const value = deps.getEnv(name);
|
|
28
|
+
if (value) {
|
|
29
|
+
return `${value} (from env)`;
|
|
30
|
+
}
|
|
31
|
+
if (defaultValue) {
|
|
32
|
+
return `${defaultValue} (default)`;
|
|
33
|
+
}
|
|
34
|
+
return '(not set)';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get current git branch
|
|
38
|
+
*/
|
|
39
|
+
export function getCurrentBranch(deps) {
|
|
40
|
+
return safeExec('git rev-parse --abbrev-ref HEAD', deps);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get latest git tag
|
|
44
|
+
*/
|
|
45
|
+
export function getLatestTag(deps) {
|
|
46
|
+
return safeExec('git describe --tags --abbrev=0', deps);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get commit count since tag (or all commits if no tag)
|
|
50
|
+
*/
|
|
51
|
+
export function getCommitCount(deps, latestTag) {
|
|
52
|
+
const commits = latestTag
|
|
53
|
+
? safeExec(`git log --pretty=format:"%h" ${latestTag}..HEAD`, deps)
|
|
54
|
+
: safeExec('git log --pretty=format:"%h"', deps);
|
|
55
|
+
return commits ? commits.split('\n').length : 0;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get files status (existence check)
|
|
59
|
+
*/
|
|
60
|
+
export function getFilesStatus(deps) {
|
|
61
|
+
return {
|
|
62
|
+
'CHANGELOG.md': deps.existsSync('CHANGELOG.md'),
|
|
63
|
+
'.release-it.json': deps.existsSync('.release-it.json'),
|
|
64
|
+
'package.json': deps.existsSync('package.json'),
|
|
65
|
+
'.git': deps.existsSync('.git'),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get npm username (if logged in)
|
|
70
|
+
*/
|
|
71
|
+
export function getNpmUsername(deps) {
|
|
72
|
+
return safeExec('npm whoami', deps);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get all environment variables with defaults
|
|
76
|
+
*/
|
|
77
|
+
export function getEnvironmentVariables(deps) {
|
|
78
|
+
const vars = [
|
|
79
|
+
['CHANGELOG_FILE', 'CHANGELOG.md'],
|
|
80
|
+
['GIT_COMMIT_MESSAGE', 'release: bump v${version}'],
|
|
81
|
+
['GIT_TAG_NAME', 'v${version}'],
|
|
82
|
+
['GIT_REQUIRE_BRANCH', 'main'],
|
|
83
|
+
['GIT_REQUIRE_UPSTREAM', 'false'],
|
|
84
|
+
['GIT_REQUIRE_CLEAN', 'false'],
|
|
85
|
+
['GIT_REMOTE', 'origin'],
|
|
86
|
+
['GITHUB_RELEASE', 'false'],
|
|
87
|
+
['GITHUB_REPOSITORY'],
|
|
88
|
+
['NPM_PUBLISH', 'false'],
|
|
89
|
+
['NPM_SKIP_CHECKS', 'false'],
|
|
90
|
+
['NPM_ACCESS', 'public'],
|
|
91
|
+
];
|
|
92
|
+
const envVars = {};
|
|
93
|
+
for (const [name, defaultValue] of vars) {
|
|
94
|
+
envVars[name] = getEnvVar(name, deps, defaultValue);
|
|
95
|
+
}
|
|
96
|
+
return envVars;
|
|
97
|
+
}
|
|
98
|
+
export function checkConfig(deps) {
|
|
99
|
+
const envVars = getEnvironmentVariables(deps);
|
|
100
|
+
const repoUrl = getGitHubRepoUrl({
|
|
101
|
+
execSync: deps.execSync,
|
|
102
|
+
getEnv: deps.getEnv,
|
|
103
|
+
}) || null;
|
|
104
|
+
const currentBranch = getCurrentBranch(deps);
|
|
105
|
+
const latestTag = getLatestTag(deps);
|
|
106
|
+
const commitCount = getCommitCount(deps, latestTag);
|
|
107
|
+
const filesStatus = getFilesStatus(deps);
|
|
108
|
+
const npmUsername = getNpmUsername(deps);
|
|
109
|
+
return {
|
|
110
|
+
envVars,
|
|
111
|
+
repoUrl,
|
|
112
|
+
currentBranch,
|
|
113
|
+
latestTag,
|
|
114
|
+
commitCount,
|
|
115
|
+
filesStatus,
|
|
116
|
+
npmUsername,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function section(title) {
|
|
120
|
+
console.log(`\n${'='.repeat(60)}`);
|
|
121
|
+
console.log(` ${title}`);
|
|
122
|
+
console.log(`${'='.repeat(60)}\n`);
|
|
123
|
+
}
|
|
124
|
+
export function displayEnvironmentVariables(envVars) {
|
|
125
|
+
section('Environment Variables');
|
|
126
|
+
for (const [name, value] of Object.entries(envVars)) {
|
|
127
|
+
console.log(` ${name.padEnd(25)} ${value}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export function displayRepositoryInfo(deps, repoUrl, currentBranch) {
|
|
131
|
+
section('Repository Information');
|
|
132
|
+
if (repoUrl) {
|
|
133
|
+
console.log(` Repository URL: ${repoUrl}`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.log(` Repository URL: ❌ Could not determine`);
|
|
137
|
+
}
|
|
138
|
+
if (currentBranch) {
|
|
139
|
+
console.log(` Current branch: ${currentBranch}`);
|
|
140
|
+
}
|
|
141
|
+
const remote = deps.getEnv('GIT_REMOTE') || 'origin';
|
|
142
|
+
const remoteUrl = safeExec(`git config --get remote.${remote}.url`, deps);
|
|
143
|
+
if (remoteUrl) {
|
|
144
|
+
console.log(` Remote (${remote}): ${remoteUrl}`);
|
|
145
|
+
}
|
|
146
|
+
const upstream = safeExec(`git rev-parse --abbrev-ref --symbolic-full-name @{u}`, deps);
|
|
147
|
+
if (upstream) {
|
|
148
|
+
console.log(` Upstream: ${upstream}`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
console.log(` Upstream: ❌ No upstream configured`);
|
|
152
|
+
}
|
|
153
|
+
const status = safeExec('git status --porcelain', deps);
|
|
154
|
+
if (status === '') {
|
|
155
|
+
console.log(` Working directory: ✅ Clean`);
|
|
156
|
+
}
|
|
157
|
+
else if (status) {
|
|
158
|
+
const fileCount = status.split('\n').length;
|
|
159
|
+
console.log(` Working directory: ⚠️ ${fileCount} uncommitted change(s)`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export function displayVersionAndTags(deps, latestTag) {
|
|
163
|
+
section('Version & Tags');
|
|
164
|
+
const packageJsonPath = 'package.json';
|
|
165
|
+
if (deps.existsSync(packageJsonPath)) {
|
|
166
|
+
try {
|
|
167
|
+
const packageJson = JSON.parse(deps.readFileSync(packageJsonPath, 'utf8'));
|
|
168
|
+
console.log(` Current version (package.json): ${packageJson.version || '❌ Not set'}`);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
console.log(` Current version: ❌ Failed to read package.json`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.log(` Current version: ❌ package.json not found`);
|
|
176
|
+
}
|
|
177
|
+
if (latestTag) {
|
|
178
|
+
console.log(` Latest git tag: ${latestTag}`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.log(` Latest git tag: ❌ No tags found`);
|
|
182
|
+
}
|
|
183
|
+
const allTags = safeExec('git tag --sort=-v:refname', deps);
|
|
184
|
+
if (allTags) {
|
|
185
|
+
const tags = allTags.split('\n').slice(0, 5);
|
|
186
|
+
console.log(` Recent tags (last 5):`);
|
|
187
|
+
for (const tag of tags) {
|
|
188
|
+
console.log(` - ${tag}`);
|
|
189
|
+
}
|
|
190
|
+
const totalTags = allTags.split('\n').length;
|
|
191
|
+
if (totalTags > 5) {
|
|
192
|
+
console.log(` ... and ${totalTags - 5} more`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export function displayCommitsSinceLastTag(deps, latestTag) {
|
|
197
|
+
section('Commits Since Last Tag');
|
|
198
|
+
let commitRange;
|
|
199
|
+
if (latestTag) {
|
|
200
|
+
commitRange = `${latestTag}..HEAD`;
|
|
201
|
+
console.log(` Range: ${commitRange}\n`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
commitRange = 'HEAD';
|
|
205
|
+
console.log(` No tags found, showing all commits\n`);
|
|
206
|
+
}
|
|
207
|
+
const commits = safeExec(`git log --pretty=format:"%h %s" ${commitRange}`, deps);
|
|
208
|
+
if (!commits) {
|
|
209
|
+
console.log(` ✨ No commits since ${latestTag || 'repository creation'}`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const commitList = commits.split('\n');
|
|
213
|
+
console.log(` Total commits: ${commitList.length}\n`);
|
|
214
|
+
const displayCount = Math.min(10, commitList.length);
|
|
215
|
+
console.log(` Last ${displayCount} commit(s):`);
|
|
216
|
+
for (const commit of commitList.slice(0, displayCount)) {
|
|
217
|
+
console.log(` ${commit}`);
|
|
218
|
+
}
|
|
219
|
+
if (commitList.length > displayCount) {
|
|
220
|
+
console.log(` ... and ${commitList.length - displayCount} more`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
export function displayConfigurationFiles(deps, filesStatus) {
|
|
224
|
+
section('Configuration Files');
|
|
225
|
+
const files = [
|
|
226
|
+
{ path: 'CHANGELOG.md', desc: 'Changelog' },
|
|
227
|
+
{ path: '.release-it.json', desc: 'Release-it config' },
|
|
228
|
+
{ path: 'package.json', desc: 'Package manifest' },
|
|
229
|
+
{ path: '.git', desc: 'Git repository' },
|
|
230
|
+
];
|
|
231
|
+
for (const { path, desc } of files) {
|
|
232
|
+
const exists = filesStatus[path];
|
|
233
|
+
const icon = exists ? '✅' : '❌';
|
|
234
|
+
console.log(` ${icon} ${desc.padEnd(20)} ${path}`);
|
|
235
|
+
}
|
|
236
|
+
const changelogPath = deps.getEnv('CHANGELOG_FILE') || 'CHANGELOG.md';
|
|
237
|
+
if (changelogPath !== 'CHANGELOG.md' && deps.existsSync(changelogPath)) {
|
|
238
|
+
console.log(` ✅ ${'Custom changelog'.padEnd(20)} ${changelogPath} (from env)`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
export function displayNpmStatus(deps, npmUsername) {
|
|
242
|
+
section('npm Status');
|
|
243
|
+
if (npmUsername) {
|
|
244
|
+
console.log(` ✅ Logged in as: ${npmUsername}`);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
console.log(` ❌ Not logged in (run: npm login)`);
|
|
248
|
+
}
|
|
249
|
+
const registry = safeExec('npm config get registry', deps);
|
|
250
|
+
if (registry) {
|
|
251
|
+
console.log(` Registry: ${registry}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* CLI entry point - only runs when script is executed directly
|
|
256
|
+
*/
|
|
257
|
+
/* c8 ignore start */
|
|
258
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
259
|
+
async function main() {
|
|
260
|
+
console.log('🔍 Checking release configuration and project status...');
|
|
261
|
+
const deps = {
|
|
262
|
+
execSync,
|
|
263
|
+
existsSync,
|
|
264
|
+
readFileSync,
|
|
265
|
+
getEnv: (key) => process.env[key],
|
|
266
|
+
log: console.log,
|
|
267
|
+
};
|
|
268
|
+
const result = checkConfig(deps);
|
|
269
|
+
displayEnvironmentVariables(result.envVars);
|
|
270
|
+
displayRepositoryInfo(deps, result.repoUrl, result.currentBranch);
|
|
271
|
+
displayVersionAndTags(deps, result.latestTag);
|
|
272
|
+
displayCommitsSinceLastTag(deps, result.latestTag);
|
|
273
|
+
displayConfigurationFiles(deps, result.filesStatus);
|
|
274
|
+
displayNpmStatus(deps, result.npmUsername);
|
|
275
|
+
console.log('\n' + '='.repeat(60));
|
|
276
|
+
console.log('\n✨ Check complete!');
|
|
277
|
+
console.log('\nTo validate release readiness, run:');
|
|
278
|
+
console.log(' pnpm release-it-preset validate\n');
|
|
279
|
+
}
|
|
280
|
+
main().catch((error) => {
|
|
281
|
+
console.error('❌ Check failed:', error);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/* c8 ignore end */
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Check pull request hygiene (changelog + commits)
|
|
4
|
+
*
|
|
5
|
+
* This script inspects the current git workspace to determine whether:
|
|
6
|
+
* - The changelog (respecting CHANGELOG_FILE env override) has been modified
|
|
7
|
+
* - A `[skip-changelog]` marker appears in commit messages
|
|
8
|
+
* - Commits follow the conventional commit format
|
|
9
|
+
*
|
|
10
|
+
* Results are written to `$GITHUB_OUTPUT` when available so GitHub Actions steps
|
|
11
|
+
* can consume them without relying on continue-on-error semantics.
|
|
12
|
+
*/
|
|
13
|
+
import { execSync } from 'node:child_process';
|
|
14
|
+
import { appendFileSync } from 'node:fs';
|
|
15
|
+
import { STRICT_CONVENTIONAL_COMMIT_REGEX } from './lib/commit-parser.js';
|
|
16
|
+
const SKIP_CHANGELOG_REGEX = /\[skip-changelog]/i;
|
|
17
|
+
export function safeExec(command, deps) {
|
|
18
|
+
try {
|
|
19
|
+
return deps.execSync(command, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
deps.warn(`⚠️ Command failed: ${command}\n${error instanceof Error ? error.message : String(error)}`);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function normalizeBaseRef(baseRef) {
|
|
27
|
+
if (!baseRef || baseRef.trim() === '') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const trimmed = baseRef.trim();
|
|
31
|
+
if (trimmed.includes('/') || trimmed.startsWith('refs/')) {
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
return `origin/${trimmed}`;
|
|
35
|
+
}
|
|
36
|
+
export function parseArgs(argv) {
|
|
37
|
+
let base = null;
|
|
38
|
+
let head = null;
|
|
39
|
+
for (let i = 0; i < argv.length; i++) {
|
|
40
|
+
const arg = argv[i];
|
|
41
|
+
if (arg === '--base' && argv[i + 1]) {
|
|
42
|
+
base = argv[++i];
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (arg === '--head' && argv[i + 1]) {
|
|
46
|
+
head = argv[++i];
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { base, head };
|
|
51
|
+
}
|
|
52
|
+
export function splitList(value) {
|
|
53
|
+
if (!value) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
return value
|
|
57
|
+
.split('\n')
|
|
58
|
+
.map(entry => entry.trim())
|
|
59
|
+
.filter(entry => entry.length > 0);
|
|
60
|
+
}
|
|
61
|
+
export function hasSkipChangelog(commits) {
|
|
62
|
+
return commits.some(commit => SKIP_CHANGELOG_REGEX.test(commit));
|
|
63
|
+
}
|
|
64
|
+
export function hasConventionalCommits(commits) {
|
|
65
|
+
return commits.some(commit => STRICT_CONVENTIONAL_COMMIT_REGEX.test(commit));
|
|
66
|
+
}
|
|
67
|
+
export function evaluateChangelogStatus(changedFiles, changelogPath, commits) {
|
|
68
|
+
const changelogFile = changelogPath || 'CHANGELOG.md';
|
|
69
|
+
const normalizedChangelog = changelogFile.trim();
|
|
70
|
+
const skipMarker = hasSkipChangelog(commits);
|
|
71
|
+
if (changedFiles.includes(normalizedChangelog)) {
|
|
72
|
+
return { status: 'updated', skipMarker };
|
|
73
|
+
}
|
|
74
|
+
if (skipMarker) {
|
|
75
|
+
return { status: 'skipped', skipMarker };
|
|
76
|
+
}
|
|
77
|
+
return { status: 'missing', skipMarker };
|
|
78
|
+
}
|
|
79
|
+
export function getDiffRange(baseRef, headRef) {
|
|
80
|
+
if (!baseRef) {
|
|
81
|
+
return headRef;
|
|
82
|
+
}
|
|
83
|
+
return `${baseRef}..${headRef}`;
|
|
84
|
+
}
|
|
85
|
+
export function runPrCheck(args, deps) {
|
|
86
|
+
const envBase = deps.getEnv('PR_BASE_REF') ?? deps.getEnv('GITHUB_BASE_REF') ?? null;
|
|
87
|
+
const envHead = deps.getEnv('PR_HEAD_REF') ?? deps.getEnv('GITHUB_HEAD_REF') ?? null;
|
|
88
|
+
const baseRef = normalizeBaseRef(args.base ?? envBase);
|
|
89
|
+
const headRef = (args.head ?? envHead ?? 'HEAD').trim() || 'HEAD';
|
|
90
|
+
const diffRange = getDiffRange(baseRef, headRef);
|
|
91
|
+
const changedFilesOutput = safeExec(baseRef ? `git diff --name-only ${diffRange}` : `git diff --name-only ${headRef}`, deps);
|
|
92
|
+
const changedFiles = splitList(changedFilesOutput);
|
|
93
|
+
const commitsOutput = safeExec(baseRef ? `git log ${diffRange} --pretty=format:%s` : `git log ${headRef} --pretty=format:%s`, deps);
|
|
94
|
+
const commits = splitList(commitsOutput);
|
|
95
|
+
const changelogPath = deps.getEnv('CHANGELOG_FILE') ?? 'CHANGELOG.md';
|
|
96
|
+
const changelogEvaluation = evaluateChangelogStatus(changedFiles, changelogPath, commits);
|
|
97
|
+
const conventional = hasConventionalCommits(commits);
|
|
98
|
+
return {
|
|
99
|
+
baseRef,
|
|
100
|
+
headRef,
|
|
101
|
+
changedFiles,
|
|
102
|
+
commits,
|
|
103
|
+
changelogStatus: changelogEvaluation.status,
|
|
104
|
+
skipChangelogMarker: changelogEvaluation.skipMarker,
|
|
105
|
+
hasConventionalCommits: conventional,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function createDefaultDeps() {
|
|
109
|
+
return {
|
|
110
|
+
execSync,
|
|
111
|
+
getEnv: (key) => process.env[key],
|
|
112
|
+
writeOutput: (name, value) => {
|
|
113
|
+
const outputFile = process.env.GITHUB_OUTPUT;
|
|
114
|
+
if (!outputFile) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
appendFileSync(outputFile, `${name}=${value}\n`, { encoding: 'utf8' });
|
|
118
|
+
},
|
|
119
|
+
log: console.log,
|
|
120
|
+
warn: console.warn,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function writeOutputs(result, deps) {
|
|
124
|
+
const commitsEncoded = Buffer.from(JSON.stringify(result.commits), 'utf8').toString('base64');
|
|
125
|
+
const filesEncoded = Buffer.from(JSON.stringify(result.changedFiles), 'utf8').toString('base64');
|
|
126
|
+
deps.writeOutput('changelog_status', result.changelogStatus);
|
|
127
|
+
deps.writeOutput('skip_changelog', result.skipChangelogMarker ? 'true' : 'false');
|
|
128
|
+
deps.writeOutput('conventional_commits', result.hasConventionalCommits ? 'true' : 'false');
|
|
129
|
+
deps.writeOutput('commit_messages', commitsEncoded);
|
|
130
|
+
deps.writeOutput('changed_files', filesEncoded);
|
|
131
|
+
deps.writeOutput('base_ref', result.baseRef ?? '');
|
|
132
|
+
deps.writeOutput('head_ref', result.headRef);
|
|
133
|
+
}
|
|
134
|
+
export function renderSummary(result, deps) {
|
|
135
|
+
deps.log('🔍 PR hygiene check');
|
|
136
|
+
deps.log(` • Base ref: ${result.baseRef ?? '(not provided)'}`);
|
|
137
|
+
deps.log(` • Head ref: ${result.headRef}`);
|
|
138
|
+
deps.log(` • Commits inspected: ${result.commits.length}`);
|
|
139
|
+
switch (result.changelogStatus) {
|
|
140
|
+
case 'updated':
|
|
141
|
+
deps.log('✅ CHANGELOG: updated in this PR');
|
|
142
|
+
break;
|
|
143
|
+
case 'skipped':
|
|
144
|
+
deps.log('ℹ️ CHANGELOG: skipped via [skip-changelog] marker');
|
|
145
|
+
break;
|
|
146
|
+
default:
|
|
147
|
+
deps.log('⚠️ CHANGELOG: no updates and no [skip-changelog] marker');
|
|
148
|
+
}
|
|
149
|
+
if (result.hasConventionalCommits) {
|
|
150
|
+
deps.log('✅ Conventional commits detected');
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
deps.log('ℹ️ No conventional commits found');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/* c8 ignore start */
|
|
157
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
158
|
+
const deps = createDefaultDeps();
|
|
159
|
+
const args = parseArgs(process.argv.slice(2));
|
|
160
|
+
const result = runPrCheck(args, deps);
|
|
161
|
+
writeOutputs(result, deps);
|
|
162
|
+
renderSummary(result, deps);
|
|
163
|
+
}
|
|
164
|
+
/* c8 ignore end */
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Extract changelog entry for a specific version
|
|
4
|
+
*
|
|
5
|
+
* This script reads CHANGELOG.md and extracts the section for a given version.
|
|
6
|
+
* Used primarily for generating GitHub release notes.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* tsx extract-changelog.ts <version>
|
|
10
|
+
*
|
|
11
|
+
* Example:
|
|
12
|
+
* tsx extract-changelog.ts 1.2.3
|
|
13
|
+
*
|
|
14
|
+
* Environment variables:
|
|
15
|
+
* CHANGELOG_FILE - Path to changelog file (default: CHANGELOG.md)
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { validateAndNormalizeSemver } from './lib/semver-utils.js';
|
|
20
|
+
import { escapeRegExp } from './lib/string-utils.js';
|
|
21
|
+
export function extractChangelog(version, deps) {
|
|
22
|
+
// Validate semver format
|
|
23
|
+
const normalizedVersion = validateAndNormalizeSemver(version);
|
|
24
|
+
const versionLabels = [`v${normalizedVersion}`, normalizedVersion];
|
|
25
|
+
const tag = version.startsWith('v') ? version : `v${normalizedVersion}`;
|
|
26
|
+
const changelogFile = deps.getEnv('CHANGELOG_FILE') || 'CHANGELOG.md';
|
|
27
|
+
const changelogPath = join(deps.getCwd(), changelogFile);
|
|
28
|
+
const changelog = deps.readFileSync(changelogPath, 'utf8');
|
|
29
|
+
const labelPattern = versionLabels.map(escapeRegExp).join('|');
|
|
30
|
+
const versionBlock = new RegExp(`^(?<prefix>[^\n]*?##\\s*\\[?(?:${labelPattern})\\]?[^\n]*\r?\n)(?<content>[\\s\\S]*?)(?=^##\\s|^\\s*---\\s*$|$(?![\\s\\S]))`, 'm');
|
|
31
|
+
const match = changelog.match(versionBlock);
|
|
32
|
+
if (!match || !match.groups) {
|
|
33
|
+
const humanLabels = versionLabels.map((label) => `[${label}]`).join(' or ');
|
|
34
|
+
throw new Error(`No ${humanLabels} section found in ${changelogFile}`);
|
|
35
|
+
}
|
|
36
|
+
const versionContent = match.groups.content.trim();
|
|
37
|
+
if (!versionContent) {
|
|
38
|
+
throw new Error(`No changelog entry found for ${tag}`);
|
|
39
|
+
}
|
|
40
|
+
const entry = match[0].trim();
|
|
41
|
+
return `# Release ${tag}\n\n${entry}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* CLI entry point - only runs when script is executed directly
|
|
45
|
+
*/
|
|
46
|
+
/* c8 ignore start */
|
|
47
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
48
|
+
const version = process.argv[2];
|
|
49
|
+
if (!version) {
|
|
50
|
+
console.error('Usage: tsx scripts/extract-changelog.ts <version>');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const result = extractChangelog(version, {
|
|
55
|
+
readFileSync,
|
|
56
|
+
getEnv: (key) => process.env[key],
|
|
57
|
+
getCwd: () => process.cwd(),
|
|
58
|
+
});
|
|
59
|
+
console.log(result);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error(`❌ ${error instanceof Error ? error.message : error}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/* c8 ignore end */
|