glab-setup-git-identity 0.6.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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/release.yml +372 -0
- package/.husky/pre-commit +1 -0
- package/.jscpd.json +20 -0
- package/.prettierignore +7 -0
- package/.prettierrc +10 -0
- package/CHANGELOG.md +143 -0
- package/LICENSE +24 -0
- package/README.md +455 -0
- package/bunfig.toml +3 -0
- package/deno.json +7 -0
- package/docs/case-studies/issue-13/README.md +195 -0
- package/docs/case-studies/issue-13/hive-mind-issue-960.json +23 -0
- package/docs/case-studies/issue-13/hive-mind-pr-961-diff.txt +773 -0
- package/docs/case-studies/issue-13/hive-mind-pr-961.json +126 -0
- package/docs/case-studies/issue-21/README.md +384 -0
- package/docs/case-studies/issue-21/ci-logs/run-20803315337.txt +1188 -0
- package/docs/case-studies/issue-21/ci-logs/run-20885464993.txt +1310 -0
- package/docs/case-studies/issue-21/issue-111-data.txt +15 -0
- package/docs/case-studies/issue-21/issue-113-data.txt +15 -0
- package/docs/case-studies/issue-21/pr-112-data.json +109 -0
- package/docs/case-studies/issue-21/pr-112-diff.patch +1336 -0
- package/docs/case-studies/issue-21/pr-114-data.json +126 -0
- package/docs/case-studies/issue-21/pr-114-diff.patch +879 -0
- package/docs/case-studies/issue-3/README.md +338 -0
- package/docs/case-studies/issue-3/created-issues.md +32 -0
- package/docs/case-studies/issue-3/issue-data.json +29 -0
- package/docs/case-studies/issue-3/original-format-release-notes.mjs +212 -0
- package/docs/case-studies/issue-3/reference-pr-59-diff.txt +614 -0
- package/docs/case-studies/issue-3/reference-pr-59.json +109 -0
- package/docs/case-studies/issue-3/release-v0.1.0.json +9 -0
- package/docs/case-studies/issue-3/repositories-with-same-script.json +22 -0
- package/docs/case-studies/issue-3/research-notes.md +33 -0
- package/docs/case-studies/issue-7/BEST-PRACTICES-COMPARISON.md +334 -0
- package/docs/case-studies/issue-7/FORMATTER-COMPARISON.md +649 -0
- package/docs/case-studies/issue-7/current-repository-analysis.json +70 -0
- package/docs/case-studies/issue-7/effect-template-analysis.json +178 -0
- package/eslint.config.js +91 -0
- package/examples/basic-usage.js +64 -0
- package/experiments/test-changeset-scripts.mjs +303 -0
- package/experiments/test-failure-detection.mjs +143 -0
- package/experiments/test-format-major-changes.mjs +49 -0
- package/experiments/test-format-minor-changes.mjs +52 -0
- package/experiments/test-format-no-hash.mjs +43 -0
- package/experiments/test-format-patch-changes.mjs +46 -0
- package/package.json +80 -0
- package/scripts/changeset-version.mjs +75 -0
- package/scripts/check-changesets.mjs +67 -0
- package/scripts/check-version.mjs +129 -0
- package/scripts/create-github-release.mjs +93 -0
- package/scripts/create-manual-changeset.mjs +89 -0
- package/scripts/detect-code-changes.mjs +194 -0
- package/scripts/format-github-release.mjs +83 -0
- package/scripts/format-release-notes.mjs +219 -0
- package/scripts/instant-version-bump.mjs +172 -0
- package/scripts/js-paths.mjs +177 -0
- package/scripts/merge-changesets.mjs +263 -0
- package/scripts/publish-to-npm.mjs +302 -0
- package/scripts/setup-npm.mjs +37 -0
- package/scripts/validate-changeset.mjs +265 -0
- package/scripts/version-and-commit.mjs +284 -0
- package/src/cli.js +386 -0
- package/src/index.d.ts +255 -0
- package/src/index.js +563 -0
- package/tests/index.test.js +137 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create GitHub Release from CHANGELOG.md
|
|
5
|
+
* Usage: node scripts/create-github-release.mjs --release-version <version> --repository <repository>
|
|
6
|
+
* release-version: Version number (e.g., 1.0.0)
|
|
7
|
+
* repository: GitHub repository (e.g., owner/repo)
|
|
8
|
+
*
|
|
9
|
+
* Uses link-foundation libraries:
|
|
10
|
+
* - use-m: Dynamic package loading without package.json dependencies
|
|
11
|
+
* - command-stream: Modern shell command execution with streaming support
|
|
12
|
+
* - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from 'fs';
|
|
16
|
+
|
|
17
|
+
// Load use-m dynamically
|
|
18
|
+
const { use } = eval(
|
|
19
|
+
await (await fetch('https://unpkg.com/use-m/use.js')).text()
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Import link-foundation libraries
|
|
23
|
+
const { $ } = await use('command-stream');
|
|
24
|
+
const { makeConfig } = await use('lino-arguments');
|
|
25
|
+
|
|
26
|
+
// Parse CLI arguments using lino-arguments
|
|
27
|
+
// Note: Using --release-version instead of --version to avoid conflict with yargs' built-in --version flag
|
|
28
|
+
const config = makeConfig({
|
|
29
|
+
yargs: ({ yargs, getenv }) =>
|
|
30
|
+
yargs
|
|
31
|
+
.option('release-version', {
|
|
32
|
+
type: 'string',
|
|
33
|
+
default: getenv('VERSION', ''),
|
|
34
|
+
describe: 'Version number (e.g., 1.0.0)',
|
|
35
|
+
})
|
|
36
|
+
.option('repository', {
|
|
37
|
+
type: 'string',
|
|
38
|
+
default: getenv('REPOSITORY', ''),
|
|
39
|
+
describe: 'GitHub repository (e.g., owner/repo)',
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const { releaseVersion: version, repository } = config;
|
|
44
|
+
|
|
45
|
+
if (!version || !repository) {
|
|
46
|
+
console.error('Error: Missing required arguments');
|
|
47
|
+
console.error(
|
|
48
|
+
'Usage: node scripts/create-github-release.mjs --release-version <version> --repository <repository>'
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const tag = `v${version}`;
|
|
54
|
+
|
|
55
|
+
console.log(`Creating GitHub release for ${tag}...`);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Read CHANGELOG.md
|
|
59
|
+
const changelog = readFileSync('./CHANGELOG.md', 'utf8');
|
|
60
|
+
|
|
61
|
+
// Extract changelog entry for this version
|
|
62
|
+
// Read from CHANGELOG.md between this version header and the next version header
|
|
63
|
+
const versionHeaderRegex = new RegExp(`## ${version}[\\s\\S]*?(?=## \\d|$)`);
|
|
64
|
+
const match = changelog.match(versionHeaderRegex);
|
|
65
|
+
|
|
66
|
+
let releaseNotes = '';
|
|
67
|
+
if (match) {
|
|
68
|
+
// Remove the version header itself and trim
|
|
69
|
+
releaseNotes = match[0].replace(`## ${version}`, '').trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!releaseNotes) {
|
|
73
|
+
releaseNotes = `Release ${version}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Create release using GitHub API with JSON input
|
|
77
|
+
// This avoids shell escaping issues that occur when passing text via command-line arguments
|
|
78
|
+
// (Previously caused apostrophes like "didn't" to appear as "didn'''" in releases)
|
|
79
|
+
const payload = JSON.stringify({
|
|
80
|
+
tag_name: tag,
|
|
81
|
+
name: version,
|
|
82
|
+
body: releaseNotes,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await $`gh api repos/${repository}/releases -X POST --input -`.run({
|
|
86
|
+
stdin: payload,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
console.log(`\u2705 Created GitHub release: ${tag}`);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('Error creating release:', error.message);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create a changeset file for manual releases
|
|
5
|
+
* Usage: node scripts/create-manual-changeset.mjs --bump-type <major|minor|patch> [--description <description>]
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: Update the PACKAGE_NAME constant below to match your package.json
|
|
8
|
+
*
|
|
9
|
+
* Uses link-foundation libraries:
|
|
10
|
+
* - use-m: Dynamic package loading without package.json dependencies
|
|
11
|
+
* - command-stream: Modern shell command execution with streaming support
|
|
12
|
+
* - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { writeFileSync } from 'fs';
|
|
16
|
+
import { randomBytes } from 'crypto';
|
|
17
|
+
|
|
18
|
+
// TODO: Update this to match your package name in package.json
|
|
19
|
+
const PACKAGE_NAME = 'glab-setup-git-identity';
|
|
20
|
+
|
|
21
|
+
// Load use-m dynamically
|
|
22
|
+
const { use } = eval(
|
|
23
|
+
await (await fetch('https://unpkg.com/use-m/use.js')).text()
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Import link-foundation libraries
|
|
27
|
+
const { $ } = await use('command-stream');
|
|
28
|
+
const { makeConfig } = await use('lino-arguments');
|
|
29
|
+
|
|
30
|
+
// Parse CLI arguments using lino-arguments
|
|
31
|
+
const config = makeConfig({
|
|
32
|
+
yargs: ({ yargs, getenv }) =>
|
|
33
|
+
yargs
|
|
34
|
+
.option('bump-type', {
|
|
35
|
+
type: 'string',
|
|
36
|
+
default: getenv('BUMP_TYPE', ''),
|
|
37
|
+
describe: 'Version bump type: major, minor, or patch',
|
|
38
|
+
choices: ['major', 'minor', 'patch'],
|
|
39
|
+
})
|
|
40
|
+
.option('description', {
|
|
41
|
+
type: 'string',
|
|
42
|
+
default: getenv('DESCRIPTION', ''),
|
|
43
|
+
describe: 'Description for the changeset',
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const { bumpType, description: descriptionArg } = config;
|
|
49
|
+
|
|
50
|
+
// Use provided description or default based on bump type
|
|
51
|
+
const description = descriptionArg || `Manual ${bumpType} release`;
|
|
52
|
+
|
|
53
|
+
if (!bumpType || !['major', 'minor', 'patch'].includes(bumpType)) {
|
|
54
|
+
console.error(
|
|
55
|
+
'Usage: node scripts/create-manual-changeset.mjs --bump-type <major|minor|patch> [--description <description>]'
|
|
56
|
+
);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Generate a random changeset ID
|
|
61
|
+
const changesetId = randomBytes(4).toString('hex');
|
|
62
|
+
const changesetFile = `.changeset/manual-release-${changesetId}.md`;
|
|
63
|
+
|
|
64
|
+
// Create the changeset file with single quotes to match Prettier config
|
|
65
|
+
const content = `---
|
|
66
|
+
'${PACKAGE_NAME}': ${bumpType}
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
${description}
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
writeFileSync(changesetFile, content, 'utf-8');
|
|
73
|
+
|
|
74
|
+
console.log(`Created changeset: ${changesetFile}`);
|
|
75
|
+
console.log('Content:');
|
|
76
|
+
console.log(content);
|
|
77
|
+
|
|
78
|
+
// Format with Prettier
|
|
79
|
+
console.log('\nFormatting with Prettier...');
|
|
80
|
+
await $`npx prettier --write "${changesetFile}"`;
|
|
81
|
+
|
|
82
|
+
console.log('\n✅ Changeset created and formatted successfully');
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Error creating changeset:', error.message);
|
|
85
|
+
if (process.env.DEBUG) {
|
|
86
|
+
console.error('Stack trace:', error.stack);
|
|
87
|
+
}
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect code changes for CI/CD pipeline
|
|
5
|
+
*
|
|
6
|
+
* This script detects what types of files have changed between two commits
|
|
7
|
+
* and outputs the results for use in GitHub Actions workflow conditions.
|
|
8
|
+
*
|
|
9
|
+
* Key behavior:
|
|
10
|
+
* - For PRs: compares PR head against base branch
|
|
11
|
+
* - For pushes: compares HEAD against HEAD^
|
|
12
|
+
* - Excludes certain folders and file types from "code changes" detection
|
|
13
|
+
*
|
|
14
|
+
* Excluded from code changes (don't require changesets):
|
|
15
|
+
* - Markdown files (*.md) in any folder
|
|
16
|
+
* - .changeset/ folder (changeset metadata)
|
|
17
|
+
* - docs/ folder (documentation)
|
|
18
|
+
* - experiments/ folder (experimental scripts)
|
|
19
|
+
* - examples/ folder (example scripts)
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* node scripts/detect-code-changes.mjs
|
|
23
|
+
*
|
|
24
|
+
* Environment variables (set by GitHub Actions):
|
|
25
|
+
* - GITHUB_EVENT_NAME: 'pull_request' or 'push'
|
|
26
|
+
* - GITHUB_BASE_SHA: Base commit SHA for PR
|
|
27
|
+
* - GITHUB_HEAD_SHA: Head commit SHA for PR
|
|
28
|
+
*
|
|
29
|
+
* Outputs (written to GITHUB_OUTPUT):
|
|
30
|
+
* - mjs: 'true' if any .mjs files changed
|
|
31
|
+
* - js: 'true' if any .js files changed
|
|
32
|
+
* - package: 'true' if package.json changed
|
|
33
|
+
* - docs: 'true' if any .md files changed
|
|
34
|
+
* - workflow: 'true' if any .github/workflows/ files changed
|
|
35
|
+
* - any-code-changed: 'true' if any code files changed (excludes docs, changesets, experiments, examples)
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { execSync } from 'child_process';
|
|
39
|
+
import { appendFileSync } from 'fs';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Execute a shell command and return trimmed output
|
|
43
|
+
* @param {string} command - The command to execute
|
|
44
|
+
* @returns {string} - The trimmed command output
|
|
45
|
+
*/
|
|
46
|
+
function exec(command) {
|
|
47
|
+
try {
|
|
48
|
+
return execSync(command, { encoding: 'utf-8' }).trim();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(`Error executing command: ${command}`);
|
|
51
|
+
console.error(error.message);
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Write output to GitHub Actions output file
|
|
58
|
+
* @param {string} name - Output name
|
|
59
|
+
* @param {string} value - Output value
|
|
60
|
+
*/
|
|
61
|
+
function setOutput(name, value) {
|
|
62
|
+
const outputFile = process.env.GITHUB_OUTPUT;
|
|
63
|
+
if (outputFile) {
|
|
64
|
+
appendFileSync(outputFile, `${name}=${value}\n`);
|
|
65
|
+
}
|
|
66
|
+
console.log(`${name}=${value}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the list of changed files between two commits
|
|
71
|
+
* @returns {string[]} Array of changed file paths
|
|
72
|
+
*/
|
|
73
|
+
function getChangedFiles() {
|
|
74
|
+
const eventName = process.env.GITHUB_EVENT_NAME || 'local';
|
|
75
|
+
|
|
76
|
+
if (eventName === 'pull_request') {
|
|
77
|
+
const baseSha = process.env.GITHUB_BASE_SHA;
|
|
78
|
+
const headSha = process.env.GITHUB_HEAD_SHA;
|
|
79
|
+
|
|
80
|
+
if (baseSha && headSha) {
|
|
81
|
+
console.log(`Comparing PR: ${baseSha}...${headSha}`);
|
|
82
|
+
try {
|
|
83
|
+
// Ensure we have the base commit
|
|
84
|
+
try {
|
|
85
|
+
execSync(`git cat-file -e ${baseSha}`, { stdio: 'ignore' });
|
|
86
|
+
} catch {
|
|
87
|
+
console.log('Base commit not available locally, attempting fetch...');
|
|
88
|
+
execSync(`git fetch origin ${baseSha}`, { stdio: 'inherit' });
|
|
89
|
+
}
|
|
90
|
+
const output = exec(`git diff --name-only ${baseSha} ${headSha}`);
|
|
91
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(`Git diff failed: ${error.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// For push events or fallback
|
|
99
|
+
console.log('Comparing HEAD^ to HEAD');
|
|
100
|
+
try {
|
|
101
|
+
const output = exec('git diff --name-only HEAD^ HEAD');
|
|
102
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
103
|
+
} catch {
|
|
104
|
+
// If HEAD^ doesn't exist (first commit), list all files in HEAD
|
|
105
|
+
console.log('HEAD^ not available, listing all files in HEAD');
|
|
106
|
+
const output = exec('git ls-tree --name-only -r HEAD');
|
|
107
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if a file should be excluded from code changes detection
|
|
113
|
+
* @param {string} filePath - The file path to check
|
|
114
|
+
* @returns {boolean} True if the file should be excluded
|
|
115
|
+
*/
|
|
116
|
+
function isExcludedFromCodeChanges(filePath) {
|
|
117
|
+
// Exclude markdown files in any folder
|
|
118
|
+
if (filePath.endsWith('.md')) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Exclude specific folders from code changes
|
|
123
|
+
const excludedFolders = ['.changeset/', 'docs/', 'experiments/', 'examples/'];
|
|
124
|
+
|
|
125
|
+
for (const folder of excludedFolders) {
|
|
126
|
+
if (filePath.startsWith(folder)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Main function to detect changes
|
|
136
|
+
*/
|
|
137
|
+
function detectChanges() {
|
|
138
|
+
console.log('Detecting file changes for CI/CD...\n');
|
|
139
|
+
|
|
140
|
+
const changedFiles = getChangedFiles();
|
|
141
|
+
|
|
142
|
+
console.log('Changed files:');
|
|
143
|
+
if (changedFiles.length === 0) {
|
|
144
|
+
console.log(' (none)');
|
|
145
|
+
} else {
|
|
146
|
+
changedFiles.forEach((file) => console.log(` ${file}`));
|
|
147
|
+
}
|
|
148
|
+
console.log('');
|
|
149
|
+
|
|
150
|
+
// Detect .mjs file changes
|
|
151
|
+
const mjsChanged = changedFiles.some((file) => file.endsWith('.mjs'));
|
|
152
|
+
setOutput('mjs-changed', mjsChanged ? 'true' : 'false');
|
|
153
|
+
|
|
154
|
+
// Detect .js file changes
|
|
155
|
+
const jsChanged = changedFiles.some((file) => file.endsWith('.js'));
|
|
156
|
+
setOutput('js-changed', jsChanged ? 'true' : 'false');
|
|
157
|
+
|
|
158
|
+
// Detect package.json changes
|
|
159
|
+
const packageChanged = changedFiles.some((file) => file === 'package.json');
|
|
160
|
+
setOutput('package-changed', packageChanged ? 'true' : 'false');
|
|
161
|
+
|
|
162
|
+
// Detect documentation changes (any .md file)
|
|
163
|
+
const docsChanged = changedFiles.some((file) => file.endsWith('.md'));
|
|
164
|
+
setOutput('docs-changed', docsChanged ? 'true' : 'false');
|
|
165
|
+
|
|
166
|
+
// Detect workflow changes
|
|
167
|
+
const workflowChanged = changedFiles.some((file) =>
|
|
168
|
+
file.startsWith('.github/workflows/')
|
|
169
|
+
);
|
|
170
|
+
setOutput('workflow-changed', workflowChanged ? 'true' : 'false');
|
|
171
|
+
|
|
172
|
+
// Detect code changes (excluding docs, changesets, experiments, examples folders, and markdown files)
|
|
173
|
+
const codeChangedFiles = changedFiles.filter(
|
|
174
|
+
(file) => !isExcludedFromCodeChanges(file)
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
console.log('\nFiles considered as code changes:');
|
|
178
|
+
if (codeChangedFiles.length === 0) {
|
|
179
|
+
console.log(' (none)');
|
|
180
|
+
} else {
|
|
181
|
+
codeChangedFiles.forEach((file) => console.log(` ${file}`));
|
|
182
|
+
}
|
|
183
|
+
console.log('');
|
|
184
|
+
|
|
185
|
+
// Check if any code files changed (.mjs, .js, .json, .yml, .yaml, or workflow files)
|
|
186
|
+
const codePattern = /\.(mjs|js|json|yml|yaml)$|\.github\/workflows\//;
|
|
187
|
+
const codeChanged = codeChangedFiles.some((file) => codePattern.test(file));
|
|
188
|
+
setOutput('any-code-changed', codeChanged ? 'true' : 'false');
|
|
189
|
+
|
|
190
|
+
console.log('\nChange detection completed.');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Run the detection
|
|
194
|
+
detectChanges();
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Format GitHub release notes using the format-release-notes.mjs script
|
|
5
|
+
* Usage: node scripts/format-github-release.mjs --release-version <version> --repository <repository> --commit-sha <commit_sha>
|
|
6
|
+
* release-version: Version number (e.g., 1.0.0)
|
|
7
|
+
* repository: GitHub repository (e.g., owner/repo)
|
|
8
|
+
* commit_sha: Commit SHA for PR detection
|
|
9
|
+
*
|
|
10
|
+
* Uses link-foundation libraries:
|
|
11
|
+
* - use-m: Dynamic package loading without package.json dependencies
|
|
12
|
+
* - command-stream: Modern shell command execution with streaming support
|
|
13
|
+
* - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Load use-m dynamically
|
|
17
|
+
const { use } = eval(
|
|
18
|
+
await (await fetch('https://unpkg.com/use-m/use.js')).text()
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// Import link-foundation libraries
|
|
22
|
+
const { $ } = await use('command-stream');
|
|
23
|
+
const { makeConfig } = await use('lino-arguments');
|
|
24
|
+
|
|
25
|
+
// Parse CLI arguments using lino-arguments
|
|
26
|
+
// Note: Using --release-version instead of --version to avoid conflict with yargs' built-in --version flag
|
|
27
|
+
const config = makeConfig({
|
|
28
|
+
yargs: ({ yargs, getenv }) =>
|
|
29
|
+
yargs
|
|
30
|
+
.option('release-version', {
|
|
31
|
+
type: 'string',
|
|
32
|
+
default: getenv('VERSION', ''),
|
|
33
|
+
describe: 'Version number (e.g., 1.0.0)',
|
|
34
|
+
})
|
|
35
|
+
.option('repository', {
|
|
36
|
+
type: 'string',
|
|
37
|
+
default: getenv('REPOSITORY', ''),
|
|
38
|
+
describe: 'GitHub repository (e.g., owner/repo)',
|
|
39
|
+
})
|
|
40
|
+
.option('commit-sha', {
|
|
41
|
+
type: 'string',
|
|
42
|
+
default: getenv('COMMIT_SHA', ''),
|
|
43
|
+
describe: 'Commit SHA for PR detection',
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const { releaseVersion: version, repository, commitSha } = config;
|
|
48
|
+
|
|
49
|
+
if (!version || !repository || !commitSha) {
|
|
50
|
+
console.error('Error: Missing required arguments');
|
|
51
|
+
console.error(
|
|
52
|
+
'Usage: node scripts/format-github-release.mjs --release-version <version> --repository <repository> --commit-sha <commit_sha>'
|
|
53
|
+
);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const tag = `v${version}`;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Get the release ID for this version
|
|
61
|
+
let releaseId = '';
|
|
62
|
+
try {
|
|
63
|
+
const result =
|
|
64
|
+
await $`gh api "repos/${repository}/releases/tags/${tag}" --jq '.id'`.run(
|
|
65
|
+
{ capture: true }
|
|
66
|
+
);
|
|
67
|
+
releaseId = result.stdout.trim();
|
|
68
|
+
} catch {
|
|
69
|
+
console.log(`\u26A0\uFE0F Could not find release for ${tag}`);
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (releaseId) {
|
|
74
|
+
console.log(`Formatting release notes for ${tag}...`);
|
|
75
|
+
// Pass the trigger commit SHA for PR detection
|
|
76
|
+
// This allows proper PR lookup even if the changelog doesn't have a commit hash
|
|
77
|
+
await $`node scripts/format-release-notes.mjs --release-id "${releaseId}" --release-version "${tag}" --repository "${repository}" --commit-sha "${commitSha}"`;
|
|
78
|
+
console.log(`\u2705 Formatted release notes for ${tag}`);
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Error formatting release:', error.message);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Script to format GitHub release notes with proper formatting:
|
|
5
|
+
* - Fix special characters like \n
|
|
6
|
+
* - Add link to PR that contains the release commit (if found)
|
|
7
|
+
* - Add shields.io NPM version badge
|
|
8
|
+
* - Format nicely with proper markdown
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: Update the PACKAGE_NAME constant below to match your package.json
|
|
11
|
+
*
|
|
12
|
+
* PR Detection Logic:
|
|
13
|
+
* 1. Extract commit hash from changelog entry (if present)
|
|
14
|
+
* 2. Fall back to --commit-sha argument (passed from workflow)
|
|
15
|
+
* 3. Look up PRs that contain the commit via GitHub API
|
|
16
|
+
* 4. If no PR found, simply don't display any PR link (no guessing)
|
|
17
|
+
*
|
|
18
|
+
* Uses link-foundation libraries:
|
|
19
|
+
* - use-m: Dynamic package loading without package.json dependencies
|
|
20
|
+
* - command-stream: Modern shell command execution with streaming support
|
|
21
|
+
* - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files
|
|
22
|
+
*
|
|
23
|
+
* Note: Uses --release-version instead of --version to avoid conflict with yargs' built-in --version flag.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// TODO: Update this to match your package name in package.json
|
|
27
|
+
const PACKAGE_NAME = 'glab-setup-git-identity';
|
|
28
|
+
|
|
29
|
+
// Load use-m dynamically
|
|
30
|
+
const { use } = eval(
|
|
31
|
+
await (await fetch('https://unpkg.com/use-m/use.js')).text()
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Import link-foundation libraries
|
|
35
|
+
const { $ } = await use('command-stream');
|
|
36
|
+
const { makeConfig } = await use('lino-arguments');
|
|
37
|
+
|
|
38
|
+
// Parse CLI arguments using lino-arguments
|
|
39
|
+
// Note: Using --release-version instead of --version to avoid conflict with yargs' built-in --version flag
|
|
40
|
+
const config = makeConfig({
|
|
41
|
+
yargs: ({ yargs, getenv }) =>
|
|
42
|
+
yargs
|
|
43
|
+
.option('release-version', {
|
|
44
|
+
type: 'string',
|
|
45
|
+
default: getenv('VERSION', ''),
|
|
46
|
+
describe: 'Version number (e.g., v0.8.36)',
|
|
47
|
+
})
|
|
48
|
+
.option('release-id', {
|
|
49
|
+
type: 'string',
|
|
50
|
+
default: getenv('RELEASE_ID', ''),
|
|
51
|
+
describe: 'GitHub release ID',
|
|
52
|
+
})
|
|
53
|
+
.option('repository', {
|
|
54
|
+
type: 'string',
|
|
55
|
+
default: getenv('REPOSITORY', ''),
|
|
56
|
+
describe: 'GitHub repository (e.g., owner/repo)',
|
|
57
|
+
})
|
|
58
|
+
.option('commit-sha', {
|
|
59
|
+
type: 'string',
|
|
60
|
+
default: getenv('COMMIT_SHA', ''),
|
|
61
|
+
describe: 'Commit SHA for PR detection',
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const releaseId = config.releaseId;
|
|
66
|
+
const version = config.releaseVersion;
|
|
67
|
+
const repository = config.repository;
|
|
68
|
+
const passedCommitSha = config.commitSha;
|
|
69
|
+
|
|
70
|
+
if (!releaseId || !version || !repository) {
|
|
71
|
+
console.error(
|
|
72
|
+
'Usage: format-release-notes.mjs --release-id <releaseId> --release-version <version> --repository <repository> [--commit-sha <sha>]'
|
|
73
|
+
);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Get current release body
|
|
79
|
+
const result = await $`gh api repos/${repository}/releases/${releaseId}`.run({
|
|
80
|
+
capture: true,
|
|
81
|
+
});
|
|
82
|
+
const releaseData = JSON.parse(result.stdout);
|
|
83
|
+
|
|
84
|
+
const currentBody = releaseData.body || '';
|
|
85
|
+
|
|
86
|
+
// Skip if already formatted (has shields.io badge image)
|
|
87
|
+
if (currentBody.includes('img.shields.io')) {
|
|
88
|
+
console.log('ℹ️ Release notes already formatted');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Extract changes section (Major, Minor, or Patch)
|
|
93
|
+
// This regex handles multiple formats:
|
|
94
|
+
// 1. With commit hash: "### [Major|Minor|Patch] Changes\n- abc1234: Description"
|
|
95
|
+
// 2. Without commit hash: "### [Major|Minor|Patch] Changes\n- Description"
|
|
96
|
+
const changesPattern =
|
|
97
|
+
/### (Major|Minor|Patch) Changes\s*\n\s*-\s+(?:([a-f0-9]+):\s+)?(.+?)$/s;
|
|
98
|
+
const changesMatch = currentBody.match(changesPattern);
|
|
99
|
+
|
|
100
|
+
let commitHash = null;
|
|
101
|
+
let rawDescription = null;
|
|
102
|
+
let changeType = null;
|
|
103
|
+
|
|
104
|
+
if (changesMatch) {
|
|
105
|
+
// Extract: [full match, changeType, commitHash (optional), description]
|
|
106
|
+
[, changeType, commitHash, rawDescription] = changesMatch;
|
|
107
|
+
console.log(`ℹ️ Found ${changeType} Changes section`);
|
|
108
|
+
|
|
109
|
+
// If commitHash is undefined and description contains it, try to extract
|
|
110
|
+
if (!commitHash && rawDescription) {
|
|
111
|
+
// This handles the case where description itself might be null/undefined
|
|
112
|
+
// and we need to safely check for commit hash at the start
|
|
113
|
+
const descWithHashMatch = rawDescription.match(/^([a-f0-9]+):\s+(.+)$/s);
|
|
114
|
+
if (descWithHashMatch) {
|
|
115
|
+
[, commitHash, rawDescription] = descWithHashMatch;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
console.log('⚠️ Could not parse changes from release notes');
|
|
120
|
+
console.log(' Looking for pattern: ### [Major|Minor|Patch] Changes');
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Clean up the description:
|
|
125
|
+
// 1. Convert literal \n sequences (escaped newlines from GitHub API) to actual newlines
|
|
126
|
+
// 2. Remove leading/trailing quotes (including escaped quotes from command-stream shell escaping)
|
|
127
|
+
// 3. Remove any trailing npm package links or markdown that might be there
|
|
128
|
+
// 4. Normalize whitespace while preserving line breaks
|
|
129
|
+
const cleanDescription = rawDescription
|
|
130
|
+
.replace(/\\n/g, '\n') // Convert escaped \n to actual newlines
|
|
131
|
+
.replace(/^(\\['"])+/g, '') // Remove leading escaped quotes (e.g., \', \", \'', \'')
|
|
132
|
+
.replace(/(['"])+$/g, '') // Remove trailing unescaped quotes (e.g., ', ", '', '')
|
|
133
|
+
.replace(/^(['"])+/g, '') // Remove leading unescaped quotes
|
|
134
|
+
.replace(/📦.*$/s, '') // Remove any existing npm package info
|
|
135
|
+
.replace(/---.*$/s, '') // Remove any existing separators and everything after
|
|
136
|
+
.trim()
|
|
137
|
+
.split('\n') // Split by lines
|
|
138
|
+
.map((line) => line.trim()) // Trim whitespace from each line
|
|
139
|
+
.join('\n') // Rejoin with newlines
|
|
140
|
+
.replace(/\n{3,}/g, '\n\n'); // Normalize excessive blank lines (3+ becomes 2)
|
|
141
|
+
|
|
142
|
+
// Find the PR that contains the release commit
|
|
143
|
+
// Uses commit hash from changelog or passed commit SHA from workflow
|
|
144
|
+
let prNumber = null;
|
|
145
|
+
|
|
146
|
+
// Determine which commit SHA to use for PR lookup
|
|
147
|
+
const commitShaToLookup = commitHash || passedCommitSha;
|
|
148
|
+
|
|
149
|
+
if (commitShaToLookup) {
|
|
150
|
+
const source = commitHash ? 'changelog' : 'workflow';
|
|
151
|
+
console.log(
|
|
152
|
+
`ℹ️ Looking up PR for commit ${commitShaToLookup} (from ${source})`
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const prResult =
|
|
157
|
+
await $`gh api "repos/${repository}/commits/${commitShaToLookup}/pulls"`.run(
|
|
158
|
+
{ capture: true }
|
|
159
|
+
);
|
|
160
|
+
const prsData = JSON.parse(prResult.stdout);
|
|
161
|
+
|
|
162
|
+
// Find the PR that's not the version bump PR (not "chore: version packages")
|
|
163
|
+
const relevantPr = prsData.find(
|
|
164
|
+
(pr) => !pr.title.includes('version packages')
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (relevantPr) {
|
|
168
|
+
prNumber = relevantPr.number;
|
|
169
|
+
console.log(`✅ Found PR #${prNumber} containing commit`);
|
|
170
|
+
} else if (prsData.length > 0) {
|
|
171
|
+
console.log(
|
|
172
|
+
'⚠️ Found PRs but all are version bump PRs, not linking any'
|
|
173
|
+
);
|
|
174
|
+
} else {
|
|
175
|
+
console.log(
|
|
176
|
+
'ℹ️ No PR found containing this commit - not adding PR link'
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.log('⚠️ Could not find PR for commit', commitShaToLookup);
|
|
181
|
+
console.log(' Error:', error.message);
|
|
182
|
+
if (process.env.DEBUG) {
|
|
183
|
+
console.error(error);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
// No commit hash available from any source
|
|
188
|
+
console.log('ℹ️ No commit SHA available - not adding PR link');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Build formatted release notes
|
|
192
|
+
const versionWithoutV = version.replace(/^v/, '');
|
|
193
|
+
const npmBadge = `[](https://www.npmjs.com/package/${PACKAGE_NAME}/v/${versionWithoutV})`;
|
|
194
|
+
|
|
195
|
+
let formattedBody = `${cleanDescription}`;
|
|
196
|
+
|
|
197
|
+
// Add PR link if available
|
|
198
|
+
if (prNumber) {
|
|
199
|
+
formattedBody += `\n\n**Related Pull Request:** #${prNumber}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
formattedBody += `\n\n---\n\n${npmBadge}`;
|
|
203
|
+
|
|
204
|
+
// Update the release using JSON input to properly handle special characters
|
|
205
|
+
const updatePayload = JSON.stringify({ body: formattedBody });
|
|
206
|
+
await $`gh api repos/${repository}/releases/${releaseId} -X PATCH --input -`.run(
|
|
207
|
+
{ stdin: updatePayload }
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
console.log(`✅ Formatted release notes for v${versionWithoutV}`);
|
|
211
|
+
if (prNumber) {
|
|
212
|
+
console.log(` - Added link to PR #${prNumber}`);
|
|
213
|
+
}
|
|
214
|
+
console.log(' - Added shields.io npm badge');
|
|
215
|
+
console.log(' - Cleaned up formatting');
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error('❌ Error formatting release notes:', error.message);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|