@vertaaux/cli 0.2.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/README.md +345 -0
- package/dist/auth/ci-token.d.ts +49 -0
- package/dist/auth/ci-token.d.ts.map +1 -0
- package/dist/auth/ci-token.js +83 -0
- package/dist/auth/device-flow.d.ts +66 -0
- package/dist/auth/device-flow.d.ts.map +1 -0
- package/dist/auth/device-flow.js +156 -0
- package/dist/auth/token-store.d.ts +53 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +78 -0
- package/dist/baseline/diff.d.ts +57 -0
- package/dist/baseline/diff.d.ts.map +1 -0
- package/dist/baseline/diff.js +152 -0
- package/dist/baseline/hash.d.ts +54 -0
- package/dist/baseline/hash.d.ts.map +1 -0
- package/dist/baseline/hash.js +66 -0
- package/dist/baseline/manager.d.ts +89 -0
- package/dist/baseline/manager.d.ts.map +1 -0
- package/dist/baseline/manager.js +157 -0
- package/dist/cache/index.d.ts +8 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +7 -0
- package/dist/cache/route-cache.d.ts +119 -0
- package/dist/cache/route-cache.d.ts.map +1 -0
- package/dist/cache/route-cache.js +213 -0
- package/dist/ci/changed-routes.d.ts +95 -0
- package/dist/ci/changed-routes.d.ts.map +1 -0
- package/dist/ci/changed-routes.js +304 -0
- package/dist/ci/github-api.d.ts +68 -0
- package/dist/ci/github-api.d.ts.map +1 -0
- package/dist/ci/github-api.js +138 -0
- package/dist/ci/gitlab-api.d.ts +75 -0
- package/dist/ci/gitlab-api.d.ts.map +1 -0
- package/dist/ci/gitlab-api.js +180 -0
- package/dist/ci/index.d.ts +6 -0
- package/dist/ci/index.d.ts.map +1 -0
- package/dist/ci/index.js +4 -0
- package/dist/commands/audit.d.ts +58 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +862 -0
- package/dist/commands/baseline.d.ts +22 -0
- package/dist/commands/baseline.d.ts.map +1 -0
- package/dist/commands/baseline.js +210 -0
- package/dist/commands/comment.d.ts +14 -0
- package/dist/commands/comment.d.ts.map +1 -0
- package/dist/commands/comment.js +363 -0
- package/dist/commands/diff.d.ts +24 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +196 -0
- package/dist/commands/doctor.d.ts +58 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +338 -0
- package/dist/commands/download.d.ts +12 -0
- package/dist/commands/download.d.ts.map +1 -0
- package/dist/commands/download.js +183 -0
- package/dist/commands/explain.d.ts +62 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +302 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +212 -0
- package/dist/commands/login.d.ts +14 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +222 -0
- package/dist/commands/policy.d.ts +13 -0
- package/dist/commands/policy.d.ts.map +1 -0
- package/dist/commands/policy.js +347 -0
- package/dist/commands/upload.d.ts +12 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +158 -0
- package/dist/config/defaults.d.ts +21 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +49 -0
- package/dist/config/loader.d.ts +66 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +167 -0
- package/dist/config/schema.d.ts +55 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +6 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1090 -0
- package/dist/interactive/fix-wizard.d.ts +44 -0
- package/dist/interactive/fix-wizard.d.ts.map +1 -0
- package/dist/interactive/fix-wizard.js +286 -0
- package/dist/interactive/init-wizard.d.ts +32 -0
- package/dist/interactive/init-wizard.d.ts.map +1 -0
- package/dist/interactive/init-wizard.js +193 -0
- package/dist/interactive/prompts.d.ts +62 -0
- package/dist/interactive/prompts.d.ts.map +1 -0
- package/dist/interactive/prompts.js +78 -0
- package/dist/monorepo/detector.d.ts +70 -0
- package/dist/monorepo/detector.d.ts.map +1 -0
- package/dist/monorepo/detector.js +278 -0
- package/dist/monorepo/index.d.ts +9 -0
- package/dist/monorepo/index.d.ts.map +1 -0
- package/dist/monorepo/index.js +8 -0
- package/dist/monorepo/workspace.d.ts +142 -0
- package/dist/monorepo/workspace.d.ts.map +1 -0
- package/dist/monorepo/workspace.js +171 -0
- package/dist/output/envelope.d.ts +21 -0
- package/dist/output/envelope.d.ts.map +1 -0
- package/dist/output/envelope.js +27 -0
- package/dist/output/factory.d.ts +73 -0
- package/dist/output/factory.d.ts.map +1 -0
- package/dist/output/factory.js +60 -0
- package/dist/output/formats.d.ts +11 -0
- package/dist/output/formats.d.ts.map +1 -0
- package/dist/output/formats.js +41 -0
- package/dist/output/html.d.ts +45 -0
- package/dist/output/html.d.ts.map +1 -0
- package/dist/output/html.js +607 -0
- package/dist/output/human.d.ts +41 -0
- package/dist/output/human.d.ts.map +1 -0
- package/dist/output/human.js +274 -0
- package/dist/output/json.d.ts +42 -0
- package/dist/output/json.d.ts.map +1 -0
- package/dist/output/json.js +37 -0
- package/dist/output/junit.d.ts +56 -0
- package/dist/output/junit.d.ts.map +1 -0
- package/dist/output/junit.js +135 -0
- package/dist/output/markdown.d.ts +77 -0
- package/dist/output/markdown.d.ts.map +1 -0
- package/dist/output/markdown.js +411 -0
- package/dist/output/sarif.d.ts +160 -0
- package/dist/output/sarif.d.ts.map +1 -0
- package/dist/output/sarif.js +207 -0
- package/dist/policy/evaluator.d.ts +111 -0
- package/dist/policy/evaluator.d.ts.map +1 -0
- package/dist/policy/evaluator.js +362 -0
- package/dist/policy/index.d.ts +15 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +11 -0
- package/dist/policy/loader.d.ts +97 -0
- package/dist/policy/loader.d.ts.map +1 -0
- package/dist/policy/loader.js +281 -0
- package/dist/policy/schema.d.ts +297 -0
- package/dist/policy/schema.d.ts.map +1 -0
- package/dist/policy/schema.js +230 -0
- package/dist/quality-gate/evaluator.d.ts +58 -0
- package/dist/quality-gate/evaluator.d.ts.map +1 -0
- package/dist/quality-gate/evaluator.js +274 -0
- package/dist/quality-gate/index.d.ts +10 -0
- package/dist/quality-gate/index.d.ts.map +1 -0
- package/dist/quality-gate/index.js +7 -0
- package/dist/quality-gate/types.d.ts +103 -0
- package/dist/quality-gate/types.d.ts.map +1 -0
- package/dist/quality-gate/types.js +23 -0
- package/dist/templates/azure-devops.d.ts +25 -0
- package/dist/templates/azure-devops.d.ts.map +1 -0
- package/dist/templates/azure-devops.js +109 -0
- package/dist/templates/circleci.d.ts +28 -0
- package/dist/templates/circleci.d.ts.map +1 -0
- package/dist/templates/circleci.js +86 -0
- package/dist/templates/github-actions.d.ts +81 -0
- package/dist/templates/github-actions.d.ts.map +1 -0
- package/dist/templates/github-actions.js +393 -0
- package/dist/templates/gitlab-ci.d.ts +26 -0
- package/dist/templates/gitlab-ci.d.ts.map +1 -0
- package/dist/templates/gitlab-ci.js +70 -0
- package/dist/templates/index.d.ts +72 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +112 -0
- package/dist/templates/jenkins.d.ts +26 -0
- package/dist/templates/jenkins.d.ts.map +1 -0
- package/dist/templates/jenkins.js +110 -0
- package/dist/ui/banner.d.ts +31 -0
- package/dist/ui/banner.d.ts.map +1 -0
- package/dist/ui/banner.js +84 -0
- package/dist/ui/diagnostics.d.ts +39 -0
- package/dist/ui/diagnostics.d.ts.map +1 -0
- package/dist/ui/diagnostics.js +153 -0
- package/dist/ui/spinner.d.ts +61 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/spinner.js +101 -0
- package/dist/ui/table.d.ts +63 -0
- package/dist/ui/table.d.ts.map +1 -0
- package/dist/ui/table.js +236 -0
- package/dist/utils/client.d.ts +82 -0
- package/dist/utils/client.d.ts.map +1 -0
- package/dist/utils/client.js +128 -0
- package/dist/utils/detect-env.d.ts +59 -0
- package/dist/utils/detect-env.d.ts.map +1 -0
- package/dist/utils/detect-env.js +115 -0
- package/dist/utils/exit-codes.d.ts +47 -0
- package/dist/utils/exit-codes.d.ts.map +1 -0
- package/dist/utils/exit-codes.js +61 -0
- package/dist/utils/logger.d.ts +87 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +185 -0
- package/dist/utils/sanitize.d.ts +36 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +64 -0
- package/dist/utils/validators.d.ts +41 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +123 -0
- package/package.json +63 -0
- package/schemas/vertaaux.config.schema.json +103 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect routes changed in PR for incremental auditing.
|
|
3
|
+
*
|
|
4
|
+
* Uses git diff to find changed files, then maps them to routes.
|
|
5
|
+
* This enables efficient CI by only auditing what changed.
|
|
6
|
+
*
|
|
7
|
+
* Implements CICD-07: Incremental mode audits only routes touched in PR.
|
|
8
|
+
*/
|
|
9
|
+
import { execSync, execFileSync } from "child_process";
|
|
10
|
+
import { validateBranchName } from "../utils/sanitize.js";
|
|
11
|
+
/**
|
|
12
|
+
* Default route patterns for common frameworks.
|
|
13
|
+
*
|
|
14
|
+
* These patterns identify files that typically map to routes.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_ROUTE_PATTERNS = [
|
|
17
|
+
// Next.js App Router
|
|
18
|
+
"src/app/**/page.tsx",
|
|
19
|
+
"src/app/**/page.ts",
|
|
20
|
+
"src/app/**/page.jsx",
|
|
21
|
+
"src/app/**/page.js",
|
|
22
|
+
// Next.js Pages Router
|
|
23
|
+
"src/pages/**/*.tsx",
|
|
24
|
+
"src/pages/**/*.ts",
|
|
25
|
+
"src/pages/**/*.jsx",
|
|
26
|
+
"src/pages/**/*.js",
|
|
27
|
+
"pages/**/*.tsx",
|
|
28
|
+
"pages/**/*.ts",
|
|
29
|
+
"pages/**/*.jsx",
|
|
30
|
+
"pages/**/*.js",
|
|
31
|
+
// SvelteKit
|
|
32
|
+
"src/routes/**/+page.svelte",
|
|
33
|
+
"src/routes/**/+page.ts",
|
|
34
|
+
"src/routes/**/+page.js",
|
|
35
|
+
// Remix
|
|
36
|
+
"app/routes/**/*.tsx",
|
|
37
|
+
"app/routes/**/*.ts",
|
|
38
|
+
"app/routes/**/*.jsx",
|
|
39
|
+
"app/routes/**/*.js",
|
|
40
|
+
// Nuxt
|
|
41
|
+
"pages/**/*.vue",
|
|
42
|
+
// Astro
|
|
43
|
+
"src/pages/**/*.astro",
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Convert a file path to a route URL.
|
|
47
|
+
*
|
|
48
|
+
* Supports common framework conventions:
|
|
49
|
+
* - Next.js App Router: src/app/about/page.tsx -> /about
|
|
50
|
+
* - Next.js Pages Router: src/pages/blog/index.tsx -> /blog
|
|
51
|
+
* - SvelteKit: src/routes/blog/+page.svelte -> /blog
|
|
52
|
+
*
|
|
53
|
+
* @param filePath - The changed file path
|
|
54
|
+
* @returns The route URL or null if not a route file
|
|
55
|
+
*/
|
|
56
|
+
export function filePathToRoute(filePath) {
|
|
57
|
+
// Next.js App Router: src/app/about/page.tsx -> /about
|
|
58
|
+
const appRouterMatch = filePath.match(/^(?:src\/)?app\/(.*)\/page\.(tsx?|jsx?|mdx?)$/);
|
|
59
|
+
if (appRouterMatch) {
|
|
60
|
+
const route = appRouterMatch[1];
|
|
61
|
+
// Handle root route
|
|
62
|
+
if (route === "")
|
|
63
|
+
return "/";
|
|
64
|
+
// Handle dynamic segments: [id] -> :id (for display, keep as-is for audit)
|
|
65
|
+
return `/${route}`;
|
|
66
|
+
}
|
|
67
|
+
// Next.js Pages Router: src/pages/blog/index.tsx -> /blog
|
|
68
|
+
const pagesRouterMatch = filePath.match(/^(?:src\/)?pages\/(.+)\.(tsx?|jsx?|mdx?)$/);
|
|
69
|
+
if (pagesRouterMatch) {
|
|
70
|
+
let route = pagesRouterMatch[1];
|
|
71
|
+
// Remove index suffix
|
|
72
|
+
route = route.replace(/\/index$/, "");
|
|
73
|
+
route = route.replace(/^index$/, "");
|
|
74
|
+
// Handle root route
|
|
75
|
+
if (route === "")
|
|
76
|
+
return "/";
|
|
77
|
+
return `/${route}`;
|
|
78
|
+
}
|
|
79
|
+
// SvelteKit: src/routes/blog/+page.svelte -> /blog
|
|
80
|
+
const sveltekitMatch = filePath.match(/^src\/routes\/(.*)\/\+page\.(svelte|ts|js)$/);
|
|
81
|
+
if (sveltekitMatch) {
|
|
82
|
+
const route = sveltekitMatch[1];
|
|
83
|
+
if (route === "")
|
|
84
|
+
return "/";
|
|
85
|
+
return `/${route}`;
|
|
86
|
+
}
|
|
87
|
+
// Remix: app/routes/blog.tsx -> /blog
|
|
88
|
+
const remixMatch = filePath.match(/^app\/routes\/(.+)\.(tsx?|jsx?)$/);
|
|
89
|
+
if (remixMatch) {
|
|
90
|
+
let route = remixMatch[1];
|
|
91
|
+
// Remix uses . for nested routes: blog.post -> blog/post
|
|
92
|
+
route = route.replace(/\./g, "/");
|
|
93
|
+
// Handle _index (root)
|
|
94
|
+
route = route.replace(/\/_index$/, "");
|
|
95
|
+
route = route.replace(/^_index$/, "");
|
|
96
|
+
if (route === "")
|
|
97
|
+
return "/";
|
|
98
|
+
return `/${route}`;
|
|
99
|
+
}
|
|
100
|
+
// Nuxt: pages/about.vue -> /about
|
|
101
|
+
const nuxtMatch = filePath.match(/^pages\/(.+)\.vue$/);
|
|
102
|
+
if (nuxtMatch) {
|
|
103
|
+
let route = nuxtMatch[1];
|
|
104
|
+
route = route.replace(/\/index$/, "");
|
|
105
|
+
route = route.replace(/^index$/, "");
|
|
106
|
+
if (route === "")
|
|
107
|
+
return "/";
|
|
108
|
+
return `/${route}`;
|
|
109
|
+
}
|
|
110
|
+
// Astro: src/pages/about.astro -> /about
|
|
111
|
+
const astroMatch = filePath.match(/^src\/pages\/(.+)\.astro$/);
|
|
112
|
+
if (astroMatch) {
|
|
113
|
+
let route = astroMatch[1];
|
|
114
|
+
route = route.replace(/\/index$/, "");
|
|
115
|
+
route = route.replace(/^index$/, "");
|
|
116
|
+
if (route === "")
|
|
117
|
+
return "/";
|
|
118
|
+
return `/${route}`;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if a file path matches any of the route patterns.
|
|
124
|
+
*/
|
|
125
|
+
function matchesRoutePattern(filePath, patterns) {
|
|
126
|
+
for (const pattern of patterns) {
|
|
127
|
+
// Convert glob pattern to regex
|
|
128
|
+
const regexPattern = pattern
|
|
129
|
+
.replace(/\*\*/g, "DOUBLESTAR")
|
|
130
|
+
.replace(/\*/g, "[^/]*")
|
|
131
|
+
.replace(/DOUBLESTAR/g, ".*")
|
|
132
|
+
.replace(/\./g, "\\.");
|
|
133
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
134
|
+
if (regex.test(filePath)) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get the list of files changed between branches.
|
|
142
|
+
*
|
|
143
|
+
* @param baseBranch - The base branch to compare against
|
|
144
|
+
* @returns Array of changed file paths
|
|
145
|
+
*/
|
|
146
|
+
function getChangedFiles(baseBranch) {
|
|
147
|
+
const validBranch = validateBranchName(baseBranch);
|
|
148
|
+
try {
|
|
149
|
+
// Try with origin/ prefix first (most common in CI)
|
|
150
|
+
const result = execFileSync("git", ["diff", "--name-only", `origin/${validBranch}...HEAD`], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
151
|
+
return result
|
|
152
|
+
.split("\n")
|
|
153
|
+
.map((line) => line.trim())
|
|
154
|
+
.filter(Boolean);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
try {
|
|
158
|
+
// Fall back to without origin/ prefix (local branches)
|
|
159
|
+
const result = execFileSync("git", ["diff", "--name-only", `${validBranch}...HEAD`], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
160
|
+
return result
|
|
161
|
+
.split("\n")
|
|
162
|
+
.map((line) => line.trim())
|
|
163
|
+
.filter(Boolean);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// If git diff fails, return empty array
|
|
167
|
+
console.error(`Warning: Could not get changed files from git (base: ${validBranch})`);
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Detect base branch from CI environment variables.
|
|
174
|
+
*
|
|
175
|
+
* Supports:
|
|
176
|
+
* - GitHub Actions: GITHUB_BASE_REF
|
|
177
|
+
* - GitLab CI: CI_MERGE_REQUEST_TARGET_BRANCH_NAME
|
|
178
|
+
* - Azure DevOps: SYSTEM_PULLREQUEST_TARGETBRANCH
|
|
179
|
+
* - Jenkins: CHANGE_TARGET
|
|
180
|
+
* - CircleCI: CIRCLE_BRANCH (for base, uses main as fallback)
|
|
181
|
+
*
|
|
182
|
+
* @returns Detected base branch or "main" as default
|
|
183
|
+
*/
|
|
184
|
+
export function detectBaseBranch() {
|
|
185
|
+
let detected;
|
|
186
|
+
// GitHub Actions
|
|
187
|
+
if (process.env.GITHUB_BASE_REF) {
|
|
188
|
+
detected = process.env.GITHUB_BASE_REF;
|
|
189
|
+
}
|
|
190
|
+
// GitLab CI
|
|
191
|
+
if (!detected && process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME) {
|
|
192
|
+
detected = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
|
|
193
|
+
}
|
|
194
|
+
// Azure DevOps
|
|
195
|
+
if (!detected && process.env.SYSTEM_PULLREQUEST_TARGETBRANCH) {
|
|
196
|
+
// Azure uses refs/heads/main format
|
|
197
|
+
detected = process.env.SYSTEM_PULLREQUEST_TARGETBRANCH.replace(/^refs\/heads\//, "");
|
|
198
|
+
}
|
|
199
|
+
// Jenkins
|
|
200
|
+
if (!detected && process.env.CHANGE_TARGET) {
|
|
201
|
+
detected = process.env.CHANGE_TARGET;
|
|
202
|
+
}
|
|
203
|
+
// Try to detect main vs master
|
|
204
|
+
if (!detected) {
|
|
205
|
+
try {
|
|
206
|
+
execSync("git rev-parse --verify origin/main", {
|
|
207
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
208
|
+
});
|
|
209
|
+
detected = "main";
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
try {
|
|
213
|
+
execSync("git rev-parse --verify origin/master", {
|
|
214
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
215
|
+
});
|
|
216
|
+
detected = "master";
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// Default to main
|
|
220
|
+
detected = "main";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Defense in depth: validate the detected branch name.
|
|
225
|
+
// If a CI env var contained malicious characters, fall back to "main".
|
|
226
|
+
try {
|
|
227
|
+
return validateBranchName(detected);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return "main";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Get list of routes affected by changed files.
|
|
235
|
+
*
|
|
236
|
+
* Uses git diff to find changed files, then maps to routes.
|
|
237
|
+
*
|
|
238
|
+
* @param options - Detection options
|
|
239
|
+
* @returns Changed routes result
|
|
240
|
+
*/
|
|
241
|
+
export async function getChangedRoutes(options) {
|
|
242
|
+
const { baseBranch, routePatterns = DEFAULT_ROUTE_PATTERNS, fileToRouteMap } = options;
|
|
243
|
+
// Get changed files from git
|
|
244
|
+
const changedFiles = getChangedFiles(baseBranch);
|
|
245
|
+
if (changedFiles.length === 0) {
|
|
246
|
+
return {
|
|
247
|
+
routes: [],
|
|
248
|
+
changedFiles: [],
|
|
249
|
+
hasChanges: false,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// Collect routes from changed files
|
|
253
|
+
const routeSet = new Set();
|
|
254
|
+
for (const filePath of changedFiles) {
|
|
255
|
+
// First, check custom mapping
|
|
256
|
+
if (fileToRouteMap) {
|
|
257
|
+
for (const [pattern, routes] of Object.entries(fileToRouteMap)) {
|
|
258
|
+
const regexPattern = pattern
|
|
259
|
+
.replace(/\*\*/g, "DOUBLESTAR")
|
|
260
|
+
.replace(/\*/g, "[^/]*")
|
|
261
|
+
.replace(/DOUBLESTAR/g, ".*")
|
|
262
|
+
.replace(/\./g, "\\.");
|
|
263
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
264
|
+
if (regex.test(filePath)) {
|
|
265
|
+
for (const route of routes) {
|
|
266
|
+
routeSet.add(route);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Check if file matches route patterns
|
|
272
|
+
if (matchesRoutePattern(filePath, routePatterns)) {
|
|
273
|
+
const route = filePathToRoute(filePath);
|
|
274
|
+
if (route) {
|
|
275
|
+
routeSet.add(route);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const routes = Array.from(routeSet).sort();
|
|
280
|
+
return {
|
|
281
|
+
routes,
|
|
282
|
+
changedFiles,
|
|
283
|
+
hasChanges: routes.length > 0,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Budget modes for controlling audit scope.
|
|
288
|
+
*
|
|
289
|
+
* CICD-13: Default mode runs under sane budget; full scan is opt-in.
|
|
290
|
+
*/
|
|
291
|
+
export const BUDGET_MODES = {
|
|
292
|
+
/** Quick scan: 5 pages, 30s timeout, 2 concurrent */
|
|
293
|
+
quick: { maxPages: 5, maxTime: 30000, concurrency: 2 },
|
|
294
|
+
/** Standard scan: 20 pages, 2min timeout, 4 concurrent */
|
|
295
|
+
standard: { maxPages: 20, maxTime: 120000, concurrency: 4 },
|
|
296
|
+
/** Full scan: unlimited pages, 10min timeout, 8 concurrent */
|
|
297
|
+
full: { maxPages: Infinity, maxTime: 600000, concurrency: 8 },
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Get budget configuration for a mode.
|
|
301
|
+
*/
|
|
302
|
+
export function getBudgetConfig(mode) {
|
|
303
|
+
return BUDGET_MODES[mode] || BUDGET_MODES.standard;
|
|
304
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub API wrapper for PR comment operations.
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for posting and updating PR comments
|
|
5
|
+
* with sticky comment semantics (find existing by hidden header).
|
|
6
|
+
*
|
|
7
|
+
* Uses native fetch (Node 18+).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Options for GitHub comment operations.
|
|
11
|
+
*/
|
|
12
|
+
export interface GitHubCommentOptions {
|
|
13
|
+
/** GitHub token (from GITHUB_TOKEN env var) */
|
|
14
|
+
token: string;
|
|
15
|
+
/** Repository owner */
|
|
16
|
+
owner: string;
|
|
17
|
+
/** Repository name */
|
|
18
|
+
repo: string;
|
|
19
|
+
/** PR number */
|
|
20
|
+
prNumber: number;
|
|
21
|
+
/** Comment body (markdown) */
|
|
22
|
+
body: string;
|
|
23
|
+
/** Header identifier for finding existing comment */
|
|
24
|
+
header: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Find existing comment by hidden header identifier.
|
|
28
|
+
*
|
|
29
|
+
* Searches PR comments for `<!-- {header} -->` pattern in body.
|
|
30
|
+
* Handles pagination for repos with many comments.
|
|
31
|
+
*
|
|
32
|
+
* @param options - Comment search options (token, owner, repo, prNumber, header)
|
|
33
|
+
* @returns Comment ID if found, null otherwise
|
|
34
|
+
*/
|
|
35
|
+
export declare function findExistingComment(options: Omit<GitHubCommentOptions, "body">): Promise<number | null>;
|
|
36
|
+
/**
|
|
37
|
+
* Post new comment or update existing one.
|
|
38
|
+
*
|
|
39
|
+
* Uses header identifier to find existing comment for update semantics.
|
|
40
|
+
* This prevents comment spam and ensures only one VertaaUX comment per PR.
|
|
41
|
+
*
|
|
42
|
+
* @param options - Comment options (token, owner, repo, prNumber, body, header)
|
|
43
|
+
* @returns Created/updated comment with ID and URL
|
|
44
|
+
*/
|
|
45
|
+
export declare function postOrUpdateGitHubComment(options: GitHubCommentOptions): Promise<{
|
|
46
|
+
id: number;
|
|
47
|
+
html_url: string;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Parse repository string into owner and repo.
|
|
51
|
+
*
|
|
52
|
+
* @param repository - Repository string in "owner/repo" format
|
|
53
|
+
* @returns Parsed owner and repo, or null if invalid
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseRepository(repository: string): {
|
|
56
|
+
owner: string;
|
|
57
|
+
repo: string;
|
|
58
|
+
} | null;
|
|
59
|
+
/**
|
|
60
|
+
* Extract PR number from GitHub Actions environment.
|
|
61
|
+
*
|
|
62
|
+
* Checks GITHUB_REF_NAME for PR merge ref format (e.g., "123/merge")
|
|
63
|
+
* and falls back to GITHUB_EVENT_NUMBER.
|
|
64
|
+
*
|
|
65
|
+
* @returns PR number or null if not in PR context
|
|
66
|
+
*/
|
|
67
|
+
export declare function extractPRNumber(): number | null;
|
|
68
|
+
//# sourceMappingURL=github-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-api.d.ts","sourceRoot":"","sources":["../../src/ci/github-api.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,sBAAsB;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;CAChB;AA8DD;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,IAAI,CAAC,oBAAoB,EAAE,MAAM,CAAC,GAC1C,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA4BxB;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAqB3C;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,GACjB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAIxC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAqB/C"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub API wrapper for PR comment operations.
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for posting and updating PR comments
|
|
5
|
+
* with sticky comment semantics (find existing by hidden header).
|
|
6
|
+
*
|
|
7
|
+
* Uses native fetch (Node 18+).
|
|
8
|
+
*/
|
|
9
|
+
const GITHUB_API_BASE = "https://api.github.com";
|
|
10
|
+
/**
|
|
11
|
+
* Make an authenticated GitHub API request.
|
|
12
|
+
*/
|
|
13
|
+
async function githubRequest(method, url, token, body) {
|
|
14
|
+
const headers = {
|
|
15
|
+
Accept: "application/vnd.github+json",
|
|
16
|
+
Authorization: `Bearer ${token}`,
|
|
17
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
18
|
+
"User-Agent": "VertaaUX-CLI",
|
|
19
|
+
};
|
|
20
|
+
if (body) {
|
|
21
|
+
headers["Content-Type"] = "application/json";
|
|
22
|
+
}
|
|
23
|
+
const response = await fetch(url, {
|
|
24
|
+
method,
|
|
25
|
+
headers,
|
|
26
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const errorData = (await response.json().catch(() => ({})));
|
|
30
|
+
throw new Error(`GitHub API error (${response.status}): ${errorData.message || response.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
// Handle no content responses
|
|
33
|
+
if (response.status === 204) {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
return response.json();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Find existing comment by hidden header identifier.
|
|
40
|
+
*
|
|
41
|
+
* Searches PR comments for `<!-- {header} -->` pattern in body.
|
|
42
|
+
* Handles pagination for repos with many comments.
|
|
43
|
+
*
|
|
44
|
+
* @param options - Comment search options (token, owner, repo, prNumber, header)
|
|
45
|
+
* @returns Comment ID if found, null otherwise
|
|
46
|
+
*/
|
|
47
|
+
export async function findExistingComment(options) {
|
|
48
|
+
const { token, owner, repo, prNumber, header } = options;
|
|
49
|
+
const headerPattern = `<!-- ${header} -->`;
|
|
50
|
+
let page = 1;
|
|
51
|
+
const perPage = 100;
|
|
52
|
+
// Paginate through comments until we find one or run out
|
|
53
|
+
while (true) {
|
|
54
|
+
const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/issues/${prNumber}/comments?page=${page}&per_page=${perPage}`;
|
|
55
|
+
const comments = await githubRequest("GET", url, token);
|
|
56
|
+
for (const comment of comments) {
|
|
57
|
+
if (comment.body?.includes(headerPattern)) {
|
|
58
|
+
return comment.id;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// No more pages
|
|
62
|
+
if (comments.length < perPage) {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
page++;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Post new comment or update existing one.
|
|
71
|
+
*
|
|
72
|
+
* Uses header identifier to find existing comment for update semantics.
|
|
73
|
+
* This prevents comment spam and ensures only one VertaaUX comment per PR.
|
|
74
|
+
*
|
|
75
|
+
* @param options - Comment options (token, owner, repo, prNumber, body, header)
|
|
76
|
+
* @returns Created/updated comment with ID and URL
|
|
77
|
+
*/
|
|
78
|
+
export async function postOrUpdateGitHubComment(options) {
|
|
79
|
+
const { token, owner, repo, prNumber, body, header } = options;
|
|
80
|
+
// Try to find existing comment
|
|
81
|
+
const existingId = await findExistingComment({
|
|
82
|
+
token,
|
|
83
|
+
owner,
|
|
84
|
+
repo,
|
|
85
|
+
prNumber,
|
|
86
|
+
header,
|
|
87
|
+
});
|
|
88
|
+
if (existingId) {
|
|
89
|
+
// Update existing comment
|
|
90
|
+
const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/issues/comments/${existingId}`;
|
|
91
|
+
return githubRequest("PATCH", url, token, { body });
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Create new comment
|
|
95
|
+
const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/issues/${prNumber}/comments`;
|
|
96
|
+
return githubRequest("POST", url, token, { body });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Parse repository string into owner and repo.
|
|
101
|
+
*
|
|
102
|
+
* @param repository - Repository string in "owner/repo" format
|
|
103
|
+
* @returns Parsed owner and repo, or null if invalid
|
|
104
|
+
*/
|
|
105
|
+
export function parseRepository(repository) {
|
|
106
|
+
const match = repository.match(/^([^/]+)\/([^/]+)$/);
|
|
107
|
+
if (!match)
|
|
108
|
+
return null;
|
|
109
|
+
return { owner: match[1], repo: match[2] };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Extract PR number from GitHub Actions environment.
|
|
113
|
+
*
|
|
114
|
+
* Checks GITHUB_REF_NAME for PR merge ref format (e.g., "123/merge")
|
|
115
|
+
* and falls back to GITHUB_EVENT_NUMBER.
|
|
116
|
+
*
|
|
117
|
+
* @returns PR number or null if not in PR context
|
|
118
|
+
*/
|
|
119
|
+
export function extractPRNumber() {
|
|
120
|
+
// From PR merge ref: refs/pull/123/merge -> GITHUB_REF_NAME = "123/merge"
|
|
121
|
+
const refName = process.env.GITHUB_REF_NAME;
|
|
122
|
+
if (refName) {
|
|
123
|
+
const match = refName.match(/^(\d+)\/merge$/);
|
|
124
|
+
if (match) {
|
|
125
|
+
return parseInt(match[1], 10);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// From workflow context
|
|
129
|
+
const eventNumber = process.env.GITHUB_EVENT_NUMBER;
|
|
130
|
+
if (eventNumber) {
|
|
131
|
+
const num = parseInt(eventNumber, 10);
|
|
132
|
+
if (!isNaN(num))
|
|
133
|
+
return num;
|
|
134
|
+
}
|
|
135
|
+
// From pull_request event payload (parsed from GITHUB_EVENT_PATH)
|
|
136
|
+
// This requires reading the event file, skipping for simplicity
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitLab API wrapper for MR note operations.
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for posting and updating MR notes (comments)
|
|
5
|
+
* with sticky note semantics (find existing by hidden header).
|
|
6
|
+
*
|
|
7
|
+
* Uses native fetch (Node 18+).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Options for GitLab note operations.
|
|
11
|
+
*/
|
|
12
|
+
export interface GitLabNoteOptions {
|
|
13
|
+
/** GitLab token (from GITLAB_TOKEN or CI_JOB_TOKEN) */
|
|
14
|
+
token: string;
|
|
15
|
+
/** GitLab API URL (from CI_API_V4_URL or https://gitlab.com/api/v4) */
|
|
16
|
+
apiUrl: string;
|
|
17
|
+
/** Project ID or URL-encoded path */
|
|
18
|
+
projectId: string;
|
|
19
|
+
/** MR IID (internal ID within project) */
|
|
20
|
+
mrIid: number;
|
|
21
|
+
/** Note body (markdown) */
|
|
22
|
+
body: string;
|
|
23
|
+
/** Header identifier for finding existing note */
|
|
24
|
+
header: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* URL-encode a project path for GitLab API.
|
|
28
|
+
*
|
|
29
|
+
* Project ID can be numeric or a path like "group/project".
|
|
30
|
+
* Numeric IDs pass through unchanged, paths are URL-encoded.
|
|
31
|
+
*
|
|
32
|
+
* @param projectId - Project ID or path
|
|
33
|
+
* @returns URL-safe project identifier
|
|
34
|
+
*/
|
|
35
|
+
export declare function encodeProjectId(projectId: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Find existing note by hidden header identifier.
|
|
38
|
+
*
|
|
39
|
+
* Searches MR notes for `<!-- {header} -->` pattern in body.
|
|
40
|
+
* Handles pagination for MRs with many notes.
|
|
41
|
+
*
|
|
42
|
+
* @param options - Note search options
|
|
43
|
+
* @returns Note ID if found, null otherwise
|
|
44
|
+
*/
|
|
45
|
+
export declare function findExistingNote(options: Omit<GitLabNoteOptions, "body">): Promise<number | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Post new note or update existing one.
|
|
48
|
+
*
|
|
49
|
+
* Uses header identifier to find existing note for update semantics.
|
|
50
|
+
* This prevents note spam and ensures only one VertaaUX note per MR.
|
|
51
|
+
*
|
|
52
|
+
* @param options - Note options
|
|
53
|
+
* @returns Created/updated note with ID and web URL
|
|
54
|
+
*/
|
|
55
|
+
export declare function postOrUpdateGitLabNote(options: GitLabNoteOptions): Promise<{
|
|
56
|
+
id: number;
|
|
57
|
+
web_url: string;
|
|
58
|
+
}>;
|
|
59
|
+
/**
|
|
60
|
+
* Extract MR IID from GitLab CI environment.
|
|
61
|
+
*
|
|
62
|
+
* @returns MR IID or null if not in MR context
|
|
63
|
+
*/
|
|
64
|
+
export declare function extractMRIid(): number | null;
|
|
65
|
+
/**
|
|
66
|
+
* Get GitLab API configuration from environment.
|
|
67
|
+
*
|
|
68
|
+
* @returns API configuration or null if not in GitLab CI
|
|
69
|
+
*/
|
|
70
|
+
export declare function getGitLabConfig(): {
|
|
71
|
+
token: string;
|
|
72
|
+
apiUrl: string;
|
|
73
|
+
projectId: string;
|
|
74
|
+
} | null;
|
|
75
|
+
//# sourceMappingURL=gitlab-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitlab-api.d.ts","sourceRoot":"","sources":["../../src/ci/gitlab-api.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;CAChB;AAwED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAOzD;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,GACvC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgCxB;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA6B1C;AAyBD;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAO5C;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,IAAI,CAeP"}
|