threadlines 0.1.33 → 0.1.35

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.
@@ -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 context_1 = require("../utils/context");
46
- const metadata_1 = require("../utils/metadata");
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 unified approach
142
- const gitContext = await (0, context_2.getGitContextForEnvironment)(environment, repoRoot);
143
- repoName = gitContext.repoName;
144
- branchName = gitContext.branchName;
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 unified approach
151
- const gitContext = await (0, context_2.getGitContextForEnvironment)(environment, repoRoot);
152
- repoName = gitContext.repoName;
153
- branchName = gitContext.branchName;
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 unified git context collection
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
- // Use unified git context collection (diff + repo + branch)
165
- const gitContext = await (0, context_2.getGitContextForEnvironment)(environment, repoRoot);
166
- gitDiff = gitContext.diff;
167
- repoName = gitContext.repoName;
168
- branchName = gitContext.branchName;
169
- // Create context for metadata collection
170
- if (environment === 'vercel') {
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 === 'github' || environment === 'gitlab') {
174
- // GitHub/GitLab: Detect context for metadata (but diff already obtained)
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
- // Local: Use local context
179
- context = { type: 'local' };
264
+ envContext = await (0, local_1.getLocalContext)(repoRoot);
180
265
  }
181
- }
182
- // 3. Collect metadata (commit SHA, commit message, PR title)
183
- const metadata = await (0, metadata_1.collectMetadata)(context, environment, repoRoot);
184
- // Debug: Log collected metadata
185
- console.log(`[DEBUG] Metadata after collectMetadata: ${JSON.stringify(metadata)}`);
186
- if (metadata.commitAuthorName || metadata.commitAuthorEmail) {
187
- console.log(chalk_1.default.gray(` Author: ${metadata.commitAuthorName} <${metadata.commitAuthorEmail}>`));
188
- }
189
- else {
190
- console.log(`[DEBUG] No author info - commitAuthorName=${metadata.commitAuthorName}, commitAuthorEmail=${metadata.commitAuthorEmail}`);
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 reviewRequest = {
323
+ const response = await client.review({
237
324
  threadlines: threadlinesWithContext,
238
325
  diff: gitDiff.diff,
239
326
  files: gitDiff.changedFiles,
@@ -247,9 +334,7 @@ async function checkCommand(options) {
247
334
  commitAuthorEmail: metadata.commitAuthorEmail,
248
335
  prTitle: metadata.prTitle,
249
336
  environment: environment
250
- };
251
- console.log(`[DEBUG] Sending to API - commitAuthorName: ${reviewRequest.commitAuthorName}, commitAuthorEmail: ${reviewRequest.commitAuthorEmail}`);
252
- const response = await client.review(reviewRequest);
337
+ });
253
338
  // 7. Display results (with filtering if --full not specified)
254
339
  displayResults(response, options.full || false);
255
340
  // 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
+ }
@@ -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
- if (author) {
73
- metadata.commitAuthorName = author.name;
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
- * For GitHub: Reads from GITHUB_EVENT_PATH JSON file (most reliable)
131
- * For GitLab: Uses CI_COMMIT_AUTHOR environment variable (most reliable)
132
- * For Local: Uses git config (for uncommitted changes, represents who will commit)
133
- * For other environments: Uses git log command
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 && fs.existsSync(eventPath)) {
141
- try {
142
- const eventData = JSON.parse(fs.readFileSync(eventPath, 'utf-8'));
143
- // For push events, use head_commit.author
144
- if (eventData.head_commit?.author) {
145
- return {
146
- name: eventData.head_commit.author.name,
147
- email: eventData.head_commit.author.email
148
- };
149
- }
150
- // For PR events, use commits[0].author (first commit in the PR)
151
- if (eventData.commits && eventData.commits.length > 0 && eventData.commits[0].author) {
152
- return {
153
- name: eventData.commits[0].author.name,
154
- email: eventData.commits[0].author.email
155
- };
156
- }
157
- // Fallback to pull_request.head.commit.author for PR events
158
- if (eventData.pull_request?.head?.commit?.author) {
159
- return {
160
- name: eventData.pull_request.head.commit.author.name,
161
- email: eventData.pull_request.head.commit.author.email
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
+ };
164
154
  }
165
- catch (error) {
166
- // If JSON parsing fails, fall through to git command
167
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
168
- console.warn(`Warning: Failed to read GitHub event JSON: ${errorMessage}`);
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
+ };
161
+ }
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') {
@@ -174,33 +186,59 @@ async function getCommitAuthorForEnvironment(environment, repoRoot, commitSha) {
174
186
  // Format: "name <email>" (e.g., "ngrootscholten <niels.grootscholten@gmail.com>")
175
187
  // This is more reliable than git commands, especially in shallow clones
176
188
  const commitAuthor = process.env.CI_COMMIT_AUTHOR;
177
- if (commitAuthor) {
178
- // Parse "name <email>" format
179
- const match = commitAuthor.match(/^(.+?)\s*<(.+?)>$/);
180
- if (match) {
181
- return {
182
- name: match[1].trim(),
183
- email: match[2].trim()
184
- };
185
- }
186
- // If format doesn't match expected pattern, try to extract anyway
187
- // Some GitLab versions might format differently
188
- const parts = commitAuthor.trim().split(/\s+/);
189
- if (parts.length >= 2) {
190
- // Assume last part is email if it contains @
191
- const emailIndex = parts.findIndex(p => p.includes('@'));
192
- if (emailIndex >= 0) {
193
- const email = parts[emailIndex].replace(/[<>]/g, '').trim();
194
- const name = parts.slice(0, emailIndex).join(' ').trim();
195
- if (name && email) {
196
- return { name, email };
197
- }
198
- }
199
- }
189
+ if (!commitAuthor) {
190
+ throw new Error('GitLab CI: CI_COMMIT_AUTHOR environment variable is not set. ' +
191
+ 'This should be automatically provided by GitLab CI.');
192
+ }
193
+ // Parse "name <email>" format
194
+ const match = commitAuthor.match(/^(.+?)\s*<(.+?)>$/);
195
+ if (!match) {
196
+ throw new Error(`GitLab CI: CI_COMMIT_AUTHOR format is invalid. ` +
197
+ `Expected format: "name <email>", got: "${commitAuthor}". ` +
198
+ `This should be automatically provided by GitLab CI in the correct format.`);
199
+ }
200
+ return {
201
+ name: match[1].trim(),
202
+ email: match[2].trim()
203
+ };
204
+ }
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.');
200
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
+ };
201
238
  }
202
- // Fallback to git command for all environments (including GitHub/GitLab if env vars unavailable)
203
- return await (0, diff_1.getCommitAuthor)(repoRoot, commitSha);
239
+ // Unknown environment - this should never happen due to TypeScript exhaustiveness
240
+ const _exhaustive = environment;
241
+ throw new Error(`Unknown environment: ${_exhaustive}`);
204
242
  }
205
243
  /**
206
244
  * Gets git user info from git config (for local uncommitted changes).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadlines",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "Threadline CLI - AI-powered linter based on your natural language documentation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {