threadlines 0.1.34 → 0.1.36
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/dist/commands/check.js +126 -40
- package/dist/git/github.js +288 -0
- package/dist/git/gitlab.js +220 -0
- package/dist/git/local.js +189 -0
- package/dist/git/vercel.js +136 -0
- package/dist/utils/metadata.js +89 -42
- package/package.json +1 -1
package/dist/commands/check.js
CHANGED
|
@@ -42,10 +42,11 @@ const file_1 = require("../git/file");
|
|
|
42
42
|
const client_1 = require("../api/client");
|
|
43
43
|
const config_1 = require("../utils/config");
|
|
44
44
|
const environment_1 = require("../utils/environment");
|
|
45
|
-
const
|
|
46
|
-
const
|
|
45
|
+
const github_1 = require("../git/github");
|
|
46
|
+
const gitlab_1 = require("../git/gitlab");
|
|
47
|
+
const vercel_1 = require("../git/vercel");
|
|
48
|
+
const local_1 = require("../git/local");
|
|
47
49
|
const git_diff_executor_1 = require("../utils/git-diff-executor");
|
|
48
|
-
const context_2 = require("../git/context");
|
|
49
50
|
const fs = __importStar(require("fs"));
|
|
50
51
|
const path = __importStar(require("path"));
|
|
51
52
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -108,6 +109,7 @@ async function checkCommand(options) {
|
|
|
108
109
|
let gitDiff;
|
|
109
110
|
let repoName;
|
|
110
111
|
let branchName;
|
|
112
|
+
let metadata = {};
|
|
111
113
|
// Validate mutually exclusive flags
|
|
112
114
|
const explicitFlags = [options.branch, options.commit, options.file, options.folder, options.files].filter(Boolean);
|
|
113
115
|
if (explicitFlags.length > 1) {
|
|
@@ -138,22 +140,108 @@ async function checkCommand(options) {
|
|
|
138
140
|
console.log(chalk_1.default.gray(`📝 Collecting git changes for branch: ${options.branch}...`));
|
|
139
141
|
context = { type: 'branch', branchName: options.branch };
|
|
140
142
|
gitDiff = await (0, git_diff_executor_1.getDiffForContext)(context, repoRoot, environment);
|
|
141
|
-
// Get repo/branch using
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
// Get repo/branch using environment-specific approach
|
|
144
|
+
if (environment === 'github') {
|
|
145
|
+
const gitContext = await (0, github_1.getGitHubContext)(repoRoot);
|
|
146
|
+
repoName = gitContext.repoName;
|
|
147
|
+
branchName = gitContext.branchName;
|
|
148
|
+
metadata = {
|
|
149
|
+
commitSha: gitContext.commitSha,
|
|
150
|
+
commitMessage: gitContext.commitMessage,
|
|
151
|
+
commitAuthorName: gitContext.commitAuthor.name,
|
|
152
|
+
commitAuthorEmail: gitContext.commitAuthor.email,
|
|
153
|
+
prTitle: gitContext.prTitle
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
else if (environment === 'gitlab') {
|
|
157
|
+
const gitContext = await (0, gitlab_1.getGitLabContext)(repoRoot);
|
|
158
|
+
repoName = gitContext.repoName;
|
|
159
|
+
branchName = gitContext.branchName;
|
|
160
|
+
metadata = {
|
|
161
|
+
commitSha: gitContext.commitSha,
|
|
162
|
+
commitMessage: gitContext.commitMessage,
|
|
163
|
+
commitAuthorName: gitContext.commitAuthor.name,
|
|
164
|
+
commitAuthorEmail: gitContext.commitAuthor.email,
|
|
165
|
+
prTitle: gitContext.prTitle
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
else if (environment === 'vercel') {
|
|
169
|
+
const gitContext = await (0, vercel_1.getVercelContext)(repoRoot);
|
|
170
|
+
repoName = gitContext.repoName;
|
|
171
|
+
branchName = gitContext.branchName;
|
|
172
|
+
metadata = {
|
|
173
|
+
commitSha: gitContext.commitSha,
|
|
174
|
+
commitMessage: gitContext.commitMessage,
|
|
175
|
+
commitAuthorName: gitContext.commitAuthor.name,
|
|
176
|
+
commitAuthorEmail: gitContext.commitAuthor.email
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const gitContext = await (0, local_1.getLocalContext)(repoRoot);
|
|
181
|
+
repoName = gitContext.repoName;
|
|
182
|
+
branchName = gitContext.branchName;
|
|
183
|
+
metadata = {
|
|
184
|
+
commitSha: gitContext.commitSha,
|
|
185
|
+
commitMessage: gitContext.commitMessage,
|
|
186
|
+
commitAuthorName: gitContext.commitAuthor.name,
|
|
187
|
+
commitAuthorEmail: gitContext.commitAuthor.email
|
|
188
|
+
};
|
|
189
|
+
}
|
|
145
190
|
}
|
|
146
191
|
else if (options.commit) {
|
|
147
192
|
console.log(chalk_1.default.gray(`📝 Collecting git changes for commit: ${options.commit}...`));
|
|
148
193
|
context = { type: 'commit', commitSha: options.commit };
|
|
149
194
|
gitDiff = await (0, git_diff_executor_1.getDiffForContext)(context, repoRoot, environment);
|
|
150
|
-
// Get repo/branch using
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
195
|
+
// Get repo/branch using environment-specific approach
|
|
196
|
+
if (environment === 'github') {
|
|
197
|
+
const gitContext = await (0, github_1.getGitHubContext)(repoRoot);
|
|
198
|
+
repoName = gitContext.repoName;
|
|
199
|
+
branchName = gitContext.branchName;
|
|
200
|
+
metadata = {
|
|
201
|
+
commitSha: gitContext.commitSha,
|
|
202
|
+
commitMessage: gitContext.commitMessage,
|
|
203
|
+
commitAuthorName: gitContext.commitAuthor.name,
|
|
204
|
+
commitAuthorEmail: gitContext.commitAuthor.email,
|
|
205
|
+
prTitle: gitContext.prTitle
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
else if (environment === 'gitlab') {
|
|
209
|
+
const gitContext = await (0, gitlab_1.getGitLabContext)(repoRoot);
|
|
210
|
+
repoName = gitContext.repoName;
|
|
211
|
+
branchName = gitContext.branchName;
|
|
212
|
+
metadata = {
|
|
213
|
+
commitSha: gitContext.commitSha,
|
|
214
|
+
commitMessage: gitContext.commitMessage,
|
|
215
|
+
commitAuthorName: gitContext.commitAuthor.name,
|
|
216
|
+
commitAuthorEmail: gitContext.commitAuthor.email,
|
|
217
|
+
prTitle: gitContext.prTitle
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
else if (environment === 'vercel') {
|
|
221
|
+
const gitContext = await (0, vercel_1.getVercelContext)(repoRoot);
|
|
222
|
+
repoName = gitContext.repoName;
|
|
223
|
+
branchName = gitContext.branchName;
|
|
224
|
+
metadata = {
|
|
225
|
+
commitSha: gitContext.commitSha,
|
|
226
|
+
commitMessage: gitContext.commitMessage,
|
|
227
|
+
commitAuthorName: gitContext.commitAuthor.name,
|
|
228
|
+
commitAuthorEmail: gitContext.commitAuthor.email
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
const gitContext = await (0, local_1.getLocalContext)(repoRoot, options.commit);
|
|
233
|
+
repoName = gitContext.repoName;
|
|
234
|
+
branchName = gitContext.branchName;
|
|
235
|
+
metadata = {
|
|
236
|
+
commitSha: gitContext.commitSha,
|
|
237
|
+
commitMessage: gitContext.commitMessage,
|
|
238
|
+
commitAuthorName: gitContext.commitAuthor.name,
|
|
239
|
+
commitAuthorEmail: gitContext.commitAuthor.email
|
|
240
|
+
};
|
|
241
|
+
}
|
|
154
242
|
}
|
|
155
243
|
else {
|
|
156
|
-
// Auto-detect: Use
|
|
244
|
+
// Auto-detect: Use environment-specific context collection (completely isolated)
|
|
157
245
|
const envNames = {
|
|
158
246
|
vercel: 'Vercel',
|
|
159
247
|
github: 'GitHub',
|
|
@@ -161,33 +249,32 @@ async function checkCommand(options) {
|
|
|
161
249
|
local: 'Local'
|
|
162
250
|
};
|
|
163
251
|
console.log(chalk_1.default.gray(`📝 Collecting git context for ${envNames[environment]}...`));
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
context = { type: 'commit', commitSha: process.env.VERCEL_GIT_COMMIT_SHA };
|
|
252
|
+
// Get all context from environment-specific module
|
|
253
|
+
let envContext;
|
|
254
|
+
if (environment === 'github') {
|
|
255
|
+
envContext = await (0, github_1.getGitHubContext)(repoRoot);
|
|
256
|
+
}
|
|
257
|
+
else if (environment === 'gitlab') {
|
|
258
|
+
envContext = await (0, gitlab_1.getGitLabContext)(repoRoot);
|
|
172
259
|
}
|
|
173
|
-
else if (environment === '
|
|
174
|
-
|
|
175
|
-
context = (0, context_1.detectContext)(environment);
|
|
260
|
+
else if (environment === 'vercel') {
|
|
261
|
+
envContext = await (0, vercel_1.getVercelContext)(repoRoot);
|
|
176
262
|
}
|
|
177
263
|
else {
|
|
178
|
-
|
|
179
|
-
context = { type: 'local' };
|
|
264
|
+
envContext = await (0, local_1.getLocalContext)(repoRoot);
|
|
180
265
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
266
|
+
gitDiff = envContext.diff;
|
|
267
|
+
repoName = envContext.repoName;
|
|
268
|
+
branchName = envContext.branchName;
|
|
269
|
+
context = envContext.context;
|
|
270
|
+
// Use metadata from environment context
|
|
271
|
+
metadata = {
|
|
272
|
+
commitSha: envContext.commitSha,
|
|
273
|
+
commitMessage: envContext.commitMessage,
|
|
274
|
+
commitAuthorName: envContext.commitAuthor.name,
|
|
275
|
+
commitAuthorEmail: envContext.commitAuthor.email,
|
|
276
|
+
prTitle: envContext.prTitle
|
|
277
|
+
};
|
|
191
278
|
}
|
|
192
279
|
if (gitDiff.changedFiles.length === 0) {
|
|
193
280
|
console.error(chalk_1.default.red('❌ Error: No changes detected.'));
|
|
@@ -233,7 +320,7 @@ async function checkCommand(options) {
|
|
|
233
320
|
// 6. Call review API
|
|
234
321
|
console.log(chalk_1.default.gray('🤖 Running threadline checks...'));
|
|
235
322
|
const client = new client_1.ReviewAPIClient(apiUrl);
|
|
236
|
-
const
|
|
323
|
+
const response = await client.review({
|
|
237
324
|
threadlines: threadlinesWithContext,
|
|
238
325
|
diff: gitDiff.diff,
|
|
239
326
|
files: gitDiff.changedFiles,
|
|
@@ -246,10 +333,9 @@ async function checkCommand(options) {
|
|
|
246
333
|
commitAuthorName: metadata.commitAuthorName,
|
|
247
334
|
commitAuthorEmail: metadata.commitAuthorEmail,
|
|
248
335
|
prTitle: metadata.prTitle,
|
|
249
|
-
environment: environment
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const response = await client.review(reviewRequest);
|
|
336
|
+
environment: environment,
|
|
337
|
+
cliVersion: CLI_VERSION
|
|
338
|
+
});
|
|
253
339
|
// 7. Display results (with filtering if --full not specified)
|
|
254
340
|
displayResults(response, options.full || false);
|
|
255
341
|
// Exit with appropriate code
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Actions Environment - Complete Isolation
|
|
4
|
+
*
|
|
5
|
+
* All GitHub-specific logic is contained in this file.
|
|
6
|
+
* No dependencies on other environment implementations.
|
|
7
|
+
*
|
|
8
|
+
* Exports a single function: getGitHubContext() that returns:
|
|
9
|
+
* - diff: GitDiffResult
|
|
10
|
+
* - repoName: string
|
|
11
|
+
* - branchName: string
|
|
12
|
+
* - commitAuthor: { name: string; email: string }
|
|
13
|
+
* - prTitle?: string
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
49
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
50
|
+
};
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.getGitHubContext = getGitHubContext;
|
|
53
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const repo_1 = require("./repo");
|
|
56
|
+
const diff_1 = require("./diff");
|
|
57
|
+
/**
|
|
58
|
+
* Gets all GitHub context in one call - completely isolated from other environments.
|
|
59
|
+
*/
|
|
60
|
+
async function getGitHubContext(repoRoot) {
|
|
61
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
62
|
+
// Check if we're in a git repo
|
|
63
|
+
const isRepo = await git.checkIsRepo();
|
|
64
|
+
if (!isRepo) {
|
|
65
|
+
throw new Error('Not a git repository. Threadline requires a git repository.');
|
|
66
|
+
}
|
|
67
|
+
// Get all GitHub context
|
|
68
|
+
const diff = await getDiff(repoRoot);
|
|
69
|
+
const repoName = await getRepoName();
|
|
70
|
+
const branchName = await getBranchName();
|
|
71
|
+
const context = detectContext();
|
|
72
|
+
const commitSha = getCommitSha(context);
|
|
73
|
+
// Get commit author (fails loudly if unavailable)
|
|
74
|
+
// Note: commitSha parameter not needed - GitHub reads from GITHUB_EVENT_PATH JSON
|
|
75
|
+
const commitAuthor = await getCommitAuthor();
|
|
76
|
+
// Get commit message if we have a SHA
|
|
77
|
+
let commitMessage;
|
|
78
|
+
if (commitSha) {
|
|
79
|
+
const message = await (0, diff_1.getCommitMessage)(repoRoot, commitSha);
|
|
80
|
+
if (message) {
|
|
81
|
+
commitMessage = message;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Get PR title if in PR context
|
|
85
|
+
const prTitle = getPRTitle(context);
|
|
86
|
+
return {
|
|
87
|
+
diff,
|
|
88
|
+
repoName,
|
|
89
|
+
branchName,
|
|
90
|
+
commitSha,
|
|
91
|
+
commitMessage,
|
|
92
|
+
commitAuthor,
|
|
93
|
+
prTitle,
|
|
94
|
+
context
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Gets diff for GitHub Actions CI environment
|
|
99
|
+
*/
|
|
100
|
+
async function getDiff(repoRoot) {
|
|
101
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
102
|
+
const defaultBranch = await (0, repo_1.getDefaultBranchName)(repoRoot);
|
|
103
|
+
// Determine context from GitHub environment variables
|
|
104
|
+
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
105
|
+
const baseRef = process.env.GITHUB_BASE_REF;
|
|
106
|
+
const headRef = process.env.GITHUB_HEAD_REF;
|
|
107
|
+
const refName = process.env.GITHUB_REF_NAME;
|
|
108
|
+
const commitSha = process.env.GITHUB_SHA;
|
|
109
|
+
// Scenario 1: PR Context
|
|
110
|
+
if (eventName === 'pull_request') {
|
|
111
|
+
if (!baseRef || !headRef) {
|
|
112
|
+
throw new Error('GitHub PR context detected but GITHUB_BASE_REF or GITHUB_HEAD_REF is missing. ' +
|
|
113
|
+
'This should be automatically provided by GitHub Actions.');
|
|
114
|
+
}
|
|
115
|
+
const diff = await git.diff([`origin/${baseRef}...origin/${headRef}`, '-U200']);
|
|
116
|
+
const diffSummary = await git.diffSummary([`origin/${baseRef}...origin/${headRef}`]);
|
|
117
|
+
const changedFiles = diffSummary.files.map(f => f.file);
|
|
118
|
+
return {
|
|
119
|
+
diff: diff || '',
|
|
120
|
+
changedFiles
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// Scenario 2 & 4: Default Branch Push
|
|
124
|
+
if (refName === defaultBranch && commitSha) {
|
|
125
|
+
try {
|
|
126
|
+
const diff = await git.diff([`origin/${defaultBranch}~1...origin/${defaultBranch}`, '-U200']);
|
|
127
|
+
const diffSummary = await git.diffSummary([`origin/${defaultBranch}~1...origin/${defaultBranch}`]);
|
|
128
|
+
const changedFiles = diffSummary.files.map(f => f.file);
|
|
129
|
+
return {
|
|
130
|
+
diff: diff || '',
|
|
131
|
+
changedFiles
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
136
|
+
throw new Error(`Could not get diff for default branch '${defaultBranch}'. ` +
|
|
137
|
+
`This might be the first commit on the branch. Error: ${errorMessage}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Scenario 3: Feature Branch Push
|
|
141
|
+
if (refName) {
|
|
142
|
+
const diff = await git.diff([`origin/${defaultBranch}...origin/${refName}`, '-U200']);
|
|
143
|
+
const diffSummary = await git.diffSummary([`origin/${defaultBranch}...origin/${refName}`]);
|
|
144
|
+
const changedFiles = diffSummary.files.map(f => f.file);
|
|
145
|
+
return {
|
|
146
|
+
diff: diff || '',
|
|
147
|
+
changedFiles
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
throw new Error('GitHub Actions environment detected but no valid context found. ' +
|
|
151
|
+
'Expected GITHUB_EVENT_NAME="pull_request" (with GITHUB_BASE_REF/GITHUB_HEAD_REF) ' +
|
|
152
|
+
'or GITHUB_REF_NAME for branch context.');
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Gets repository name for GitHub Actions
|
|
156
|
+
*/
|
|
157
|
+
async function getRepoName() {
|
|
158
|
+
const githubRepo = process.env.GITHUB_REPOSITORY;
|
|
159
|
+
if (!githubRepo) {
|
|
160
|
+
throw new Error('GitHub Actions: GITHUB_REPOSITORY environment variable is not set. ' +
|
|
161
|
+
'This should be automatically provided by GitHub Actions.');
|
|
162
|
+
}
|
|
163
|
+
const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com';
|
|
164
|
+
return `${serverUrl}/${githubRepo}.git`;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Gets branch name for GitHub Actions
|
|
168
|
+
*/
|
|
169
|
+
async function getBranchName() {
|
|
170
|
+
const refName = process.env.GITHUB_REF_NAME;
|
|
171
|
+
if (!refName) {
|
|
172
|
+
throw new Error('GitHub Actions: GITHUB_REF_NAME environment variable is not set. ' +
|
|
173
|
+
'This should be automatically provided by GitHub Actions.');
|
|
174
|
+
}
|
|
175
|
+
return refName;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Detects GitHub context (PR, branch, or commit)
|
|
179
|
+
*/
|
|
180
|
+
function detectContext() {
|
|
181
|
+
// 1. Check for PR context
|
|
182
|
+
if (process.env.GITHUB_EVENT_NAME === 'pull_request') {
|
|
183
|
+
const targetBranch = process.env.GITHUB_BASE_REF;
|
|
184
|
+
const sourceBranch = process.env.GITHUB_HEAD_REF;
|
|
185
|
+
const prNumber = process.env.GITHUB_EVENT_PULL_REQUEST_NUMBER || process.env.GITHUB_EVENT_NUMBER;
|
|
186
|
+
if (targetBranch && sourceBranch && prNumber) {
|
|
187
|
+
return {
|
|
188
|
+
type: 'pr',
|
|
189
|
+
prNumber,
|
|
190
|
+
sourceBranch,
|
|
191
|
+
targetBranch
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// 2. Check for branch context
|
|
196
|
+
if (process.env.GITHUB_REF_NAME) {
|
|
197
|
+
return {
|
|
198
|
+
type: 'branch',
|
|
199
|
+
branchName: process.env.GITHUB_REF_NAME
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// 3. Check for commit context
|
|
203
|
+
if (process.env.GITHUB_SHA) {
|
|
204
|
+
return {
|
|
205
|
+
type: 'commit',
|
|
206
|
+
commitSha: process.env.GITHUB_SHA
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// 4. Fallback to local (shouldn't happen in GitHub Actions, but TypeScript needs it)
|
|
210
|
+
return { type: 'local' };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Gets commit SHA from context
|
|
214
|
+
*/
|
|
215
|
+
function getCommitSha(context) {
|
|
216
|
+
if (context.type === 'commit') {
|
|
217
|
+
return context.commitSha;
|
|
218
|
+
}
|
|
219
|
+
if (context.type === 'branch' || context.type === 'pr') {
|
|
220
|
+
return process.env.GITHUB_SHA;
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Gets commit author for GitHub Actions
|
|
226
|
+
* Reads from GITHUB_EVENT_PATH JSON file (most reliable)
|
|
227
|
+
* Note: commitSha parameter not used - GitHub provides author info in event JSON
|
|
228
|
+
*/
|
|
229
|
+
async function getCommitAuthor() {
|
|
230
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
231
|
+
if (!eventPath) {
|
|
232
|
+
throw new Error('GitHub Actions: GITHUB_EVENT_PATH environment variable is not set. ' +
|
|
233
|
+
'This should be automatically provided by GitHub Actions.');
|
|
234
|
+
}
|
|
235
|
+
if (!fs.existsSync(eventPath)) {
|
|
236
|
+
throw new Error(`GitHub Actions: GITHUB_EVENT_PATH file does not exist: ${eventPath}. ` +
|
|
237
|
+
'This should be automatically provided by GitHub Actions.');
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const eventData = JSON.parse(fs.readFileSync(eventPath, 'utf-8'));
|
|
241
|
+
// For push events, use head_commit.author
|
|
242
|
+
if (eventData.head_commit?.author) {
|
|
243
|
+
return {
|
|
244
|
+
name: eventData.head_commit.author.name,
|
|
245
|
+
email: eventData.head_commit.author.email
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// For PR events, use commits[0].author (first commit in the PR)
|
|
249
|
+
if (eventData.commits && eventData.commits.length > 0 && eventData.commits[0].author) {
|
|
250
|
+
return {
|
|
251
|
+
name: eventData.commits[0].author.name,
|
|
252
|
+
email: eventData.commits[0].author.email
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// Fallback to pull_request.head.commit.author for PR events
|
|
256
|
+
if (eventData.pull_request?.head?.commit?.author) {
|
|
257
|
+
return {
|
|
258
|
+
name: eventData.pull_request.head.commit.author.name,
|
|
259
|
+
email: eventData.pull_request.head.commit.author.email
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
// If we get here, the event JSON doesn't contain author info
|
|
263
|
+
throw new Error(`GitHub Actions: GITHUB_EVENT_PATH JSON does not contain commit author information. ` +
|
|
264
|
+
`Event type: ${eventData.action || 'unknown'}. ` +
|
|
265
|
+
`This should be automatically provided by GitHub Actions.`);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
// If JSON parsing fails, fail loudly
|
|
269
|
+
if (error instanceof Error && error.message.includes('GitHub Actions:')) {
|
|
270
|
+
throw error; // Re-throw our own errors
|
|
271
|
+
}
|
|
272
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
273
|
+
throw new Error(`GitHub Actions: Failed to read or parse GITHUB_EVENT_PATH JSON: ${errorMessage}. ` +
|
|
274
|
+
'This should be automatically provided by GitHub Actions.');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Gets PR title for GitHub Actions
|
|
279
|
+
* Note: GitHub Actions doesn't provide PR title as an env var by default.
|
|
280
|
+
* It would need to be passed from the workflow YAML or fetched via API.
|
|
281
|
+
*/
|
|
282
|
+
function getPRTitle(context) {
|
|
283
|
+
if (context.type !== 'pr') {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
// Only if passed from workflow: PR_TITLE: ${{ github.event.pull_request.title }}
|
|
287
|
+
return process.env.PR_TITLE;
|
|
288
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GitLab CI Environment - Complete Isolation
|
|
4
|
+
*
|
|
5
|
+
* All GitLab-specific logic is contained in this file.
|
|
6
|
+
* No dependencies on other environment implementations.
|
|
7
|
+
*
|
|
8
|
+
* Exports a single function: getGitLabContext() that returns:
|
|
9
|
+
* - diff: GitDiffResult
|
|
10
|
+
* - repoName: string
|
|
11
|
+
* - branchName: string
|
|
12
|
+
* - commitAuthor: { name: string; email: string }
|
|
13
|
+
* - prTitle?: string (MR title)
|
|
14
|
+
*/
|
|
15
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
16
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
17
|
+
};
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.getGitLabContext = getGitLabContext;
|
|
20
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
21
|
+
const diff_1 = require("./diff");
|
|
22
|
+
/**
|
|
23
|
+
* Gets all GitLab context in one call - completely isolated from other environments.
|
|
24
|
+
*/
|
|
25
|
+
async function getGitLabContext(repoRoot) {
|
|
26
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
27
|
+
// Check if we're in a git repo
|
|
28
|
+
const isRepo = await git.checkIsRepo();
|
|
29
|
+
if (!isRepo) {
|
|
30
|
+
throw new Error('Not a git repository. Threadline requires a git repository.');
|
|
31
|
+
}
|
|
32
|
+
// Get all GitLab context
|
|
33
|
+
const diff = await getDiff(repoRoot);
|
|
34
|
+
const repoName = await getRepoName();
|
|
35
|
+
const branchName = await getBranchName();
|
|
36
|
+
const context = detectContext();
|
|
37
|
+
const commitSha = getCommitSha(context);
|
|
38
|
+
// Get commit author (fails loudly if unavailable)
|
|
39
|
+
const commitAuthor = await getCommitAuthor();
|
|
40
|
+
// Get commit message if we have a SHA
|
|
41
|
+
let commitMessage;
|
|
42
|
+
if (commitSha) {
|
|
43
|
+
const message = await (0, diff_1.getCommitMessage)(repoRoot, commitSha);
|
|
44
|
+
if (message) {
|
|
45
|
+
commitMessage = message;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Get MR title if in MR context
|
|
49
|
+
const prTitle = getMRTitle(context);
|
|
50
|
+
return {
|
|
51
|
+
diff,
|
|
52
|
+
repoName,
|
|
53
|
+
branchName,
|
|
54
|
+
commitSha,
|
|
55
|
+
commitMessage,
|
|
56
|
+
commitAuthor,
|
|
57
|
+
prTitle,
|
|
58
|
+
context
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get diff for GitLab CI environment
|
|
63
|
+
*
|
|
64
|
+
* GitLab CI does a shallow clone of ONLY the current branch. The default branch
|
|
65
|
+
* (e.g., origin/main) is NOT available by default. We fetch it on-demand.
|
|
66
|
+
*
|
|
67
|
+
* Scenarios handled:
|
|
68
|
+
*
|
|
69
|
+
* 1. MR Context (CI_MERGE_REQUEST_IID is set):
|
|
70
|
+
* - Fetch target branch, then diff target vs source
|
|
71
|
+
*
|
|
72
|
+
* 2. Feature Branch Push (CI_COMMIT_REF_NAME != CI_DEFAULT_BRANCH):
|
|
73
|
+
* - Fetch default branch, then diff default vs feature
|
|
74
|
+
*
|
|
75
|
+
* 3. Default Branch Push (CI_COMMIT_REF_NAME == CI_DEFAULT_BRANCH):
|
|
76
|
+
* - Use HEAD~1...HEAD (last commit only, no fetch needed)
|
|
77
|
+
*/
|
|
78
|
+
async function getDiff(repoRoot) {
|
|
79
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
80
|
+
// Get GitLab CI environment variables
|
|
81
|
+
const mrIid = process.env.CI_MERGE_REQUEST_IID;
|
|
82
|
+
const targetBranch = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
|
|
83
|
+
const sourceBranch = process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME;
|
|
84
|
+
const refName = process.env.CI_COMMIT_REF_NAME;
|
|
85
|
+
const defaultBranch = process.env.CI_DEFAULT_BRANCH || 'main';
|
|
86
|
+
// Scenario 1: MR Context
|
|
87
|
+
if (mrIid) {
|
|
88
|
+
if (!targetBranch || !sourceBranch) {
|
|
89
|
+
throw new Error('GitLab MR context detected but CI_MERGE_REQUEST_TARGET_BRANCH_NAME or ' +
|
|
90
|
+
'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME is missing. ' +
|
|
91
|
+
'This should be automatically provided by GitLab CI.');
|
|
92
|
+
}
|
|
93
|
+
console.log(` [GitLab] Fetching target branch: origin/${targetBranch}`);
|
|
94
|
+
await git.fetch(['origin', `${targetBranch}:refs/remotes/origin/${targetBranch}`, '--depth=1']);
|
|
95
|
+
const diff = await git.diff([`origin/${targetBranch}...origin/${sourceBranch}`, '-U200']);
|
|
96
|
+
const diffSummary = await git.diffSummary([`origin/${targetBranch}...origin/${sourceBranch}`]);
|
|
97
|
+
const changedFiles = diffSummary.files.map(f => f.file);
|
|
98
|
+
return { diff: diff || '', changedFiles };
|
|
99
|
+
}
|
|
100
|
+
if (!refName) {
|
|
101
|
+
throw new Error('GitLab CI: CI_COMMIT_REF_NAME environment variable is not set. ' +
|
|
102
|
+
'This should be automatically provided by GitLab CI.');
|
|
103
|
+
}
|
|
104
|
+
// Scenario 3: Default Branch Push
|
|
105
|
+
if (refName === defaultBranch) {
|
|
106
|
+
console.log(` [GitLab] Push to default branch (${defaultBranch}), using HEAD~1...HEAD`);
|
|
107
|
+
const diff = await git.diff(['HEAD~1...HEAD', '-U200']);
|
|
108
|
+
const diffSummary = await git.diffSummary(['HEAD~1...HEAD']);
|
|
109
|
+
const changedFiles = diffSummary.files.map(f => f.file);
|
|
110
|
+
return { diff: diff || '', changedFiles };
|
|
111
|
+
}
|
|
112
|
+
// Scenario 2: Feature Branch Push
|
|
113
|
+
console.log(` [GitLab] Feature branch push, fetching default branch: origin/${defaultBranch}`);
|
|
114
|
+
await git.fetch(['origin', `${defaultBranch}:refs/remotes/origin/${defaultBranch}`, '--depth=1']);
|
|
115
|
+
const diff = await git.diff([`origin/${defaultBranch}...origin/${refName}`, '-U200']);
|
|
116
|
+
const diffSummary = await git.diffSummary([`origin/${defaultBranch}...origin/${refName}`]);
|
|
117
|
+
const changedFiles = diffSummary.files.map(f => f.file);
|
|
118
|
+
return { diff: diff || '', changedFiles };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Gets repository name for GitLab CI
|
|
122
|
+
*/
|
|
123
|
+
async function getRepoName() {
|
|
124
|
+
const projectUrl = process.env.CI_PROJECT_URL;
|
|
125
|
+
if (!projectUrl) {
|
|
126
|
+
throw new Error('GitLab CI: CI_PROJECT_URL environment variable is not set. ' +
|
|
127
|
+
'This should be automatically provided by GitLab CI.');
|
|
128
|
+
}
|
|
129
|
+
return `${projectUrl}.git`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Gets branch name for GitLab CI
|
|
133
|
+
*/
|
|
134
|
+
async function getBranchName() {
|
|
135
|
+
const refName = process.env.CI_COMMIT_REF_NAME;
|
|
136
|
+
if (!refName) {
|
|
137
|
+
throw new Error('GitLab CI: CI_COMMIT_REF_NAME environment variable is not set. ' +
|
|
138
|
+
'This should be automatically provided by GitLab CI.');
|
|
139
|
+
}
|
|
140
|
+
return refName;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Detects GitLab context (MR, branch, or commit)
|
|
144
|
+
*/
|
|
145
|
+
function detectContext() {
|
|
146
|
+
// 1. Check for MR context
|
|
147
|
+
const mrIid = process.env.CI_MERGE_REQUEST_IID;
|
|
148
|
+
if (mrIid) {
|
|
149
|
+
const targetBranch = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
|
|
150
|
+
const sourceBranch = process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME;
|
|
151
|
+
if (targetBranch && sourceBranch) {
|
|
152
|
+
return {
|
|
153
|
+
type: 'mr',
|
|
154
|
+
mrNumber: mrIid,
|
|
155
|
+
sourceBranch,
|
|
156
|
+
targetBranch
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// 2. Check for branch context
|
|
161
|
+
if (process.env.CI_COMMIT_REF_NAME) {
|
|
162
|
+
return {
|
|
163
|
+
type: 'branch',
|
|
164
|
+
branchName: process.env.CI_COMMIT_REF_NAME
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// 3. Check for commit context
|
|
168
|
+
if (process.env.CI_COMMIT_SHA) {
|
|
169
|
+
return {
|
|
170
|
+
type: 'commit',
|
|
171
|
+
commitSha: process.env.CI_COMMIT_SHA
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// 4. Fallback to local (shouldn't happen in GitLab CI, but TypeScript needs it)
|
|
175
|
+
return { type: 'local' };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Gets commit SHA from context
|
|
179
|
+
*/
|
|
180
|
+
function getCommitSha(context) {
|
|
181
|
+
if (context.type === 'commit') {
|
|
182
|
+
return context.commitSha;
|
|
183
|
+
}
|
|
184
|
+
if (context.type === 'branch' || context.type === 'mr') {
|
|
185
|
+
return process.env.CI_COMMIT_SHA;
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Gets commit author for GitLab CI
|
|
191
|
+
* Uses CI_COMMIT_AUTHOR environment variable (most reliable)
|
|
192
|
+
*/
|
|
193
|
+
async function getCommitAuthor() {
|
|
194
|
+
const commitAuthor = process.env.CI_COMMIT_AUTHOR;
|
|
195
|
+
if (!commitAuthor) {
|
|
196
|
+
throw new Error('GitLab CI: CI_COMMIT_AUTHOR environment variable is not set. ' +
|
|
197
|
+
'This should be automatically provided by GitLab CI.');
|
|
198
|
+
}
|
|
199
|
+
// Parse "name <email>" format
|
|
200
|
+
const match = commitAuthor.match(/^(.+?)\s*<(.+?)>$/);
|
|
201
|
+
if (!match) {
|
|
202
|
+
throw new Error(`GitLab CI: CI_COMMIT_AUTHOR format is invalid. ` +
|
|
203
|
+
`Expected format: "name <email>", got: "${commitAuthor}". ` +
|
|
204
|
+
`This should be automatically provided by GitLab CI in the correct format.`);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
name: match[1].trim(),
|
|
208
|
+
email: match[2].trim()
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Gets MR title for GitLab CI
|
|
213
|
+
*/
|
|
214
|
+
function getMRTitle(context) {
|
|
215
|
+
if (context.type !== 'mr') {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
// GitLab CI provides MR title as env var
|
|
219
|
+
return process.env.CI_MERGE_REQUEST_TITLE;
|
|
220
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Local Environment - Complete Isolation
|
|
4
|
+
*
|
|
5
|
+
* All Local-specific logic is contained in this file.
|
|
6
|
+
* No dependencies on other environment implementations.
|
|
7
|
+
*
|
|
8
|
+
* Exports a single function: getLocalContext() that returns:
|
|
9
|
+
* - diff: GitDiffResult
|
|
10
|
+
* - repoName: string
|
|
11
|
+
* - branchName: string
|
|
12
|
+
* - commitAuthor: { name: string; email: string }
|
|
13
|
+
*/
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.getLocalContext = getLocalContext;
|
|
19
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
20
|
+
const diff_1 = require("./diff");
|
|
21
|
+
/**
|
|
22
|
+
* Gets all Local context in one call - completely isolated from other environments.
|
|
23
|
+
*/
|
|
24
|
+
async function getLocalContext(repoRoot, commitSha) {
|
|
25
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
26
|
+
// Check if we're in a git repo
|
|
27
|
+
const isRepo = await git.checkIsRepo();
|
|
28
|
+
if (!isRepo) {
|
|
29
|
+
throw new Error('Not a git repository. Threadline requires a git repository.');
|
|
30
|
+
}
|
|
31
|
+
// Get all Local context
|
|
32
|
+
const diff = commitSha ? await getCommitDiff(repoRoot, commitSha) : await getDiff(repoRoot);
|
|
33
|
+
const repoName = await getRepoName(repoRoot);
|
|
34
|
+
const branchName = await getBranchName(repoRoot);
|
|
35
|
+
const context = commitSha ? { type: 'commit', commitSha } : { type: 'local' };
|
|
36
|
+
// Get commit author (fails loudly if unavailable)
|
|
37
|
+
const commitAuthor = commitSha
|
|
38
|
+
? await getCommitAuthorFromGit(repoRoot, commitSha)
|
|
39
|
+
: await getCommitAuthorFromConfig(repoRoot);
|
|
40
|
+
// Get commit message if we have a SHA
|
|
41
|
+
let commitMessage;
|
|
42
|
+
if (commitSha) {
|
|
43
|
+
const message = await (0, diff_1.getCommitMessage)(repoRoot, commitSha);
|
|
44
|
+
if (message) {
|
|
45
|
+
commitMessage = message;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
diff,
|
|
50
|
+
repoName,
|
|
51
|
+
branchName,
|
|
52
|
+
commitSha,
|
|
53
|
+
commitMessage,
|
|
54
|
+
commitAuthor,
|
|
55
|
+
prTitle: undefined, // Not applicable for local
|
|
56
|
+
context
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get diff for local development environment
|
|
61
|
+
*
|
|
62
|
+
* For local development, we check staged changes first, then unstaged changes.
|
|
63
|
+
* This allows developers to review what they've staged before committing,
|
|
64
|
+
* or review unstaged changes if nothing is staged.
|
|
65
|
+
*/
|
|
66
|
+
async function getDiff(repoRoot) {
|
|
67
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
68
|
+
// Get git status to determine what changes exist
|
|
69
|
+
const status = await git.status();
|
|
70
|
+
let diff;
|
|
71
|
+
let changedFiles;
|
|
72
|
+
// Priority 1: Use staged changes if available
|
|
73
|
+
if (status.staged.length > 0) {
|
|
74
|
+
diff = await git.diff(['--cached', '-U200']);
|
|
75
|
+
// status.staged is an array of strings (file paths)
|
|
76
|
+
changedFiles = status.staged;
|
|
77
|
+
}
|
|
78
|
+
// Priority 2: Use unstaged changes if no staged changes
|
|
79
|
+
else if (status.files.length > 0) {
|
|
80
|
+
diff = await git.diff(['-U200']);
|
|
81
|
+
changedFiles = status.files
|
|
82
|
+
.filter(f => f.working_dir !== ' ' || f.index !== ' ')
|
|
83
|
+
.map(f => f.path);
|
|
84
|
+
}
|
|
85
|
+
// No changes at all
|
|
86
|
+
else {
|
|
87
|
+
return {
|
|
88
|
+
diff: '',
|
|
89
|
+
changedFiles: []
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
diff: diff || '',
|
|
94
|
+
changedFiles
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get diff for a specific commit (when --commit flag is used)
|
|
99
|
+
*/
|
|
100
|
+
async function getCommitDiff(repoRoot, commitSha) {
|
|
101
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
102
|
+
// Get diff using git show
|
|
103
|
+
const diff = await git.show([commitSha, '--format=', '--no-color', '-U200']);
|
|
104
|
+
// Get changed files using git show --name-only
|
|
105
|
+
const commitFiles = await git.show([commitSha, '--name-only', '--format=', '--pretty=format:']);
|
|
106
|
+
const changedFiles = commitFiles
|
|
107
|
+
.split('\n')
|
|
108
|
+
.filter(line => line.trim().length > 0)
|
|
109
|
+
.map(line => line.trim());
|
|
110
|
+
return {
|
|
111
|
+
diff: diff || '',
|
|
112
|
+
changedFiles
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Gets repository name for local environment
|
|
117
|
+
*/
|
|
118
|
+
async function getRepoName(repoRoot) {
|
|
119
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
120
|
+
try {
|
|
121
|
+
const remotes = await git.getRemotes(true);
|
|
122
|
+
const origin = remotes.find(r => r.name === 'origin');
|
|
123
|
+
if (!origin || !origin.refs?.fetch) {
|
|
124
|
+
throw new Error('No origin remote found. Please set up a git remote named "origin".');
|
|
125
|
+
}
|
|
126
|
+
return origin.refs.fetch;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
130
|
+
throw new Error(`Failed to get repository name from git remote: ${errorMessage}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Gets branch name for local environment
|
|
135
|
+
*/
|
|
136
|
+
async function getBranchName(repoRoot) {
|
|
137
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
138
|
+
try {
|
|
139
|
+
const branchSummary = await git.branchLocal();
|
|
140
|
+
const currentBranch = branchSummary.current;
|
|
141
|
+
if (!currentBranch) {
|
|
142
|
+
throw new Error('Could not determine current branch. Are you in a git repository?');
|
|
143
|
+
}
|
|
144
|
+
return currentBranch;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
148
|
+
throw new Error(`Failed to get branch name: ${errorMessage}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Gets commit author from git config (for uncommitted changes)
|
|
153
|
+
* This represents who is currently working on the changes and will commit them.
|
|
154
|
+
*
|
|
155
|
+
* No fallbacks - if git config is not set or fails, throws an error.
|
|
156
|
+
*/
|
|
157
|
+
async function getCommitAuthorFromConfig(repoRoot) {
|
|
158
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
159
|
+
try {
|
|
160
|
+
const name = await git.getConfig('user.name');
|
|
161
|
+
const email = await git.getConfig('user.email');
|
|
162
|
+
if (!name.value || !email.value) {
|
|
163
|
+
throw new Error('Git config user.name or user.email is not set. ' +
|
|
164
|
+
'Run: git config user.name "Your Name" && git config user.email "your.email@example.com"');
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
name: name.value.trim(),
|
|
168
|
+
email: email.value.trim()
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
173
|
+
throw new Error(`Failed to get git config user: ${errorMessage}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Gets commit author from git log (for specific commits)
|
|
178
|
+
*/
|
|
179
|
+
async function getCommitAuthorFromGit(repoRoot, commitSha) {
|
|
180
|
+
const gitAuthor = await (0, diff_1.getCommitAuthor)(repoRoot, commitSha);
|
|
181
|
+
if (!gitAuthor || !gitAuthor.email) {
|
|
182
|
+
throw new Error(`Local: Failed to get commit author from git log for commit ${commitSha}. ` +
|
|
183
|
+
'This should be available in your local git repository.');
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
name: gitAuthor.name,
|
|
187
|
+
email: gitAuthor.email
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Vercel Environment - Complete Isolation
|
|
4
|
+
*
|
|
5
|
+
* All Vercel-specific logic is contained in this file.
|
|
6
|
+
* No dependencies on other environment implementations.
|
|
7
|
+
*
|
|
8
|
+
* Exports a single function: getVercelContext() that returns:
|
|
9
|
+
* - diff: GitDiffResult
|
|
10
|
+
* - repoName: string
|
|
11
|
+
* - branchName: string
|
|
12
|
+
* - commitAuthor: { name: string; email: string }
|
|
13
|
+
*/
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.getVercelContext = getVercelContext;
|
|
19
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
20
|
+
const diff_1 = require("./diff");
|
|
21
|
+
/**
|
|
22
|
+
* Gets all Vercel context in one call - completely isolated from other environments.
|
|
23
|
+
*/
|
|
24
|
+
async function getVercelContext(repoRoot) {
|
|
25
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
26
|
+
// Check if we're in a git repo
|
|
27
|
+
const isRepo = await git.checkIsRepo();
|
|
28
|
+
if (!isRepo) {
|
|
29
|
+
throw new Error('Not a git repository. Threadline requires a git repository.');
|
|
30
|
+
}
|
|
31
|
+
// Get all Vercel context
|
|
32
|
+
const diff = await getDiff(repoRoot);
|
|
33
|
+
const repoName = await getRepoName();
|
|
34
|
+
const branchName = await getBranchName();
|
|
35
|
+
const commitSha = getCommitSha();
|
|
36
|
+
const context = { type: 'commit', commitSha };
|
|
37
|
+
// Get commit author (fails loudly if unavailable)
|
|
38
|
+
const commitAuthor = await getCommitAuthorForVercel(repoRoot, commitSha);
|
|
39
|
+
// Get commit message
|
|
40
|
+
let commitMessage;
|
|
41
|
+
const message = await (0, diff_1.getCommitMessage)(repoRoot, commitSha);
|
|
42
|
+
if (message) {
|
|
43
|
+
commitMessage = message;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
diff,
|
|
47
|
+
repoName,
|
|
48
|
+
branchName,
|
|
49
|
+
commitSha,
|
|
50
|
+
commitMessage,
|
|
51
|
+
commitAuthor,
|
|
52
|
+
context
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get diff for Vercel CI environment
|
|
57
|
+
*
|
|
58
|
+
* Vercel provides VERCEL_GIT_COMMIT_SHA which contains the commit being deployed.
|
|
59
|
+
* This function gets the diff for that specific commit using git show.
|
|
60
|
+
*/
|
|
61
|
+
async function getDiff(repoRoot) {
|
|
62
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
63
|
+
// Get commit SHA from Vercel environment variable
|
|
64
|
+
const commitSha = process.env.VERCEL_GIT_COMMIT_SHA;
|
|
65
|
+
if (!commitSha) {
|
|
66
|
+
throw new Error('VERCEL_GIT_COMMIT_SHA environment variable is not set. ' +
|
|
67
|
+
'This should be automatically provided by Vercel CI.');
|
|
68
|
+
}
|
|
69
|
+
// Get diff using git show - this is the ONLY way we get diff in Vercel
|
|
70
|
+
const diff = await git.show([commitSha, '--format=', '--no-color', '-U200']);
|
|
71
|
+
// Get changed files using git show --name-only
|
|
72
|
+
const commitFiles = await git.show([commitSha, '--name-only', '--format=', '--pretty=format:']);
|
|
73
|
+
const changedFiles = commitFiles
|
|
74
|
+
.split('\n')
|
|
75
|
+
.filter(line => line.trim().length > 0)
|
|
76
|
+
.map(line => line.trim());
|
|
77
|
+
return {
|
|
78
|
+
diff: diff || '',
|
|
79
|
+
changedFiles
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Gets repository name for Vercel
|
|
84
|
+
*/
|
|
85
|
+
async function getRepoName() {
|
|
86
|
+
const owner = process.env.VERCEL_GIT_REPO_OWNER;
|
|
87
|
+
const slug = process.env.VERCEL_GIT_REPO_SLUG;
|
|
88
|
+
if (!owner || !slug) {
|
|
89
|
+
throw new Error('Vercel: VERCEL_GIT_REPO_OWNER or VERCEL_GIT_REPO_SLUG environment variable is not set. ' +
|
|
90
|
+
'This should be automatically provided by Vercel CI.');
|
|
91
|
+
}
|
|
92
|
+
return `https://github.com/${owner}/${slug}.git`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Gets branch name for Vercel
|
|
96
|
+
*/
|
|
97
|
+
async function getBranchName() {
|
|
98
|
+
const branchName = process.env.VERCEL_GIT_COMMIT_REF;
|
|
99
|
+
if (!branchName) {
|
|
100
|
+
throw new Error('Vercel: VERCEL_GIT_COMMIT_REF environment variable is not set. ' +
|
|
101
|
+
'This should be automatically provided by Vercel CI.');
|
|
102
|
+
}
|
|
103
|
+
return branchName;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Gets commit SHA for Vercel
|
|
107
|
+
*/
|
|
108
|
+
function getCommitSha() {
|
|
109
|
+
const commitSha = process.env.VERCEL_GIT_COMMIT_SHA;
|
|
110
|
+
if (!commitSha) {
|
|
111
|
+
throw new Error('Vercel: VERCEL_GIT_COMMIT_SHA environment variable is not set. ' +
|
|
112
|
+
'This should be automatically provided by Vercel CI.');
|
|
113
|
+
}
|
|
114
|
+
return commitSha;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Gets commit author for Vercel
|
|
118
|
+
* Uses VERCEL_GIT_COMMIT_AUTHOR_NAME for name, git log for email
|
|
119
|
+
*/
|
|
120
|
+
async function getCommitAuthorForVercel(repoRoot, commitSha) {
|
|
121
|
+
const authorName = process.env.VERCEL_GIT_COMMIT_AUTHOR_NAME;
|
|
122
|
+
if (!authorName) {
|
|
123
|
+
throw new Error('Vercel: VERCEL_GIT_COMMIT_AUTHOR_NAME environment variable is not set. ' +
|
|
124
|
+
'This should be automatically provided by Vercel.');
|
|
125
|
+
}
|
|
126
|
+
// Get email from git log - fail loudly if this doesn't work
|
|
127
|
+
const gitAuthor = await (0, diff_1.getCommitAuthor)(repoRoot, commitSha);
|
|
128
|
+
if (!gitAuthor || !gitAuthor.email) {
|
|
129
|
+
throw new Error(`Vercel: Failed to get commit author email from git log for commit ${commitSha}. ` +
|
|
130
|
+
`This should be available in Vercel's build environment.`);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
name: authorName.trim(),
|
|
134
|
+
email: gitAuthor.email.trim()
|
|
135
|
+
};
|
|
136
|
+
}
|
package/dist/utils/metadata.js
CHANGED
|
@@ -67,23 +67,18 @@ async function collectMetadata(context, environment, repoRoot) {
|
|
|
67
67
|
if (message) {
|
|
68
68
|
metadata.commitMessage = message;
|
|
69
69
|
}
|
|
70
|
-
// Get commit author - environment-specific approach
|
|
70
|
+
// Get commit author - environment-specific approach (fails loudly if unavailable)
|
|
71
71
|
const author = await getCommitAuthorForEnvironment(environment, repoRoot, metadata.commitSha);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
metadata.commitAuthorEmail = author.email;
|
|
75
|
-
}
|
|
72
|
+
metadata.commitAuthorName = author.name;
|
|
73
|
+
metadata.commitAuthorEmail = author.email;
|
|
76
74
|
}
|
|
77
75
|
else {
|
|
78
76
|
// For local environment without explicit commit SHA:
|
|
79
77
|
// Use git config (who will commit staged/unstaged changes)
|
|
80
78
|
// No fallbacks - if git config fails, the error propagates and fails the check
|
|
81
|
-
console.log('[DEBUG] Local environment - calling getGitConfigUser()');
|
|
82
79
|
const author = await getGitConfigUser(repoRoot);
|
|
83
|
-
console.log(`[DEBUG] getGitConfigUser returned: ${JSON.stringify(author)}`);
|
|
84
80
|
metadata.commitAuthorName = author.name;
|
|
85
81
|
metadata.commitAuthorEmail = author.email;
|
|
86
|
-
console.log(`[DEBUG] metadata after assignment: commitAuthorName=${metadata.commitAuthorName}, commitAuthorEmail=${metadata.commitAuthorEmail}`);
|
|
87
82
|
}
|
|
88
83
|
// Collect PR/MR title (environment-specific)
|
|
89
84
|
metadata.prTitle = getPRTitle(context, environment);
|
|
@@ -127,46 +122,63 @@ function getCommitSha(context, environment) {
|
|
|
127
122
|
/**
|
|
128
123
|
* Gets commit author information using environment-specific methods.
|
|
129
124
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
125
|
+
* Each environment has a single, isolated strategy:
|
|
126
|
+
* - GitHub: Reads from GITHUB_EVENT_PATH JSON file (fails loudly if unavailable)
|
|
127
|
+
* - GitLab: Uses CI_COMMIT_AUTHOR environment variable (fails loudly if unavailable)
|
|
128
|
+
* - Vercel: Uses VERCEL_GIT_COMMIT_AUTHOR_NAME + git log (fails loudly if unavailable)
|
|
129
|
+
* - Local: Uses git config (handled separately in collectMetadata, fails loudly if unavailable)
|
|
130
|
+
*
|
|
131
|
+
* No fallbacks - each environment is completely isolated.
|
|
134
132
|
*/
|
|
135
133
|
async function getCommitAuthorForEnvironment(environment, repoRoot, commitSha) {
|
|
136
134
|
if (environment === 'github') {
|
|
137
135
|
// GitHub: Read from GITHUB_EVENT_PATH JSON file
|
|
138
136
|
// This is more reliable than git commands, especially in shallow clones
|
|
139
137
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
140
|
-
if (eventPath
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
138
|
+
if (!eventPath) {
|
|
139
|
+
throw new Error('GitHub Actions: GITHUB_EVENT_PATH environment variable is not set. ' +
|
|
140
|
+
'This should be automatically provided by GitHub Actions.');
|
|
141
|
+
}
|
|
142
|
+
if (!fs.existsSync(eventPath)) {
|
|
143
|
+
throw new Error(`GitHub Actions: GITHUB_EVENT_PATH file does not exist: ${eventPath}. ` +
|
|
144
|
+
'This should be automatically provided by GitHub Actions.');
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const eventData = JSON.parse(fs.readFileSync(eventPath, 'utf-8'));
|
|
148
|
+
// For push events, use head_commit.author
|
|
149
|
+
if (eventData.head_commit?.author) {
|
|
150
|
+
return {
|
|
151
|
+
name: eventData.head_commit.author.name,
|
|
152
|
+
email: eventData.head_commit.author.email
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// For PR events, use commits[0].author (first commit in the PR)
|
|
156
|
+
if (eventData.commits && eventData.commits.length > 0 && eventData.commits[0].author) {
|
|
157
|
+
return {
|
|
158
|
+
name: eventData.commits[0].author.name,
|
|
159
|
+
email: eventData.commits[0].author.email
|
|
160
|
+
};
|
|
164
161
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
162
|
+
// Fallback to pull_request.head.commit.author for PR events
|
|
163
|
+
if (eventData.pull_request?.head?.commit?.author) {
|
|
164
|
+
return {
|
|
165
|
+
name: eventData.pull_request.head.commit.author.name,
|
|
166
|
+
email: eventData.pull_request.head.commit.author.email
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// If we get here, the event JSON doesn't contain author info
|
|
170
|
+
throw new Error(`GitHub Actions: GITHUB_EVENT_PATH JSON does not contain commit author information. ` +
|
|
171
|
+
`Event type: ${eventData.action || 'unknown'}. ` +
|
|
172
|
+
`This should be automatically provided by GitHub Actions.`);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
// If JSON parsing fails, fail loudly
|
|
176
|
+
if (error instanceof Error && error.message.includes('GitHub Actions:')) {
|
|
177
|
+
throw error; // Re-throw our own errors
|
|
169
178
|
}
|
|
179
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
180
|
+
throw new Error(`GitHub Actions: Failed to read or parse GITHUB_EVENT_PATH JSON: ${errorMessage}. ` +
|
|
181
|
+
'This should be automatically provided by GitHub Actions.');
|
|
170
182
|
}
|
|
171
183
|
}
|
|
172
184
|
if (environment === 'gitlab') {
|
|
@@ -190,8 +202,43 @@ async function getCommitAuthorForEnvironment(environment, repoRoot, commitSha) {
|
|
|
190
202
|
email: match[2].trim()
|
|
191
203
|
};
|
|
192
204
|
}
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
if (environment === 'vercel') {
|
|
206
|
+
// Vercel: Use VERCEL_GIT_COMMIT_AUTHOR_NAME for name, git log for email
|
|
207
|
+
// Vercel provides author name but not email in environment variables
|
|
208
|
+
// git log works reliably in Vercel's build environment
|
|
209
|
+
const authorName = process.env.VERCEL_GIT_COMMIT_AUTHOR_NAME;
|
|
210
|
+
if (!authorName) {
|
|
211
|
+
throw new Error('Vercel: VERCEL_GIT_COMMIT_AUTHOR_NAME environment variable is not set. ' +
|
|
212
|
+
'This should be automatically provided by Vercel.');
|
|
213
|
+
}
|
|
214
|
+
// Get email from git log - fail loudly if this doesn't work
|
|
215
|
+
const gitAuthor = await (0, diff_1.getCommitAuthor)(repoRoot, commitSha);
|
|
216
|
+
if (!gitAuthor || !gitAuthor.email) {
|
|
217
|
+
throw new Error(`Vercel: Failed to get commit author email from git log for commit ${commitSha}. ` +
|
|
218
|
+
`This should be available in Vercel's build environment.`);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
name: authorName.trim(),
|
|
222
|
+
email: gitAuthor.email.trim()
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// Local environment should not reach here - it's handled separately in collectMetadata
|
|
226
|
+
// when commitSha is undefined. If we get here with 'local', it means commitSha was set
|
|
227
|
+
// (e.g., --commit flag), so we can use git log.
|
|
228
|
+
if (environment === 'local') {
|
|
229
|
+
const gitAuthor = await (0, diff_1.getCommitAuthor)(repoRoot, commitSha);
|
|
230
|
+
if (!gitAuthor || !gitAuthor.email) {
|
|
231
|
+
throw new Error(`Local: Failed to get commit author from git log for commit ${commitSha}. ` +
|
|
232
|
+
'This should be available in your local git repository.');
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
name: gitAuthor.name,
|
|
236
|
+
email: gitAuthor.email
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// Unknown environment - this should never happen due to TypeScript exhaustiveness
|
|
240
|
+
const _exhaustive = environment;
|
|
241
|
+
throw new Error(`Unknown environment: ${_exhaustive}`);
|
|
195
242
|
}
|
|
196
243
|
/**
|
|
197
244
|
* Gets git user info from git config (for local uncommitted changes).
|