korekt-cli 0.8.4 โ 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/formatter.js +2 -0
- package/src/git-logic.js +5 -0
- package/src/git-logic.test.js +181 -0
- package/src/index.js +4 -55
- package/src/index.test.js +109 -1
- package/src/utils.js +84 -0
package/package.json
CHANGED
package/src/formatter.js
CHANGED
package/src/git-logic.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import { detectCIProvider, getPrUrl } from './utils.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Truncate content to a maximum number of lines using "head and tail".
|
|
@@ -244,6 +245,8 @@ export async function runUncommittedReview(mode = 'unstaged') {
|
|
|
244
245
|
changed_files: changedFiles,
|
|
245
246
|
source_branch: branchName,
|
|
246
247
|
changed_lines: calculateChangedLines(changedFiles),
|
|
248
|
+
is_ci: detectCIProvider() !== null,
|
|
249
|
+
pr_url: null, // Uncommitted changes are never part of a PR
|
|
247
250
|
};
|
|
248
251
|
} catch (error) {
|
|
249
252
|
console.error(chalk.red('Failed to analyze uncommitted changes:'), error.message);
|
|
@@ -524,6 +527,8 @@ export async function runLocalReview(targetBranch = null, ignorePatterns = null)
|
|
|
524
527
|
author_name,
|
|
525
528
|
contributors,
|
|
526
529
|
changed_lines: calculateChangedLines(changedFiles),
|
|
530
|
+
is_ci: detectCIProvider() !== null,
|
|
531
|
+
pr_url: getPrUrl(),
|
|
527
532
|
};
|
|
528
533
|
} catch (error) {
|
|
529
534
|
console.error(chalk.red('Failed to run local review analysis:'), error.message);
|
package/src/git-logic.test.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getContributors,
|
|
10
10
|
} from './git-logic.js';
|
|
11
11
|
import { execa } from 'execa';
|
|
12
|
+
import { detectCIProvider } from './utils.js';
|
|
12
13
|
|
|
13
14
|
describe('parseNameStatus', () => {
|
|
14
15
|
it('should correctly parse M, A, and D statuses', () => {
|
|
@@ -830,3 +831,183 @@ describe('changed_lines calculation', () => {
|
|
|
830
831
|
expect(result.changed_lines).toBe(0);
|
|
831
832
|
});
|
|
832
833
|
});
|
|
834
|
+
|
|
835
|
+
describe('is_ci flag in payload', () => {
|
|
836
|
+
beforeEach(() => {
|
|
837
|
+
vi.mock('execa');
|
|
838
|
+
vi.mock('./utils.js', () => ({
|
|
839
|
+
detectCIProvider: vi.fn(),
|
|
840
|
+
getPrUrl: vi.fn().mockReturnValue(null),
|
|
841
|
+
}));
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
afterEach(() => {
|
|
845
|
+
vi.restoreAllMocks();
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it('should set is_ci to true when detectCIProvider returns a provider', async () => {
|
|
849
|
+
vi.mocked(detectCIProvider).mockReturnValue('github');
|
|
850
|
+
|
|
851
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
852
|
+
const command = [cmd, ...args].join(' ');
|
|
853
|
+
|
|
854
|
+
if (command.includes('remote get-url origin')) {
|
|
855
|
+
return { stdout: 'https://github.com/user/repo.git' };
|
|
856
|
+
}
|
|
857
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) {
|
|
858
|
+
return { stdout: 'feature-branch' };
|
|
859
|
+
}
|
|
860
|
+
if (command.includes('rev-parse --show-toplevel')) {
|
|
861
|
+
return { stdout: '/fake/repo/path' };
|
|
862
|
+
}
|
|
863
|
+
if (command.includes('diff --cached --name-status')) {
|
|
864
|
+
return { stdout: 'M\tfile.js' };
|
|
865
|
+
}
|
|
866
|
+
if (command.includes('diff --cached -U15 -- file.js')) {
|
|
867
|
+
return { stdout: 'diff --git a/file.js b/file.js\n+new line' };
|
|
868
|
+
}
|
|
869
|
+
if (command.includes('show HEAD:file.js')) {
|
|
870
|
+
return { stdout: 'old content' };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
const result = await runUncommittedReview('staged');
|
|
877
|
+
|
|
878
|
+
expect(result).toBeDefined();
|
|
879
|
+
expect(result.is_ci).toBe(true);
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it('should set is_ci to false when detectCIProvider returns null', async () => {
|
|
883
|
+
vi.mocked(detectCIProvider).mockReturnValue(null);
|
|
884
|
+
|
|
885
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
886
|
+
const command = [cmd, ...args].join(' ');
|
|
887
|
+
|
|
888
|
+
if (command.includes('remote get-url origin')) {
|
|
889
|
+
return { stdout: 'https://github.com/user/repo.git' };
|
|
890
|
+
}
|
|
891
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) {
|
|
892
|
+
return { stdout: 'feature-branch' };
|
|
893
|
+
}
|
|
894
|
+
if (command.includes('rev-parse --show-toplevel')) {
|
|
895
|
+
return { stdout: '/fake/repo/path' };
|
|
896
|
+
}
|
|
897
|
+
if (command.includes('diff --cached --name-status')) {
|
|
898
|
+
return { stdout: 'M\tfile.js' };
|
|
899
|
+
}
|
|
900
|
+
if (command.includes('diff --cached -U15 -- file.js')) {
|
|
901
|
+
return { stdout: 'diff --git a/file.js b/file.js\n+new line' };
|
|
902
|
+
}
|
|
903
|
+
if (command.includes('show HEAD:file.js')) {
|
|
904
|
+
return { stdout: 'old content' };
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
const result = await runUncommittedReview('staged');
|
|
911
|
+
|
|
912
|
+
expect(result).toBeDefined();
|
|
913
|
+
expect(result.is_ci).toBe(false);
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it('should include is_ci in runLocalReview payload', async () => {
|
|
917
|
+
vi.mocked(detectCIProvider).mockReturnValue('azure');
|
|
918
|
+
|
|
919
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
920
|
+
const command = [cmd, ...args].join(' ');
|
|
921
|
+
|
|
922
|
+
if (command.includes('remote get-url origin')) {
|
|
923
|
+
return { stdout: 'https://github.com/user/repo.git' };
|
|
924
|
+
}
|
|
925
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) {
|
|
926
|
+
return { stdout: 'feature-branch' };
|
|
927
|
+
}
|
|
928
|
+
if (command.includes('rev-parse --show-toplevel')) {
|
|
929
|
+
return { stdout: '/path/to/repo' };
|
|
930
|
+
}
|
|
931
|
+
if (command.includes('rev-parse --verify main')) {
|
|
932
|
+
return { stdout: 'commit-hash' };
|
|
933
|
+
}
|
|
934
|
+
if (command === 'git fetch origin main') {
|
|
935
|
+
return { stdout: '' };
|
|
936
|
+
}
|
|
937
|
+
if (command.includes('merge-base origin/main HEAD')) {
|
|
938
|
+
return { stdout: 'abc123' };
|
|
939
|
+
}
|
|
940
|
+
if (command.includes('log --no-merges --pretty=%B---EOC---')) {
|
|
941
|
+
return { stdout: 'feat: add feature---EOC---' };
|
|
942
|
+
}
|
|
943
|
+
if (command.includes('log --no-merges --format=%ae|%an')) {
|
|
944
|
+
return { stdout: 'user@example.com|User Name' };
|
|
945
|
+
}
|
|
946
|
+
if (command.includes('diff --name-status')) {
|
|
947
|
+
return { stdout: 'M\tfile.js' };
|
|
948
|
+
}
|
|
949
|
+
if (command.includes('diff -U15')) {
|
|
950
|
+
return { stdout: 'diff content' };
|
|
951
|
+
}
|
|
952
|
+
if (command.includes('show abc123:file.js')) {
|
|
953
|
+
return { stdout: 'original content' };
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return { stdout: '' };
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
const result = await runLocalReview('main');
|
|
960
|
+
|
|
961
|
+
expect(result).toBeDefined();
|
|
962
|
+
expect(result.is_ci).toBe(true);
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
it('should set is_ci to false in runLocalReview when not in CI', async () => {
|
|
966
|
+
vi.mocked(detectCIProvider).mockReturnValue(null);
|
|
967
|
+
|
|
968
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
969
|
+
const command = [cmd, ...args].join(' ');
|
|
970
|
+
|
|
971
|
+
if (command.includes('remote get-url origin')) {
|
|
972
|
+
return { stdout: 'https://github.com/user/repo.git' };
|
|
973
|
+
}
|
|
974
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) {
|
|
975
|
+
return { stdout: 'feature-branch' };
|
|
976
|
+
}
|
|
977
|
+
if (command.includes('rev-parse --show-toplevel')) {
|
|
978
|
+
return { stdout: '/path/to/repo' };
|
|
979
|
+
}
|
|
980
|
+
if (command.includes('rev-parse --verify main')) {
|
|
981
|
+
return { stdout: 'commit-hash' };
|
|
982
|
+
}
|
|
983
|
+
if (command === 'git fetch origin main') {
|
|
984
|
+
return { stdout: '' };
|
|
985
|
+
}
|
|
986
|
+
if (command.includes('merge-base origin/main HEAD')) {
|
|
987
|
+
return { stdout: 'abc123' };
|
|
988
|
+
}
|
|
989
|
+
if (command.includes('log --no-merges --pretty=%B---EOC---')) {
|
|
990
|
+
return { stdout: 'feat: add feature---EOC---' };
|
|
991
|
+
}
|
|
992
|
+
if (command.includes('log --no-merges --format=%ae|%an')) {
|
|
993
|
+
return { stdout: 'user@example.com|User Name' };
|
|
994
|
+
}
|
|
995
|
+
if (command.includes('diff --name-status')) {
|
|
996
|
+
return { stdout: 'M\tfile.js' };
|
|
997
|
+
}
|
|
998
|
+
if (command.includes('diff -U15')) {
|
|
999
|
+
return { stdout: 'diff content' };
|
|
1000
|
+
}
|
|
1001
|
+
if (command.includes('show abc123:file.js')) {
|
|
1002
|
+
return { stdout: 'original content' };
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return { stdout: '' };
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
const result = await runLocalReview('main');
|
|
1009
|
+
|
|
1010
|
+
expect(result).toBeDefined();
|
|
1011
|
+
expect(result.is_ci).toBe(false);
|
|
1012
|
+
});
|
|
1013
|
+
});
|
package/src/index.js
CHANGED
|
@@ -14,6 +14,10 @@ import { tmpdir } from 'os';
|
|
|
14
14
|
import { runLocalReview } from './git-logic.js';
|
|
15
15
|
import { getApiKey, setApiKey, getApiEndpoint, setApiEndpoint } from './config.js';
|
|
16
16
|
import { formatReviewOutput } from './formatter.js';
|
|
17
|
+
import { detectCIProvider, truncateFileData, formatErrorOutput } from './utils.js';
|
|
18
|
+
|
|
19
|
+
// Re-export utilities for backward compatibility
|
|
20
|
+
export { detectCIProvider, truncateFileData, formatErrorOutput, getPrUrl } from './utils.js';
|
|
17
21
|
|
|
18
22
|
const require = createRequire(import.meta.url);
|
|
19
23
|
const { version } = require('../package.json');
|
|
@@ -26,44 +30,6 @@ const { version } = require('../package.json');
|
|
|
26
30
|
const log = (msg) => process.stderr.write(msg + '\n');
|
|
27
31
|
const output = (msg) => process.stdout.write(msg + '\n');
|
|
28
32
|
|
|
29
|
-
/**
|
|
30
|
-
* Truncates file data (diff and content) for display purposes
|
|
31
|
-
* @param {Object} file - File object with path, status, diff, content, etc.
|
|
32
|
-
* @param {number} maxLength - Maximum length before truncation (default: 500)
|
|
33
|
-
* @returns {Object} File object with truncated diff and content
|
|
34
|
-
*/
|
|
35
|
-
export function truncateFileData(file, maxLength = 500) {
|
|
36
|
-
return {
|
|
37
|
-
path: file.path,
|
|
38
|
-
status: file.status,
|
|
39
|
-
...(file.old_path && { old_path: file.old_path }),
|
|
40
|
-
diff:
|
|
41
|
-
file.diff.length > maxLength
|
|
42
|
-
? `${file.diff.substring(0, maxLength)}... [truncated ${file.diff.length - maxLength} chars]`
|
|
43
|
-
: file.diff,
|
|
44
|
-
content:
|
|
45
|
-
file.content.length > maxLength
|
|
46
|
-
? `${file.content.substring(0, maxLength)}... [truncated ${file.content.length - maxLength} chars]`
|
|
47
|
-
: file.content,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Formats error object for JSON output
|
|
53
|
-
* @param {Error} error - Error object from axios or other source
|
|
54
|
-
* @returns {Object} Formatted error output with success: false
|
|
55
|
-
*/
|
|
56
|
-
export function formatErrorOutput(error) {
|
|
57
|
-
return {
|
|
58
|
-
success: false,
|
|
59
|
-
error: error.message,
|
|
60
|
-
...(error.response && {
|
|
61
|
-
status: error.response.status,
|
|
62
|
-
data: error.response.data,
|
|
63
|
-
}),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
33
|
/**
|
|
68
34
|
* Ask for user confirmation before proceeding
|
|
69
35
|
*/
|
|
@@ -84,23 +50,6 @@ async function confirmAction(message) {
|
|
|
84
50
|
});
|
|
85
51
|
}
|
|
86
52
|
|
|
87
|
-
/**
|
|
88
|
-
* Detect CI provider from environment variables
|
|
89
|
-
* @returns {string|null} Provider name or null if not detected
|
|
90
|
-
*/
|
|
91
|
-
export function detectCIProvider() {
|
|
92
|
-
if (process.env.GITHUB_TOKEN && process.env.GITHUB_REPOSITORY) {
|
|
93
|
-
return 'github';
|
|
94
|
-
}
|
|
95
|
-
if (process.env.SYSTEM_ACCESSTOKEN && process.env.SYSTEM_PULLREQUEST_PULLREQUESTID) {
|
|
96
|
-
return 'azure';
|
|
97
|
-
}
|
|
98
|
-
if (process.env.BITBUCKET_REPO_SLUG && process.env.BITBUCKET_PR_ID) {
|
|
99
|
-
return 'bitbucket';
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
53
|
/**
|
|
105
54
|
* Run the CI integration script to post comments
|
|
106
55
|
* @param {string} provider - CI provider (github, azure, bitbucket)
|
package/src/index.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { truncateFileData, formatErrorOutput, detectCIProvider } from './index.js';
|
|
2
|
+
import { truncateFileData, formatErrorOutput, detectCIProvider, getPrUrl } from './index.js';
|
|
3
3
|
|
|
4
4
|
describe('CLI JSON output mode', () => {
|
|
5
5
|
let stdoutSpy;
|
|
@@ -438,3 +438,111 @@ describe('--comment flag behavior', () => {
|
|
|
438
438
|
expect(shouldShowConfirmation).toBe(true);
|
|
439
439
|
});
|
|
440
440
|
});
|
|
441
|
+
|
|
442
|
+
describe('getPrUrl', () => {
|
|
443
|
+
const originalEnv = process.env;
|
|
444
|
+
|
|
445
|
+
beforeEach(() => {
|
|
446
|
+
// Reset environment before each test
|
|
447
|
+
process.env = { ...originalEnv };
|
|
448
|
+
// Clear all PR-related env vars
|
|
449
|
+
delete process.env.GITHUB_REPOSITORY;
|
|
450
|
+
delete process.env.PR_NUMBER;
|
|
451
|
+
delete process.env.BITBUCKET_WORKSPACE;
|
|
452
|
+
delete process.env.BITBUCKET_REPO_SLUG;
|
|
453
|
+
delete process.env.BITBUCKET_PR_ID;
|
|
454
|
+
delete process.env.SYSTEM_COLLECTIONURI;
|
|
455
|
+
delete process.env.SYSTEM_TEAMPROJECT;
|
|
456
|
+
delete process.env.BUILD_REPOSITORY_NAME;
|
|
457
|
+
delete process.env.SYSTEM_PULLREQUEST_PULLREQUESTID;
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
afterEach(() => {
|
|
461
|
+
process.env = originalEnv;
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should return GitHub PR URL when GitHub env vars are set', () => {
|
|
465
|
+
process.env.GITHUB_REPOSITORY = 'owner/repo';
|
|
466
|
+
process.env.PR_NUMBER = '123';
|
|
467
|
+
|
|
468
|
+
expect(getPrUrl()).toBe('https://github.com/owner/repo/pull/123');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('should return Bitbucket PR URL when Bitbucket env vars are set', () => {
|
|
472
|
+
process.env.BITBUCKET_WORKSPACE = 'myworkspace';
|
|
473
|
+
process.env.BITBUCKET_REPO_SLUG = 'myrepo';
|
|
474
|
+
process.env.BITBUCKET_PR_ID = '456';
|
|
475
|
+
|
|
476
|
+
expect(getPrUrl()).toBe('https://bitbucket.org/myworkspace/myrepo/pull-requests/456');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should return Azure DevOps PR URL when Azure env vars are set', () => {
|
|
480
|
+
process.env.SYSTEM_COLLECTIONURI = 'https://dev.azure.com/myorg/';
|
|
481
|
+
process.env.SYSTEM_TEAMPROJECT = 'myproject';
|
|
482
|
+
process.env.BUILD_REPOSITORY_NAME = 'myrepo';
|
|
483
|
+
process.env.SYSTEM_PULLREQUEST_PULLREQUESTID = '789';
|
|
484
|
+
|
|
485
|
+
expect(getPrUrl()).toBe('https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequest/789');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('should strip trailing slash from Azure DevOps collection URI', () => {
|
|
489
|
+
process.env.SYSTEM_COLLECTIONURI = 'https://dev.azure.com/myorg/';
|
|
490
|
+
process.env.SYSTEM_TEAMPROJECT = 'myproject';
|
|
491
|
+
process.env.BUILD_REPOSITORY_NAME = 'myrepo';
|
|
492
|
+
process.env.SYSTEM_PULLREQUEST_PULLREQUESTID = '789';
|
|
493
|
+
|
|
494
|
+
const url = getPrUrl();
|
|
495
|
+
expect(url).not.toContain('myorg//myproject');
|
|
496
|
+
expect(url).toContain('myorg/myproject');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should URL-encode spaces in Azure DevOps project and repo names', () => {
|
|
500
|
+
process.env.SYSTEM_COLLECTIONURI = 'https://dev.azure.com/myorg/';
|
|
501
|
+
process.env.SYSTEM_TEAMPROJECT = 'My Project';
|
|
502
|
+
process.env.BUILD_REPOSITORY_NAME = 'My Repo';
|
|
503
|
+
process.env.SYSTEM_PULLREQUEST_PULLREQUESTID = '789';
|
|
504
|
+
|
|
505
|
+
expect(getPrUrl()).toBe(
|
|
506
|
+
'https://dev.azure.com/myorg/My%20Project/_git/My%20Repo/pullrequest/789'
|
|
507
|
+
);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should return null when no PR env vars are set', () => {
|
|
511
|
+
expect(getPrUrl()).toBe(null);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('should return null when only partial GitHub env vars are set', () => {
|
|
515
|
+
process.env.GITHUB_REPOSITORY = 'owner/repo';
|
|
516
|
+
// PR_NUMBER not set
|
|
517
|
+
|
|
518
|
+
expect(getPrUrl()).toBe(null);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should return null when only partial Bitbucket env vars are set', () => {
|
|
522
|
+
process.env.BITBUCKET_WORKSPACE = 'myworkspace';
|
|
523
|
+
process.env.BITBUCKET_REPO_SLUG = 'myrepo';
|
|
524
|
+
// BITBUCKET_PR_ID not set
|
|
525
|
+
|
|
526
|
+
expect(getPrUrl()).toBe(null);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('should return null when only partial Azure env vars are set', () => {
|
|
530
|
+
process.env.SYSTEM_COLLECTIONURI = 'https://dev.azure.com/myorg/';
|
|
531
|
+
process.env.SYSTEM_TEAMPROJECT = 'myproject';
|
|
532
|
+
// BUILD_REPOSITORY_NAME and SYSTEM_PULLREQUEST_PULLREQUESTID not set
|
|
533
|
+
|
|
534
|
+
expect(getPrUrl()).toBe(null);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('should prioritize GitHub over other providers when multiple are set', () => {
|
|
538
|
+
// Set all providers
|
|
539
|
+
process.env.GITHUB_REPOSITORY = 'owner/repo';
|
|
540
|
+
process.env.PR_NUMBER = '123';
|
|
541
|
+
process.env.BITBUCKET_WORKSPACE = 'myworkspace';
|
|
542
|
+
process.env.BITBUCKET_REPO_SLUG = 'myrepo';
|
|
543
|
+
process.env.BITBUCKET_PR_ID = '456';
|
|
544
|
+
|
|
545
|
+
// GitHub should be detected first due to check order
|
|
546
|
+
expect(getPrUrl()).toBe('https://github.com/owner/repo/pull/123');
|
|
547
|
+
});
|
|
548
|
+
});
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect CI provider from environment variables
|
|
3
|
+
* @returns {string|null} Provider name or null if not detected
|
|
4
|
+
*/
|
|
5
|
+
export function detectCIProvider() {
|
|
6
|
+
if (process.env.GITHUB_TOKEN && process.env.GITHUB_REPOSITORY) {
|
|
7
|
+
return 'github';
|
|
8
|
+
}
|
|
9
|
+
if (process.env.SYSTEM_ACCESSTOKEN && process.env.SYSTEM_PULLREQUEST_PULLREQUESTID) {
|
|
10
|
+
return 'azure';
|
|
11
|
+
}
|
|
12
|
+
if (process.env.BITBUCKET_REPO_SLUG && process.env.BITBUCKET_PR_ID) {
|
|
13
|
+
return 'bitbucket';
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build PR URL from CI environment variables
|
|
20
|
+
* @returns {string|null} Full PR URL or null if not in CI PR context
|
|
21
|
+
*/
|
|
22
|
+
export function getPrUrl() {
|
|
23
|
+
// GitHub Actions
|
|
24
|
+
if (process.env.GITHUB_REPOSITORY && process.env.PR_NUMBER) {
|
|
25
|
+
return `https://github.com/${process.env.GITHUB_REPOSITORY}/pull/${process.env.PR_NUMBER}`;
|
|
26
|
+
}
|
|
27
|
+
// Bitbucket Pipelines
|
|
28
|
+
if (
|
|
29
|
+
process.env.BITBUCKET_WORKSPACE &&
|
|
30
|
+
process.env.BITBUCKET_REPO_SLUG &&
|
|
31
|
+
process.env.BITBUCKET_PR_ID
|
|
32
|
+
) {
|
|
33
|
+
return `https://bitbucket.org/${process.env.BITBUCKET_WORKSPACE}/${process.env.BITBUCKET_REPO_SLUG}/pull-requests/${process.env.BITBUCKET_PR_ID}`;
|
|
34
|
+
}
|
|
35
|
+
// Azure DevOps Pipelines
|
|
36
|
+
if (
|
|
37
|
+
process.env.SYSTEM_COLLECTIONURI &&
|
|
38
|
+
process.env.SYSTEM_TEAMPROJECT &&
|
|
39
|
+
process.env.BUILD_REPOSITORY_NAME &&
|
|
40
|
+
process.env.SYSTEM_PULLREQUEST_PULLREQUESTID
|
|
41
|
+
) {
|
|
42
|
+
const collectionUri = process.env.SYSTEM_COLLECTIONURI.replace(/\/$/, '');
|
|
43
|
+
return `${collectionUri}/${encodeURIComponent(process.env.SYSTEM_TEAMPROJECT)}/_git/${encodeURIComponent(process.env.BUILD_REPOSITORY_NAME)}/pullrequest/${process.env.SYSTEM_PULLREQUEST_PULLREQUESTID}`;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Truncates file data (diff and content) for display purposes
|
|
50
|
+
* @param {Object} file - File object with path, status, diff, content, etc.
|
|
51
|
+
* @param {number} maxLength - Maximum length before truncation (default: 500)
|
|
52
|
+
* @returns {Object} File object with truncated diff and content
|
|
53
|
+
*/
|
|
54
|
+
export function truncateFileData(file, maxLength = 500) {
|
|
55
|
+
return {
|
|
56
|
+
path: file.path,
|
|
57
|
+
status: file.status,
|
|
58
|
+
...(file.old_path && { old_path: file.old_path }),
|
|
59
|
+
diff:
|
|
60
|
+
file.diff.length > maxLength
|
|
61
|
+
? `${file.diff.substring(0, maxLength)}... [truncated ${file.diff.length - maxLength} chars]`
|
|
62
|
+
: file.diff,
|
|
63
|
+
content:
|
|
64
|
+
file.content.length > maxLength
|
|
65
|
+
? `${file.content.substring(0, maxLength)}... [truncated ${file.content.length - maxLength} chars]`
|
|
66
|
+
: file.content,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Formats error object for JSON output
|
|
72
|
+
* @param {Error} error - Error object from axios or other source
|
|
73
|
+
* @returns {Object} Formatted error output with success: false
|
|
74
|
+
*/
|
|
75
|
+
export function formatErrorOutput(error) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: error.message,
|
|
79
|
+
...(error.response && {
|
|
80
|
+
status: error.response.status,
|
|
81
|
+
data: error.response.data,
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
}
|