@aikidosec/ci-api-client 1.0.18 → 1.0.19

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.
@@ -0,0 +1,73 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ // Drop coverage that points past the end of the source file.
5
+ // applied per-extension in their uploader and are not language-agnostic — we only
6
+ export async function loadSourceLineFixes(
7
+ repositoryRoot: string,
8
+ sourcePath: string
9
+ ): Promise<{ eof: number } | null> {
10
+ if (!repositoryRoot || !sourcePath) {
11
+ return null;
12
+ }
13
+
14
+ try {
15
+ const base = path.resolve(repositoryRoot);
16
+ const target = path.resolve(base, sourcePath);
17
+ const relative = path.relative(base, target);
18
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
19
+ return null;
20
+ }
21
+
22
+ // Do not follow symlinks: a tracked link can point at an unbounded
23
+ // special file (e.g. /dev/zero) and hang or exhaust memory on read.
24
+ const stats = await fs.lstat(target);
25
+ if (stats.isSymbolicLink() || !stats.isFile()) {
26
+ return null;
27
+ }
28
+
29
+ const content = await fs.readFile(target, 'utf8');
30
+ const lineCount = content.split(/\r?\n/).length;
31
+ return { eof: lineCount };
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ type TCoverageRecord = {
38
+ lines: Map<number, unknown>;
39
+ functions: Map<string, { line: number }>;
40
+ branches: Map<string, unknown>;
41
+ };
42
+
43
+ export function applySourceLineFixes(
44
+ record: TCoverageRecord,
45
+ lineFixes: { eof: number } | null
46
+ ): TCoverageRecord {
47
+ if (!lineFixes) {
48
+ return record;
49
+ }
50
+
51
+ const { eof } = lineFixes;
52
+
53
+ for (const lineNumber of record.lines.keys()) {
54
+ if (lineNumber < 1 || lineNumber > eof) {
55
+ record.lines.delete(lineNumber);
56
+ }
57
+ }
58
+
59
+ for (const [functionName, meta] of record.functions.entries()) {
60
+ if (meta.line < 1 || meta.line > eof) {
61
+ record.functions.delete(functionName);
62
+ }
63
+ }
64
+
65
+ for (const branchKey of record.branches.keys()) {
66
+ const lineNumber = Number(branchKey.split('\0')[0]);
67
+ if (lineNumber < 1 || lineNumber > eof) {
68
+ record.branches.delete(branchKey);
69
+ }
70
+ }
71
+
72
+ return record;
73
+ }
@@ -0,0 +1,115 @@
1
+ import { Command } from 'commander';
2
+ import { Ora } from 'ora';
3
+ import { uploadCodeCoverage } from '../aikidoApi.js';
4
+ import { collectUploadPayload } from '../codeCoverage/collectUploadPayload.js';
5
+ import { getApiKey } from '../configuration.js';
6
+ import {
7
+ outputError,
8
+ outputHttpError,
9
+ outputLog,
10
+ startSpinner,
11
+ } from '../output.js';
12
+
13
+ type TUploadCoverageUserCliOptions = {
14
+ repoName: string;
15
+ commitSha: string;
16
+ branchName: string;
17
+ filePaths: string[];
18
+ repositoryId: string | number;
19
+ };
20
+
21
+ async function cli(options: TUploadCoverageUserCliOptions) {
22
+ const apiKey = getApiKey();
23
+
24
+ if (!apiKey) {
25
+ outputError('Please set an api key using: aikido-api-client apikey <key>');
26
+ return;
27
+ return;
28
+ }
29
+
30
+ const filePaths = normalizeFilePaths(options.filePaths);
31
+ if (filePaths.length === 0) {
32
+ outputError(
33
+ 'No code coverage file(s) provided. Specify at least one path with --file-paths.'
34
+ );
35
+ }
36
+
37
+ let loader: Ora | null = startSpinner(
38
+ 'Collecting coverage files and repository metadata'
39
+ );
40
+
41
+ let payload;
42
+ try {
43
+ payload = await collectUploadPayload(filePaths);
44
+ } catch (error) {
45
+ loader?.fail();
46
+ outputError(error instanceof Error ? error.message : String(error));
47
+ return;
48
+ }
49
+
50
+ loader?.succeed(
51
+ `Found ${payload.files.length} coverage file(s) (source paths=${payload.repository_source_paths.length}, eof=${Object.keys(payload.eof).length})`
52
+ );
53
+
54
+ for (const file of payload.files) {
55
+ outputLog(` ${file.filename} (${file.format})`);
56
+ }
57
+
58
+ loader = startSpinner('Uploading code coverage to Aikido');
59
+
60
+ try {
61
+ await uploadCodeCoverage({
62
+ repository_id: options.repositoryId,
63
+ repo_name: options.repoName,
64
+ commit_sha: options.commitSha,
65
+ branch_name: options.branchName,
66
+ repository_source_paths: payload.repository_source_paths,
67
+ eof: payload.eof,
68
+ files: payload.files,
69
+ });
70
+
71
+
72
+ loader?.succeed('Code coverage upload completed');
73
+ process.exit(0);
74
+ } catch (error) {
75
+ loader?.fail();
76
+ outputHttpError(error as any);
77
+ process.exit(1);
78
+ }
79
+ }
80
+
81
+ const normalizeFilePaths = (filePaths: string[]): string[] =>
82
+ filePaths
83
+ .flatMap((entry) => entry.split(/\n|\s+|,/))
84
+ .map((filePath) => filePath.trim())
85
+ .filter(Boolean);
86
+
87
+ export const cliSetup = (program: Command) =>
88
+ program
89
+ .command('upload-coverage')
90
+ .requiredOption(
91
+ '-r, --repo-name <reponame>',
92
+ 'Repository name as owner/repo (e.g. org/my-repo)'
93
+ )
94
+ .requiredOption(
95
+ '-c, --commit-sha <commitsha>',
96
+ 'The commit SHA for this CI run'
97
+ )
98
+ .requiredOption(
99
+ '-b, --branch-name <branchname>',
100
+ 'The branch name for this CI run'
101
+ )
102
+ .requiredOption(
103
+ '-f, --file-paths <paths...>',
104
+ 'Path(s) to LCOV or Cobertura coverage report(s). Format is detected from each filename.'
105
+ )
106
+ .requiredOption(
107
+ '-ri, --repository-id <repositoryid>',
108
+ 'The scm repository id'
109
+ )
110
+ .description(
111
+ 'Upload LCOV or Cobertura coverage reports to Aikido (with repository_source_paths and EOF metadata).'
112
+ )
113
+ .action(cli);
114
+
115
+ export default { cli, cliSetup };
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ import apiKey from './commands/apiKey.js';
6
6
  import scan from './commands/scan.js';
7
7
  import scanRelease from './commands/scanRelease.js';
8
8
  import upload from './commands/upload.js';
9
+ import uploadCoverage from './commands/uploadCoverage.js';
9
10
 
10
11
  // Load all .env configuration variables and auto-inject them into process.env
11
12
  dotenv.config(
@@ -27,6 +28,7 @@ apiKey.cliSetup(program);
27
28
  scan.cliSetup(program);
28
29
  scanRelease.cliSetup(program);
29
30
  upload.cliSetup(program);
31
+ uploadCoverage.cliSetup(program);
30
32
 
31
33
  // Override global process.env type
32
34
  declare global {