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,302 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Publish to npm using OIDC trusted publishing
|
|
5
|
+
* Usage: node scripts/publish-to-npm.mjs [--should-pull] [--js-root <path>]
|
|
6
|
+
* should_pull: Optional flag to pull latest changes before publishing (for release job)
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: Update the PACKAGE_NAME constant below to match your package.json
|
|
9
|
+
*
|
|
10
|
+
* Configuration:
|
|
11
|
+
* - CLI: --js-root <path> to explicitly set JavaScript root
|
|
12
|
+
* - Environment: JS_ROOT=<path>
|
|
13
|
+
*
|
|
14
|
+
* Uses link-foundation libraries:
|
|
15
|
+
* - use-m: Dynamic package loading without package.json dependencies
|
|
16
|
+
* - command-stream: Modern shell command execution with streaming support
|
|
17
|
+
* - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files
|
|
18
|
+
*
|
|
19
|
+
* Addresses issues documented in:
|
|
20
|
+
* - Issue #21: Supporting both single and multi-language repository structures
|
|
21
|
+
* - Reference: link-assistant/agent PR #112 (--legacy-peer-deps fix)
|
|
22
|
+
* - Reference: link-assistant/agent PR #114 (configurable package root)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readFileSync, appendFileSync } from 'fs';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
getJsRoot,
|
|
29
|
+
getPackageJsonPath,
|
|
30
|
+
needsCd,
|
|
31
|
+
parseJsRootConfig,
|
|
32
|
+
} from './js-paths.mjs';
|
|
33
|
+
|
|
34
|
+
// TODO: Update this to match your package name in package.json
|
|
35
|
+
const PACKAGE_NAME = 'glab-setup-git-identity';
|
|
36
|
+
|
|
37
|
+
// Load use-m dynamically
|
|
38
|
+
const { use } = eval(
|
|
39
|
+
await (await fetch('https://unpkg.com/use-m/use.js')).text()
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Import link-foundation libraries
|
|
43
|
+
const { $ } = await use('command-stream');
|
|
44
|
+
const { makeConfig } = await use('lino-arguments');
|
|
45
|
+
|
|
46
|
+
// Parse CLI arguments using lino-arguments
|
|
47
|
+
const config = makeConfig({
|
|
48
|
+
yargs: ({ yargs, getenv }) =>
|
|
49
|
+
yargs
|
|
50
|
+
.option('should-pull', {
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
default: getenv('SHOULD_PULL', false),
|
|
53
|
+
describe: 'Pull latest changes before publishing',
|
|
54
|
+
})
|
|
55
|
+
.option('js-root', {
|
|
56
|
+
type: 'string',
|
|
57
|
+
default: getenv('JS_ROOT', ''),
|
|
58
|
+
describe:
|
|
59
|
+
'JavaScript package root directory (auto-detected if not specified)',
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const { shouldPull, jsRoot: jsRootArg } = config;
|
|
64
|
+
|
|
65
|
+
// Get JavaScript package root (auto-detect or use explicit config)
|
|
66
|
+
const jsRootConfig = jsRootArg || parseJsRootConfig();
|
|
67
|
+
const jsRoot = getJsRoot({ jsRoot: jsRootConfig, verbose: true });
|
|
68
|
+
|
|
69
|
+
const MAX_RETRIES = 3;
|
|
70
|
+
const RETRY_DELAY = 10000; // 10 seconds
|
|
71
|
+
|
|
72
|
+
// Store the original working directory to restore after cd commands
|
|
73
|
+
// IMPORTANT: command-stream's cd is a virtual command that calls process.chdir()
|
|
74
|
+
const originalCwd = process.cwd();
|
|
75
|
+
|
|
76
|
+
// Patterns that indicate publish failure in changeset output
|
|
77
|
+
// Reference: link-assistant/agent PR #116 - prevent false positives in CI/CD
|
|
78
|
+
const FAILURE_PATTERNS = [
|
|
79
|
+
'packages failed to publish',
|
|
80
|
+
'error occurred while publishing',
|
|
81
|
+
'npm error code E',
|
|
82
|
+
'npm error 404',
|
|
83
|
+
'npm error 401',
|
|
84
|
+
'npm error 403',
|
|
85
|
+
'Access token expired',
|
|
86
|
+
'ENEEDAUTH',
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Sleep for specified milliseconds
|
|
91
|
+
* @param {number} ms
|
|
92
|
+
*/
|
|
93
|
+
function sleep(ms) {
|
|
94
|
+
return new Promise((resolve) => globalThis.setTimeout(resolve, ms));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if the output contains any failure patterns
|
|
99
|
+
* Reference: link-assistant/agent PR #116
|
|
100
|
+
* @param {string} output - Combined stdout and stderr
|
|
101
|
+
* @returns {string|null} - The matched failure pattern or null if no failure detected
|
|
102
|
+
*/
|
|
103
|
+
function detectPublishFailure(output) {
|
|
104
|
+
const lowerOutput = output.toLowerCase();
|
|
105
|
+
for (const pattern of FAILURE_PATTERNS) {
|
|
106
|
+
if (lowerOutput.includes(pattern.toLowerCase())) {
|
|
107
|
+
return pattern;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Verify that a package version is published on npm
|
|
115
|
+
* Reference: link-assistant/agent PR #116
|
|
116
|
+
* @param {string} packageName
|
|
117
|
+
* @param {string} version
|
|
118
|
+
* @returns {Promise<boolean>}
|
|
119
|
+
*/
|
|
120
|
+
async function verifyPublished(packageName, version) {
|
|
121
|
+
const result = await $`npm view "${packageName}@${version}" version`.run({
|
|
122
|
+
capture: true,
|
|
123
|
+
});
|
|
124
|
+
return result.code === 0 && result.stdout.trim().includes(version);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Append to GitHub Actions output file
|
|
129
|
+
* @param {string} key
|
|
130
|
+
* @param {string} value
|
|
131
|
+
*/
|
|
132
|
+
function setOutput(key, value) {
|
|
133
|
+
const outputFile = process.env.GITHUB_OUTPUT;
|
|
134
|
+
if (outputFile) {
|
|
135
|
+
appendFileSync(outputFile, `${key}=${value}\n`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Run changeset:publish command with output capture
|
|
141
|
+
* @returns {Promise<{result: object|null, error: Error|null}>}
|
|
142
|
+
*/
|
|
143
|
+
async function runChangesetPublish() {
|
|
144
|
+
try {
|
|
145
|
+
// Run changeset:publish from the js directory where package.json with this script exists
|
|
146
|
+
// IMPORTANT: Use .run({ capture: true }) to capture output for failure detection
|
|
147
|
+
// IMPORTANT: cd is a virtual command that calls process.chdir(), so we restore after
|
|
148
|
+
if (needsCd({ jsRoot })) {
|
|
149
|
+
const result = await $`cd ${jsRoot} && npm run changeset:publish`.run({
|
|
150
|
+
capture: true,
|
|
151
|
+
});
|
|
152
|
+
process.chdir(originalCwd);
|
|
153
|
+
return { result, error: null };
|
|
154
|
+
}
|
|
155
|
+
const result = await $`npm run changeset:publish`.run({ capture: true });
|
|
156
|
+
return { result, error: null };
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// Restore cwd on error before retry
|
|
159
|
+
if (needsCd({ jsRoot })) {
|
|
160
|
+
process.chdir(originalCwd);
|
|
161
|
+
}
|
|
162
|
+
return { result: null, error };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Analyze publish result for failures using multi-layer detection
|
|
168
|
+
* Reference: link-assistant/agent PR #116
|
|
169
|
+
* @param {object|null} publishResult - The result from runChangesetPublish
|
|
170
|
+
* @param {Error|null} commandError - Error thrown by the command
|
|
171
|
+
* @returns {Error|null} - Error if failure detected, null otherwise
|
|
172
|
+
*/
|
|
173
|
+
function analyzePublishResult(publishResult, commandError) {
|
|
174
|
+
if (commandError) {
|
|
175
|
+
return commandError;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const combinedOutput = publishResult
|
|
179
|
+
? `${publishResult.stdout || ''}\n${publishResult.stderr || ''}`
|
|
180
|
+
: '';
|
|
181
|
+
|
|
182
|
+
// Log the output for debugging
|
|
183
|
+
if (combinedOutput.trim()) {
|
|
184
|
+
console.log('Changeset output:', combinedOutput);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check for failure patterns in output (most reliable for changeset)
|
|
188
|
+
const failurePattern = detectPublishFailure(combinedOutput);
|
|
189
|
+
if (failurePattern) {
|
|
190
|
+
console.error(`Detected publish failure: "${failurePattern}"`);
|
|
191
|
+
return new Error(`Publish failed: detected "${failurePattern}" in output`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check exit code (if available and non-zero)
|
|
195
|
+
if (publishResult && publishResult.code !== 0) {
|
|
196
|
+
console.error(`Changeset exited with code ${publishResult.code}`);
|
|
197
|
+
return new Error(`Publish failed with exit code ${publishResult.code}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Perform a single publish attempt with verification
|
|
205
|
+
* @param {string} currentVersion
|
|
206
|
+
* @returns {Promise<{success: boolean, error: Error|null}>}
|
|
207
|
+
*/
|
|
208
|
+
async function attemptPublish(currentVersion) {
|
|
209
|
+
const { result, error } = await runChangesetPublish();
|
|
210
|
+
const analysisError = analyzePublishResult(result, error);
|
|
211
|
+
|
|
212
|
+
if (analysisError) {
|
|
213
|
+
return { success: false, error: analysisError };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Verify the package is actually on npm (ultimate verification)
|
|
217
|
+
console.log('Verifying package was published to npm...');
|
|
218
|
+
await sleep(2000); // Wait for npm registry to propagate
|
|
219
|
+
const isPublished = await verifyPublished(PACKAGE_NAME, currentVersion);
|
|
220
|
+
|
|
221
|
+
if (isPublished) {
|
|
222
|
+
return { success: true, error: null };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.error('Verification failed: package not found on npm after publish');
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
error: new Error('Package not found on npm after publish attempt'),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function main() {
|
|
233
|
+
try {
|
|
234
|
+
if (shouldPull) {
|
|
235
|
+
// Pull the latest changes we just pushed
|
|
236
|
+
await $`git pull origin main`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Get current version
|
|
240
|
+
const packageJsonPath = getPackageJsonPath({ jsRoot });
|
|
241
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
242
|
+
const currentVersion = packageJson.version;
|
|
243
|
+
console.log(`Current version to publish: ${currentVersion}`);
|
|
244
|
+
|
|
245
|
+
// Check if this version is already published on npm
|
|
246
|
+
console.log(
|
|
247
|
+
`Checking if version ${currentVersion} is already published...`
|
|
248
|
+
);
|
|
249
|
+
const checkResult =
|
|
250
|
+
await $`npm view "${PACKAGE_NAME}@${currentVersion}" version`.run({
|
|
251
|
+
capture: true,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// command-stream returns { code: 0 } on success, { code: 1 } on failure (e.g., E404)
|
|
255
|
+
// Exit code 0 means version exists, non-zero means version not found
|
|
256
|
+
if (checkResult.code === 0) {
|
|
257
|
+
console.log(`Version ${currentVersion} is already published to npm`);
|
|
258
|
+
setOutput('published', 'true');
|
|
259
|
+
setOutput('published_version', currentVersion);
|
|
260
|
+
setOutput('already_published', 'true');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Version not found on npm (E404), proceed with publish
|
|
265
|
+
console.log(
|
|
266
|
+
`Version ${currentVersion} not found on npm, proceeding with publish...`
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Publish to npm using OIDC trusted publishing with retry logic
|
|
270
|
+
// Multi-layer failure detection based on link-assistant/agent PR #116
|
|
271
|
+
for (let i = 1; i <= MAX_RETRIES; i++) {
|
|
272
|
+
console.log(`Publish attempt ${i} of ${MAX_RETRIES}...`);
|
|
273
|
+
const { success, error } = await attemptPublish(currentVersion);
|
|
274
|
+
|
|
275
|
+
if (success) {
|
|
276
|
+
setOutput('published', 'true');
|
|
277
|
+
setOutput('published_version', currentVersion);
|
|
278
|
+
console.log(
|
|
279
|
+
`\u2705 Published ${PACKAGE_NAME}@${currentVersion} to npm`
|
|
280
|
+
);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (i < MAX_RETRIES) {
|
|
285
|
+
console.log(
|
|
286
|
+
`Publish failed: ${error.message}, waiting ${RETRY_DELAY / 1000}s before retry...`
|
|
287
|
+
);
|
|
288
|
+
await sleep(RETRY_DELAY);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.error(`\u274C Failed to publish after ${MAX_RETRIES} attempts`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
// Restore cwd on error
|
|
296
|
+
process.chdir(originalCwd);
|
|
297
|
+
console.error('Error:', error.message);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
main();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Update npm for OIDC trusted publishing
|
|
5
|
+
* npm trusted publishing requires npm >= 11.5.1
|
|
6
|
+
* Node.js 20.x ships with npm 10.x, so we need to update
|
|
7
|
+
*
|
|
8
|
+
* Uses link-foundation libraries:
|
|
9
|
+
* - use-m: Dynamic package loading without package.json dependencies
|
|
10
|
+
* - command-stream: Modern shell command execution with streaming support
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Load use-m dynamically
|
|
14
|
+
const { use } = eval(
|
|
15
|
+
await (await fetch('https://unpkg.com/use-m/use.js')).text()
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// Import command-stream for shell command execution
|
|
19
|
+
const { $ } = await use('command-stream');
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Get current npm version
|
|
23
|
+
const currentResult = await $`npm --version`.run({ capture: true });
|
|
24
|
+
const currentVersion = currentResult.stdout.trim();
|
|
25
|
+
console.log(`Current npm version: ${currentVersion}`);
|
|
26
|
+
|
|
27
|
+
// Update npm to latest
|
|
28
|
+
await $`npm install -g npm@latest`;
|
|
29
|
+
|
|
30
|
+
// Get updated npm version
|
|
31
|
+
const updatedResult = await $`npm --version`.run({ capture: true });
|
|
32
|
+
const updatedVersion = updatedResult.stdout.trim();
|
|
33
|
+
console.log(`Updated npm version: ${updatedVersion}`);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Error updating npm:', error.message);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validate changeset for CI - ensures exactly one valid changeset is added by the PR
|
|
5
|
+
*
|
|
6
|
+
* Key behavior:
|
|
7
|
+
* - Only checks changeset files ADDED by the current PR (not pre-existing ones)
|
|
8
|
+
* - Uses git diff to compare PR head against base branch
|
|
9
|
+
* - Validates that the PR adds exactly one changeset with proper format
|
|
10
|
+
* - Falls back to checking all changesets for local development
|
|
11
|
+
*
|
|
12
|
+
* IMPORTANT: Update the package name below to match your package.json
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
|
|
19
|
+
// TODO: Update this to match your package name in package.json
|
|
20
|
+
const PACKAGE_NAME = 'glab-setup-git-identity';
|
|
21
|
+
const CHANGESET_DIR = '.changeset';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ensure a git commit is available locally, fetching if necessary
|
|
25
|
+
* @param {string} sha The commit SHA to check
|
|
26
|
+
*/
|
|
27
|
+
function ensureCommitAvailable(sha) {
|
|
28
|
+
try {
|
|
29
|
+
execSync(`git cat-file -e ${sha}`, { stdio: 'ignore' });
|
|
30
|
+
} catch {
|
|
31
|
+
console.log('Base commit not available locally, attempting fetch...');
|
|
32
|
+
try {
|
|
33
|
+
execSync(`git fetch origin ${sha}`, { stdio: 'inherit' });
|
|
34
|
+
} catch {
|
|
35
|
+
execSync(`git fetch origin`, { stdio: 'inherit' });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse git diff output and extract added changeset files
|
|
42
|
+
* @param {string} diffOutput Output from git diff --name-status
|
|
43
|
+
* @returns {string[]} Array of added changeset file names
|
|
44
|
+
*/
|
|
45
|
+
function parseAddedChangesets(diffOutput) {
|
|
46
|
+
const addedChangesets = [];
|
|
47
|
+
for (const line of diffOutput.trim().split('\n')) {
|
|
48
|
+
if (!line) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const [status, filePath] = line.split('\t');
|
|
52
|
+
if (
|
|
53
|
+
status === 'A' &&
|
|
54
|
+
filePath.startsWith(`${CHANGESET_DIR}/`) &&
|
|
55
|
+
filePath.endsWith('.md') &&
|
|
56
|
+
!filePath.endsWith('README.md')
|
|
57
|
+
) {
|
|
58
|
+
addedChangesets.push(filePath.replace(`${CHANGESET_DIR}/`, ''));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return addedChangesets;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Try to get changesets using explicit SHA comparison
|
|
66
|
+
* @param {string} baseSha Base commit SHA
|
|
67
|
+
* @param {string} headSha Head commit SHA
|
|
68
|
+
* @returns {string[] | null} Array of changeset files or null if failed
|
|
69
|
+
*/
|
|
70
|
+
function tryExplicitShaComparison(baseSha, headSha) {
|
|
71
|
+
console.log(`Comparing ${baseSha}...${headSha}`);
|
|
72
|
+
try {
|
|
73
|
+
ensureCommitAvailable(baseSha);
|
|
74
|
+
const diffOutput = execSync(
|
|
75
|
+
`git diff --name-status ${baseSha} ${headSha}`,
|
|
76
|
+
{ encoding: 'utf-8' }
|
|
77
|
+
);
|
|
78
|
+
return parseAddedChangesets(diffOutput);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.log(`Git diff with explicit SHAs failed: ${error.message}`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Try to get changesets using base branch comparison
|
|
87
|
+
* @param {string} prBase Base branch name
|
|
88
|
+
* @returns {string[] | null} Array of changeset files or null if failed
|
|
89
|
+
*/
|
|
90
|
+
function tryBaseBranchComparison(prBase) {
|
|
91
|
+
console.log(`Comparing against base branch: ${prBase}`);
|
|
92
|
+
try {
|
|
93
|
+
try {
|
|
94
|
+
execSync(`git fetch origin ${prBase}`, { stdio: 'inherit' });
|
|
95
|
+
} catch {
|
|
96
|
+
// Ignore fetch errors, we might already have it
|
|
97
|
+
}
|
|
98
|
+
const diffOutput = execSync(
|
|
99
|
+
`git diff --name-status origin/${prBase}...HEAD`,
|
|
100
|
+
{ encoding: 'utf-8' }
|
|
101
|
+
);
|
|
102
|
+
return parseAddedChangesets(diffOutput);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.log(`Git diff with base ref failed: ${error.message}`);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Fallback: get all changesets in directory
|
|
111
|
+
* @returns {string[]} Array of all changeset file names
|
|
112
|
+
*/
|
|
113
|
+
function getAllChangesets() {
|
|
114
|
+
console.log(
|
|
115
|
+
'Warning: Could not determine PR diff, checking all changesets in directory'
|
|
116
|
+
);
|
|
117
|
+
if (!existsSync(CHANGESET_DIR)) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
return readdirSync(CHANGESET_DIR).filter(
|
|
121
|
+
(file) => file.endsWith('.md') && file !== 'README.md'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get changeset files added in the current PR using git diff
|
|
127
|
+
* @returns {string[]} Array of added changeset file names
|
|
128
|
+
*/
|
|
129
|
+
function getAddedChangesetFiles() {
|
|
130
|
+
const baseSha = process.env.GITHUB_BASE_SHA || process.env.BASE_SHA;
|
|
131
|
+
const headSha = process.env.GITHUB_HEAD_SHA || process.env.HEAD_SHA;
|
|
132
|
+
|
|
133
|
+
// Try explicit SHAs first
|
|
134
|
+
if (baseSha && headSha) {
|
|
135
|
+
const result = tryExplicitShaComparison(baseSha, headSha);
|
|
136
|
+
if (result !== null) {
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Try base branch comparison
|
|
142
|
+
const prBase = process.env.GITHUB_BASE_REF;
|
|
143
|
+
if (prBase) {
|
|
144
|
+
const result = tryBaseBranchComparison(prBase);
|
|
145
|
+
if (result !== null) {
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Fallback to checking all changesets
|
|
151
|
+
return getAllChangesets();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Validate a single changeset file
|
|
156
|
+
* @param {string} filePath Full path to the changeset file
|
|
157
|
+
* @returns {{valid: boolean, type?: string, description?: string, error?: string}}
|
|
158
|
+
*/
|
|
159
|
+
function validateChangesetFile(filePath) {
|
|
160
|
+
try {
|
|
161
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
162
|
+
|
|
163
|
+
// Check if changeset has a valid type (major, minor, or patch)
|
|
164
|
+
const versionTypeRegex = new RegExp(
|
|
165
|
+
`^['"]${PACKAGE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]:\\s+(major|minor|patch)`,
|
|
166
|
+
'm'
|
|
167
|
+
);
|
|
168
|
+
const versionTypeMatch = content.match(versionTypeRegex);
|
|
169
|
+
|
|
170
|
+
if (!versionTypeMatch) {
|
|
171
|
+
return {
|
|
172
|
+
valid: false,
|
|
173
|
+
error: `Changeset must specify a version type: major, minor, or patch\nExpected format:\n---\n'${PACKAGE_NAME}': patch\n---\n\nYour description here`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Extract description (everything after the closing ---) and check it's not empty
|
|
178
|
+
const parts = content.split('---');
|
|
179
|
+
if (parts.length < 3) {
|
|
180
|
+
return {
|
|
181
|
+
valid: false,
|
|
182
|
+
error:
|
|
183
|
+
"Changeset must include a description of the changes (after the closing '---')",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const description = parts.slice(2).join('---').trim();
|
|
188
|
+
if (!description) {
|
|
189
|
+
return {
|
|
190
|
+
valid: false,
|
|
191
|
+
error: 'Changeset must include a non-empty description of the changes',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
valid: true,
|
|
197
|
+
type: versionTypeMatch[1],
|
|
198
|
+
description,
|
|
199
|
+
};
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return {
|
|
202
|
+
valid: false,
|
|
203
|
+
error: `Failed to read changeset file: ${error.message}`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
console.log('Validating changesets added by this PR...');
|
|
210
|
+
|
|
211
|
+
// Get changeset files added in this PR
|
|
212
|
+
const addedChangesetFiles = getAddedChangesetFiles();
|
|
213
|
+
const changesetCount = addedChangesetFiles.length;
|
|
214
|
+
|
|
215
|
+
console.log(`Found ${changesetCount} changeset file(s) added by this PR`);
|
|
216
|
+
if (changesetCount > 0) {
|
|
217
|
+
console.log('Added changesets:');
|
|
218
|
+
addedChangesetFiles.forEach((file) => console.log(` - ${file}`));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Ensure exactly one changeset file was added
|
|
222
|
+
if (changesetCount === 0) {
|
|
223
|
+
console.error(
|
|
224
|
+
"::error::No changeset found in this PR. Please add a changeset by running 'npm run changeset' and commit the result."
|
|
225
|
+
);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
} else if (changesetCount > 1) {
|
|
228
|
+
console.error(
|
|
229
|
+
`::error::Multiple changesets found in this PR (${changesetCount}). Each PR should add exactly ONE changeset.`
|
|
230
|
+
);
|
|
231
|
+
console.error('::error::Found changeset files added by this PR:');
|
|
232
|
+
addedChangesetFiles.forEach((file) => console.error(` ${file}`));
|
|
233
|
+
console.error(
|
|
234
|
+
'\n::error::Please combine these into a single changeset or remove the extras.'
|
|
235
|
+
);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Validate the single changeset file
|
|
240
|
+
const changesetFile = join(CHANGESET_DIR, addedChangesetFiles[0]);
|
|
241
|
+
console.log(`Validating changeset: ${changesetFile}`);
|
|
242
|
+
|
|
243
|
+
const validation = validateChangesetFile(changesetFile);
|
|
244
|
+
|
|
245
|
+
if (!validation.valid) {
|
|
246
|
+
console.error(`::error::${validation.error}`);
|
|
247
|
+
console.error(`\nFile content of ${changesetFile}:`);
|
|
248
|
+
try {
|
|
249
|
+
console.error(readFileSync(changesetFile, 'utf-8'));
|
|
250
|
+
} catch {
|
|
251
|
+
console.error('(could not read file)');
|
|
252
|
+
}
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
console.log('Changeset validation passed');
|
|
257
|
+
console.log(` Type: ${validation.type}`);
|
|
258
|
+
console.log(` Description: ${validation.description}`);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error('Error during changeset validation:', error.message);
|
|
261
|
+
if (process.env.DEBUG) {
|
|
262
|
+
console.error('Stack trace:', error.stack);
|
|
263
|
+
}
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|