@aspruyt/xfg 3.7.2 → 3.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +63 -78
- package/dist/logger.d.ts +0 -15
- package/dist/logger.js +0 -34
- package/dist/plan-formatter.d.ts +39 -0
- package/dist/plan-formatter.js +84 -0
- package/dist/plan-summary.d.ts +8 -0
- package/dist/plan-summary.js +110 -0
- package/dist/repo-settings-diff.js +3 -3
- package/dist/repo-settings-processor.js +7 -1
- package/dist/resource-converters.d.ts +25 -0
- package/dist/resource-converters.js +95 -0
- package/dist/strategies/github-repo-settings-strategy.d.ts +4 -0
- package/dist/strategies/github-repo-settings-strategy.js +56 -1
- package/dist/strategies/repo-settings-strategy.d.ts +7 -0
- package/dist/strategies/repo-settings-strategy.js +2 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -15,12 +15,14 @@ import { sanitizeBranchName, validateBranchName } from "./git-ops.js";
|
|
|
15
15
|
import { logger } from "./logger.js";
|
|
16
16
|
import { generateWorkspaceName } from "./workspace-utils.js";
|
|
17
17
|
import { RepositoryProcessor, } from "./repository-processor.js";
|
|
18
|
-
import { writeSummary } from "./github-summary.js";
|
|
19
18
|
import { buildRepoResult, buildErrorResult } from "./summary-utils.js";
|
|
20
19
|
import { RulesetProcessor, } from "./ruleset-processor.js";
|
|
21
20
|
import { getManagedRulesets } from "./manifest.js";
|
|
22
21
|
import { isGitHubRepo } from "./repo-detector.js";
|
|
23
22
|
import { RepoSettingsProcessor, } from "./repo-settings-processor.js";
|
|
23
|
+
import { printPlan } from "./plan-formatter.js";
|
|
24
|
+
import { writePlanSummary } from "./plan-summary.js";
|
|
25
|
+
import { rulesetResultToResources, syncResultToResources, repoSettingsResultToResources, } from "./resource-converters.js";
|
|
24
26
|
/**
|
|
25
27
|
* Default factory that creates a real RepositoryProcessor.
|
|
26
28
|
*/
|
|
@@ -115,6 +117,8 @@ export async function runSync(options, processorFactory = defaultProcessorFactor
|
|
|
115
117
|
console.log(`Branch: ${branchName}\n`);
|
|
116
118
|
const processor = processorFactory();
|
|
117
119
|
const results = [];
|
|
120
|
+
// Build plan for Terraform-style output
|
|
121
|
+
const plan = { resources: [], errors: [] };
|
|
118
122
|
for (let i = 0; i < config.repos.length; i++) {
|
|
119
123
|
const repoConfig = config.repos[i];
|
|
120
124
|
// Apply CLI merge overrides to repo config
|
|
@@ -136,6 +140,10 @@ export async function runSync(options, processorFactory = defaultProcessorFactor
|
|
|
136
140
|
catch (error) {
|
|
137
141
|
logger.error(current, repoConfig.git, String(error));
|
|
138
142
|
results.push(buildErrorResult(repoConfig.git, error));
|
|
143
|
+
plan.errors.push({
|
|
144
|
+
repo: repoConfig.git,
|
|
145
|
+
message: error instanceof Error ? error.message : String(error),
|
|
146
|
+
});
|
|
139
147
|
continue;
|
|
140
148
|
}
|
|
141
149
|
const repoName = getRepoDisplayName(repoInfo);
|
|
@@ -162,27 +170,27 @@ export async function runSync(options, processorFactory = defaultProcessorFactor
|
|
|
162
170
|
else {
|
|
163
171
|
logger.error(current, repoName, result.message);
|
|
164
172
|
}
|
|
173
|
+
// Collect resources for plan output
|
|
174
|
+
plan.resources.push(...syncResultToResources(repoName, repoConfig, result));
|
|
165
175
|
}
|
|
166
176
|
catch (error) {
|
|
167
177
|
logger.error(current, repoName, String(error));
|
|
168
178
|
results.push(buildErrorResult(repoName, error));
|
|
179
|
+
plan.errors.push({
|
|
180
|
+
repo: repoName,
|
|
181
|
+
message: error instanceof Error ? error.message : String(error),
|
|
182
|
+
});
|
|
169
183
|
}
|
|
170
184
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
writeSummary({
|
|
185
|
+
// Print Terraform-style plan summary
|
|
186
|
+
console.log("");
|
|
187
|
+
printPlan(plan);
|
|
188
|
+
// Write GitHub Actions job summary
|
|
189
|
+
writePlanSummary(plan, {
|
|
177
190
|
title: "Config Sync Summary",
|
|
178
|
-
dryRun: options.dryRun,
|
|
179
|
-
total: config.repos.length,
|
|
180
|
-
succeeded,
|
|
181
|
-
skipped,
|
|
182
|
-
failed,
|
|
183
|
-
results,
|
|
191
|
+
dryRun: options.dryRun ?? false,
|
|
184
192
|
});
|
|
185
|
-
if (
|
|
193
|
+
if (plan.errors && plan.errors.length > 0) {
|
|
186
194
|
process.exit(1);
|
|
187
195
|
}
|
|
188
196
|
}
|
|
@@ -228,14 +236,8 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
228
236
|
const processor = processorFactory();
|
|
229
237
|
const repoProcessor = repoProcessorFactory();
|
|
230
238
|
const results = [];
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
let skipCount = 0;
|
|
234
|
-
// Tracking for multi-repo summary in dry-run mode
|
|
235
|
-
let totalCreates = 0;
|
|
236
|
-
let totalUpdates = 0;
|
|
237
|
-
let totalDeletes = 0;
|
|
238
|
-
let reposWithChanges = 0;
|
|
239
|
+
// Build plan for Terraform-style output
|
|
240
|
+
const plan = { resources: [], errors: [] };
|
|
239
241
|
for (let i = 0; i < reposWithRulesets.length; i++) {
|
|
240
242
|
const repoConfig = reposWithRulesets[i];
|
|
241
243
|
let repoInfo;
|
|
@@ -247,14 +249,28 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
247
249
|
catch (error) {
|
|
248
250
|
logger.error(i + 1, repoConfig.git, String(error));
|
|
249
251
|
results.push(buildErrorResult(repoConfig.git, error));
|
|
250
|
-
|
|
252
|
+
plan.errors.push({
|
|
253
|
+
repo: repoConfig.git,
|
|
254
|
+
message: error instanceof Error ? error.message : String(error),
|
|
255
|
+
});
|
|
251
256
|
continue;
|
|
252
257
|
}
|
|
253
258
|
const repoName = getRepoDisplayName(repoInfo);
|
|
254
259
|
// Skip non-GitHub repos
|
|
255
260
|
if (!isGitHubRepo(repoInfo)) {
|
|
256
261
|
logger.skip(i + 1, repoName, "GitHub Rulesets only supported for GitHub repos");
|
|
257
|
-
|
|
262
|
+
// Mark all rulesets from this repo as skipped
|
|
263
|
+
if (repoConfig.settings?.rulesets) {
|
|
264
|
+
for (const rulesetName of Object.keys(repoConfig.settings.rulesets)) {
|
|
265
|
+
plan.resources.push({
|
|
266
|
+
type: "ruleset",
|
|
267
|
+
repo: repoName,
|
|
268
|
+
name: rulesetName,
|
|
269
|
+
action: "skipped",
|
|
270
|
+
skipReason: "GitHub Rulesets only supported for GitHub repos",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
258
274
|
continue;
|
|
259
275
|
}
|
|
260
276
|
// Note: For settings command, we don't clone repos - we work with the API directly.
|
|
@@ -269,34 +285,11 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
269
285
|
managedRulesets,
|
|
270
286
|
noDelete: options.noDelete,
|
|
271
287
|
});
|
|
272
|
-
// Display plan output for dry-run mode
|
|
273
|
-
if (options.dryRun && result.planOutput) {
|
|
274
|
-
if (result.planOutput.lines.length > 0) {
|
|
275
|
-
logger.rulesetPlan(repoName, result.planOutput.lines, {
|
|
276
|
-
creates: result.planOutput.creates,
|
|
277
|
-
updates: result.planOutput.updates,
|
|
278
|
-
deletes: result.planOutput.deletes,
|
|
279
|
-
unchanged: result.planOutput.unchanged,
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
// Accumulate totals for multi-repo summary
|
|
283
|
-
totalCreates += result.planOutput.creates;
|
|
284
|
-
totalUpdates += result.planOutput.updates;
|
|
285
|
-
totalDeletes += result.planOutput.deletes;
|
|
286
|
-
if (result.planOutput.creates +
|
|
287
|
-
result.planOutput.updates +
|
|
288
|
-
result.planOutput.deletes >
|
|
289
|
-
0) {
|
|
290
|
-
reposWithChanges++;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
288
|
if (result.skipped) {
|
|
294
289
|
logger.skip(i + 1, repoName, result.message);
|
|
295
|
-
skipCount++;
|
|
296
290
|
}
|
|
297
291
|
else if (result.success) {
|
|
298
292
|
logger.success(i + 1, repoName, result.message);
|
|
299
|
-
successCount++;
|
|
300
293
|
// Update manifest with ruleset tracking if there are rulesets to track
|
|
301
294
|
if (result.manifestUpdate &&
|
|
302
295
|
result.manifestUpdate.rulesets.length > 0) {
|
|
@@ -316,7 +309,6 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
316
309
|
}
|
|
317
310
|
else {
|
|
318
311
|
logger.error(i + 1, repoName, result.message);
|
|
319
|
-
failCount++;
|
|
320
312
|
}
|
|
321
313
|
results.push({
|
|
322
314
|
repoName,
|
|
@@ -328,11 +320,16 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
328
320
|
message: result.message,
|
|
329
321
|
rulesetPlanDetails: result.planOutput?.entries,
|
|
330
322
|
});
|
|
323
|
+
// Collect resources for plan output
|
|
324
|
+
plan.resources.push(...rulesetResultToResources(repoName, result));
|
|
331
325
|
}
|
|
332
326
|
catch (error) {
|
|
333
327
|
logger.error(i + 1, repoName, String(error));
|
|
334
328
|
results.push(buildErrorResult(repoName, error));
|
|
335
|
-
|
|
329
|
+
plan.errors.push({
|
|
330
|
+
repo: repoName,
|
|
331
|
+
message: error instanceof Error ? error.message : String(error),
|
|
332
|
+
});
|
|
336
333
|
}
|
|
337
334
|
}
|
|
338
335
|
// Process repo settings
|
|
@@ -349,7 +346,10 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
349
346
|
}
|
|
350
347
|
catch (error) {
|
|
351
348
|
console.error(`Failed to parse ${repoConfig.git}: ${error}`);
|
|
352
|
-
|
|
349
|
+
plan.errors.push({
|
|
350
|
+
repo: repoConfig.git,
|
|
351
|
+
message: error instanceof Error ? error.message : String(error),
|
|
352
|
+
});
|
|
353
353
|
continue;
|
|
354
354
|
}
|
|
355
355
|
const repoName = getRepoDisplayName(repoInfo);
|
|
@@ -374,11 +374,9 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
374
374
|
}
|
|
375
375
|
else if (result.success) {
|
|
376
376
|
console.log(chalk.green(` ✓ ${repoName}: ${result.message}`));
|
|
377
|
-
successCount++;
|
|
378
377
|
}
|
|
379
378
|
else {
|
|
380
379
|
console.log(chalk.red(` ✗ ${repoName}: ${result.message}`));
|
|
381
|
-
failCount++;
|
|
382
380
|
}
|
|
383
381
|
// Merge repo settings plan details into existing result or push new
|
|
384
382
|
if (!result.skipped) {
|
|
@@ -395,40 +393,27 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
395
393
|
});
|
|
396
394
|
}
|
|
397
395
|
}
|
|
396
|
+
// Collect resources for plan output
|
|
397
|
+
plan.resources.push(...repoSettingsResultToResources(repoName, result));
|
|
398
398
|
}
|
|
399
399
|
catch (error) {
|
|
400
400
|
console.error(` ✗ ${repoName}: ${error}`);
|
|
401
|
-
|
|
401
|
+
plan.errors.push({
|
|
402
|
+
repo: repoName,
|
|
403
|
+
message: error instanceof Error ? error.message : String(error),
|
|
404
|
+
});
|
|
402
405
|
}
|
|
403
406
|
}
|
|
404
407
|
}
|
|
405
|
-
//
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (totalCreates > 0)
|
|
411
|
-
totalParts.push(chalk.green(`${totalCreates} to create`));
|
|
412
|
-
if (totalUpdates > 0)
|
|
413
|
-
totalParts.push(chalk.yellow(`${totalUpdates} to update`));
|
|
414
|
-
if (totalDeletes > 0)
|
|
415
|
-
totalParts.push(chalk.red(`${totalDeletes} to delete`));
|
|
416
|
-
console.log(chalk.bold(`Total: ${totalParts.join(", ")} across ${reposWithChanges} ${reposWithChanges === 1 ? "repository" : "repositories"}`));
|
|
417
|
-
}
|
|
418
|
-
// Summary
|
|
419
|
-
console.log("\n" + "=".repeat(50));
|
|
420
|
-
console.log(`Completed: ${successCount} succeeded, ${skipCount} skipped, ${failCount} failed`);
|
|
421
|
-
// Write GitHub Actions job summary if available
|
|
422
|
-
writeSummary({
|
|
408
|
+
// Print Terraform-style plan summary
|
|
409
|
+
console.log("");
|
|
410
|
+
printPlan(plan);
|
|
411
|
+
// Write GitHub Actions job summary
|
|
412
|
+
writePlanSummary(plan, {
|
|
423
413
|
title: "Repository Settings Summary",
|
|
424
|
-
dryRun: options.dryRun,
|
|
425
|
-
total: reposWithRulesets.length + reposWithRepoSettings.length,
|
|
426
|
-
succeeded: successCount,
|
|
427
|
-
skipped: skipCount,
|
|
428
|
-
failed: failCount,
|
|
429
|
-
results,
|
|
414
|
+
dryRun: options.dryRun ?? false,
|
|
430
415
|
});
|
|
431
|
-
if (
|
|
416
|
+
if (plan.errors && plan.errors.length > 0) {
|
|
432
417
|
process.exit(1);
|
|
433
418
|
}
|
|
434
419
|
}
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
import { FileStatus } from "./diff-utils.js";
|
|
2
|
-
export interface RulesetPlanCounts {
|
|
3
|
-
creates: number;
|
|
4
|
-
updates: number;
|
|
5
|
-
deletes: number;
|
|
6
|
-
unchanged: number;
|
|
7
|
-
}
|
|
8
2
|
export interface ILogger {
|
|
9
3
|
info(message: string): void;
|
|
10
4
|
fileDiff(fileName: string, status: FileStatus, diffLines: string[]): void;
|
|
11
5
|
diffSummary(newCount: number, modifiedCount: number, unchangedCount: number, deletedCount?: number): void;
|
|
12
|
-
rulesetPlan(repoName: string, planLines: string[], counts: RulesetPlanCounts): void;
|
|
13
6
|
setTotal(total: number): void;
|
|
14
7
|
progress(current: number, repoName: string, message: string): void;
|
|
15
8
|
success(current: number, repoName: string, message: string): void;
|
|
16
9
|
skip(current: number, repoName: string, reason: string): void;
|
|
17
10
|
error(current: number, repoName: string, error: string): void;
|
|
18
|
-
summary(): void;
|
|
19
|
-
hasFailures(): boolean;
|
|
20
11
|
}
|
|
21
12
|
export interface LoggerStats {
|
|
22
13
|
total: number;
|
|
@@ -41,11 +32,5 @@ export declare class Logger implements ILogger {
|
|
|
41
32
|
* Display summary statistics for dry-run diff.
|
|
42
33
|
*/
|
|
43
34
|
diffSummary(newCount: number, modifiedCount: number, unchangedCount: number, deletedCount?: number): void;
|
|
44
|
-
/**
|
|
45
|
-
* Display ruleset plan output for dry-run mode.
|
|
46
|
-
*/
|
|
47
|
-
rulesetPlan(repoName: string, planLines: string[], counts: RulesetPlanCounts): void;
|
|
48
|
-
summary(): void;
|
|
49
|
-
hasFailures(): boolean;
|
|
50
35
|
}
|
|
51
36
|
export declare const logger: Logger;
|
package/dist/logger.js
CHANGED
|
@@ -62,39 +62,5 @@ export class Logger {
|
|
|
62
62
|
console.log(chalk.gray(` Summary: ${parts.join(", ")}`));
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
/**
|
|
66
|
-
* Display ruleset plan output for dry-run mode.
|
|
67
|
-
*/
|
|
68
|
-
rulesetPlan(repoName, planLines, counts) {
|
|
69
|
-
console.log("");
|
|
70
|
-
console.log(chalk.bold(`Repository: ${repoName}`));
|
|
71
|
-
for (const line of planLines) {
|
|
72
|
-
console.log(line);
|
|
73
|
-
}
|
|
74
|
-
// Summary line
|
|
75
|
-
const parts = [];
|
|
76
|
-
if (counts.creates > 0)
|
|
77
|
-
parts.push(chalk.green(`${counts.creates} to create`));
|
|
78
|
-
if (counts.updates > 0)
|
|
79
|
-
parts.push(chalk.yellow(`${counts.updates} to update`));
|
|
80
|
-
if (counts.deletes > 0)
|
|
81
|
-
parts.push(chalk.red(`${counts.deletes} to delete`));
|
|
82
|
-
const unchangedPart = counts.unchanged > 0
|
|
83
|
-
? chalk.gray(` (${counts.unchanged} unchanged)`)
|
|
84
|
-
: "";
|
|
85
|
-
const summaryLine = parts.length > 0 ? parts.join(", ") + unchangedPart : "No changes";
|
|
86
|
-
console.log(chalk.gray(`Plan: ${summaryLine}`));
|
|
87
|
-
}
|
|
88
|
-
summary() {
|
|
89
|
-
console.log("");
|
|
90
|
-
console.log(chalk.bold("Summary:"));
|
|
91
|
-
console.log(` Total: ${this.stats.total}`);
|
|
92
|
-
console.log(chalk.green(` Succeeded: ${this.stats.succeeded}`));
|
|
93
|
-
console.log(chalk.yellow(` Skipped: ${this.stats.skipped}`));
|
|
94
|
-
console.log(chalk.red(` Failed: ${this.stats.failed}`));
|
|
95
|
-
}
|
|
96
|
-
hasFailures() {
|
|
97
|
-
return this.stats.failed > 0;
|
|
98
|
-
}
|
|
99
65
|
}
|
|
100
66
|
export const logger = new Logger();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type ResourceType = "file" | "ruleset" | "setting";
|
|
2
|
+
export type ResourceAction = "create" | "update" | "delete" | "unchanged" | "skipped";
|
|
3
|
+
export interface Resource {
|
|
4
|
+
type: ResourceType;
|
|
5
|
+
repo: string;
|
|
6
|
+
name: string;
|
|
7
|
+
action: ResourceAction;
|
|
8
|
+
details?: ResourceDetails;
|
|
9
|
+
skipReason?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ResourceDetails {
|
|
12
|
+
diff?: string[];
|
|
13
|
+
properties?: PropertyChange[];
|
|
14
|
+
}
|
|
15
|
+
export interface PropertyChange {
|
|
16
|
+
path: string;
|
|
17
|
+
action: "add" | "change" | "remove";
|
|
18
|
+
oldValue?: unknown;
|
|
19
|
+
newValue?: unknown;
|
|
20
|
+
}
|
|
21
|
+
export declare function formatResourceId(resource: Resource): string;
|
|
22
|
+
export declare function formatResourceLine(resource: Resource): string;
|
|
23
|
+
export interface PlanCounts {
|
|
24
|
+
create: number;
|
|
25
|
+
update: number;
|
|
26
|
+
delete: number;
|
|
27
|
+
skipped?: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function formatPlanSummary(counts: PlanCounts): string;
|
|
30
|
+
export interface Plan {
|
|
31
|
+
resources: Resource[];
|
|
32
|
+
errors?: RepoError[];
|
|
33
|
+
}
|
|
34
|
+
export interface RepoError {
|
|
35
|
+
repo: string;
|
|
36
|
+
message: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function formatPlan(plan: Plan): string[];
|
|
39
|
+
export declare function printPlan(plan: Plan): void;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
export function formatResourceId(resource) {
|
|
3
|
+
return `${resource.type} "${resource.repo}/${resource.name}"`;
|
|
4
|
+
}
|
|
5
|
+
export function formatResourceLine(resource) {
|
|
6
|
+
const id = formatResourceId(resource);
|
|
7
|
+
switch (resource.action) {
|
|
8
|
+
case "create":
|
|
9
|
+
return chalk.green(`+ ${id}`);
|
|
10
|
+
case "update":
|
|
11
|
+
return chalk.yellow(`~ ${id}`);
|
|
12
|
+
case "delete":
|
|
13
|
+
return chalk.red(`- ${id}`);
|
|
14
|
+
case "skipped":
|
|
15
|
+
return chalk.gray(`⊘ ${id}`);
|
|
16
|
+
case "unchanged":
|
|
17
|
+
return chalk.gray(` ${id}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function formatPlanSummary(counts) {
|
|
21
|
+
const parts = [];
|
|
22
|
+
if (counts.create > 0) {
|
|
23
|
+
parts.push(chalk.green(`${counts.create} to create`));
|
|
24
|
+
}
|
|
25
|
+
if (counts.update > 0) {
|
|
26
|
+
parts.push(chalk.yellow(`${counts.update} to change`));
|
|
27
|
+
}
|
|
28
|
+
if (counts.delete > 0) {
|
|
29
|
+
parts.push(chalk.red(`${counts.delete} to destroy`));
|
|
30
|
+
}
|
|
31
|
+
if (parts.length === 0 && (!counts.skipped || counts.skipped === 0)) {
|
|
32
|
+
return "No changes. Your repositories match the configuration.";
|
|
33
|
+
}
|
|
34
|
+
let summary = parts.length > 0 ? `Plan: ${parts.join(", ")}` : "Plan:";
|
|
35
|
+
if (counts.skipped && counts.skipped > 0) {
|
|
36
|
+
summary += chalk.gray(` (${counts.skipped} skipped)`);
|
|
37
|
+
}
|
|
38
|
+
return summary;
|
|
39
|
+
}
|
|
40
|
+
export function formatPlan(plan) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
// Filter to only changed resources
|
|
43
|
+
const changedResources = plan.resources.filter((r) => r.action !== "unchanged");
|
|
44
|
+
// Format each resource
|
|
45
|
+
for (const resource of changedResources) {
|
|
46
|
+
lines.push(formatResourceLine(resource));
|
|
47
|
+
// Add details if present (indented)
|
|
48
|
+
if (resource.details?.diff) {
|
|
49
|
+
for (const diffLine of resource.details.diff) {
|
|
50
|
+
lines.push(` ${diffLine}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Add errors
|
|
55
|
+
if (plan.errors && plan.errors.length > 0) {
|
|
56
|
+
for (const error of plan.errors) {
|
|
57
|
+
lines.push(chalk.red(`✗ ${error.repo}`));
|
|
58
|
+
lines.push(chalk.red(` Error: ${error.message}`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Add blank line before summary
|
|
62
|
+
if (lines.length > 0) {
|
|
63
|
+
lines.push("");
|
|
64
|
+
}
|
|
65
|
+
// Count actions
|
|
66
|
+
const counts = {
|
|
67
|
+
create: plan.resources.filter((r) => r.action === "create").length,
|
|
68
|
+
update: plan.resources.filter((r) => r.action === "update").length,
|
|
69
|
+
delete: plan.resources.filter((r) => r.action === "delete").length,
|
|
70
|
+
skipped: plan.resources.filter((r) => r.action === "skipped").length,
|
|
71
|
+
};
|
|
72
|
+
lines.push(formatPlanSummary(counts));
|
|
73
|
+
// Add error count if any
|
|
74
|
+
if (plan.errors && plan.errors.length > 0) {
|
|
75
|
+
lines.push(chalk.red(`${plan.errors.length} ${plan.errors.length === 1 ? "repository" : "repositories"} failed.`));
|
|
76
|
+
}
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
export function printPlan(plan) {
|
|
80
|
+
const lines = formatPlan(plan);
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
console.log(line);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Plan, Resource, PlanCounts, RepoError } from "./plan-formatter.js";
|
|
2
|
+
export type { Plan, Resource, PlanCounts, RepoError };
|
|
3
|
+
export interface PlanMarkdownOptions {
|
|
4
|
+
title: string;
|
|
5
|
+
dryRun: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function formatPlanMarkdown(plan: Plan, options: PlanMarkdownOptions): string;
|
|
8
|
+
export declare function writePlanSummary(plan: Plan, options: PlanMarkdownOptions): void;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
2
|
+
function getActionSymbol(action) {
|
|
3
|
+
switch (action) {
|
|
4
|
+
case "create":
|
|
5
|
+
return "+";
|
|
6
|
+
case "update":
|
|
7
|
+
return "~";
|
|
8
|
+
case "delete":
|
|
9
|
+
return "-";
|
|
10
|
+
case "skipped":
|
|
11
|
+
return "⊘";
|
|
12
|
+
default:
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function formatResourceIdPlain(resource) {
|
|
17
|
+
return `${resource.type} "${resource.repo}/${resource.name}"`;
|
|
18
|
+
}
|
|
19
|
+
function countActions(resources) {
|
|
20
|
+
return {
|
|
21
|
+
create: resources.filter((r) => r.action === "create").length,
|
|
22
|
+
update: resources.filter((r) => r.action === "update").length,
|
|
23
|
+
delete: resources.filter((r) => r.action === "delete").length,
|
|
24
|
+
skipped: resources.filter((r) => r.action === "skipped").length,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function formatPlanSummaryPlain(counts) {
|
|
28
|
+
const parts = [];
|
|
29
|
+
if (counts.create > 0)
|
|
30
|
+
parts.push(`${counts.create} to create`);
|
|
31
|
+
if (counts.update > 0)
|
|
32
|
+
parts.push(`${counts.update} to change`);
|
|
33
|
+
if (counts.delete > 0)
|
|
34
|
+
parts.push(`${counts.delete} to destroy`);
|
|
35
|
+
if (parts.length === 0) {
|
|
36
|
+
return "No changes";
|
|
37
|
+
}
|
|
38
|
+
return parts.join(", ");
|
|
39
|
+
}
|
|
40
|
+
export function formatPlanMarkdown(plan, options) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
const counts = countActions(plan.resources);
|
|
43
|
+
const changedResources = plan.resources.filter((r) => r.action !== "unchanged");
|
|
44
|
+
// Title
|
|
45
|
+
const titleSuffix = options.dryRun ? " (Dry Run)" : "";
|
|
46
|
+
lines.push(`## ${options.title}${titleSuffix}`);
|
|
47
|
+
lines.push("");
|
|
48
|
+
// Dry-run warning
|
|
49
|
+
if (options.dryRun) {
|
|
50
|
+
lines.push("> [!WARNING]");
|
|
51
|
+
lines.push("> This was a dry run — no changes were applied");
|
|
52
|
+
lines.push("");
|
|
53
|
+
}
|
|
54
|
+
// Plan summary as heading
|
|
55
|
+
const summaryText = formatPlanSummaryPlain(counts);
|
|
56
|
+
lines.push(`### Plan: ${summaryText}`);
|
|
57
|
+
lines.push("");
|
|
58
|
+
// Resource table (if any changes)
|
|
59
|
+
if (changedResources.length > 0) {
|
|
60
|
+
lines.push("<details open>");
|
|
61
|
+
lines.push("<summary><strong>Resources</strong></summary>");
|
|
62
|
+
lines.push("");
|
|
63
|
+
lines.push("| Resource | Action |");
|
|
64
|
+
lines.push("|----------|--------|");
|
|
65
|
+
for (const resource of changedResources) {
|
|
66
|
+
const symbol = getActionSymbol(resource.action);
|
|
67
|
+
const id = formatResourceIdPlain(resource);
|
|
68
|
+
lines.push(`| \`${symbol} ${id}\` | ${resource.action} |`);
|
|
69
|
+
}
|
|
70
|
+
lines.push("");
|
|
71
|
+
lines.push("</details>");
|
|
72
|
+
}
|
|
73
|
+
// Add diff details for resources that have them
|
|
74
|
+
const resourcesWithDiffs = changedResources.filter((r) => r.details?.diff && r.details.diff.length > 0);
|
|
75
|
+
for (const resource of resourcesWithDiffs) {
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push("<details>");
|
|
78
|
+
lines.push(`<summary><strong>Diff: ${formatResourceIdPlain(resource)}</strong></summary>`);
|
|
79
|
+
lines.push("");
|
|
80
|
+
lines.push("```diff");
|
|
81
|
+
for (const diffLine of resource.details.diff) {
|
|
82
|
+
lines.push(diffLine);
|
|
83
|
+
}
|
|
84
|
+
lines.push("```");
|
|
85
|
+
lines.push("");
|
|
86
|
+
lines.push("</details>");
|
|
87
|
+
}
|
|
88
|
+
// Error section
|
|
89
|
+
if (plan.errors && plan.errors.length > 0) {
|
|
90
|
+
lines.push("");
|
|
91
|
+
lines.push("<details open>");
|
|
92
|
+
lines.push("<summary><strong>Errors</strong></summary>");
|
|
93
|
+
lines.push("");
|
|
94
|
+
lines.push("| Repository | Error |");
|
|
95
|
+
lines.push("|------------|-------|");
|
|
96
|
+
for (const error of plan.errors) {
|
|
97
|
+
lines.push(`| ${error.repo} | ${error.message} |`);
|
|
98
|
+
}
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push("</details>");
|
|
101
|
+
}
|
|
102
|
+
return lines.join("\n");
|
|
103
|
+
}
|
|
104
|
+
export function writePlanSummary(plan, options) {
|
|
105
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
106
|
+
if (!summaryPath)
|
|
107
|
+
return;
|
|
108
|
+
const markdown = formatPlanMarkdown(plan, options);
|
|
109
|
+
appendFileSync(summaryPath, "\n" + markdown + "\n");
|
|
110
|
+
}
|
|
@@ -22,11 +22,11 @@ const PROPERTY_MAPPING = {
|
|
|
22
22
|
mergeCommitMessage: "merge_commit_message",
|
|
23
23
|
webCommitSignoffRequired: "web_commit_signoff_required",
|
|
24
24
|
defaultBranch: "default_branch",
|
|
25
|
-
vulnerabilityAlerts: "
|
|
26
|
-
automatedSecurityFixes: "
|
|
25
|
+
vulnerabilityAlerts: "vulnerability_alerts",
|
|
26
|
+
automatedSecurityFixes: "automated_security_fixes",
|
|
27
27
|
secretScanning: "_secret_scanning",
|
|
28
28
|
secretScanningPushProtection: "_secret_scanning_push_protection",
|
|
29
|
-
privateVulnerabilityReporting: "
|
|
29
|
+
privateVulnerabilityReporting: "private_vulnerability_reporting",
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
32
|
* Gets the current value for a property from GitHub API response.
|
|
@@ -91,16 +91,22 @@ export class RepoSettingsProcessor {
|
|
|
91
91
|
}
|
|
92
92
|
async applyChanges(repoInfo, settings, options) {
|
|
93
93
|
// Extract settings that need separate API calls
|
|
94
|
-
const { vulnerabilityAlerts, automatedSecurityFixes, ...mainSettings } = settings;
|
|
94
|
+
const { vulnerabilityAlerts, automatedSecurityFixes, privateVulnerabilityReporting, ...mainSettings } = settings;
|
|
95
95
|
// Update main settings via PATCH /repos
|
|
96
96
|
if (Object.keys(mainSettings).length > 0) {
|
|
97
97
|
await this.strategy.updateSettings(repoInfo, mainSettings, options);
|
|
98
98
|
}
|
|
99
99
|
// Handle vulnerability alerts (separate endpoint)
|
|
100
|
+
// Must be done before automated security fixes
|
|
100
101
|
if (vulnerabilityAlerts !== undefined) {
|
|
101
102
|
await this.strategy.setVulnerabilityAlerts(repoInfo, vulnerabilityAlerts, options);
|
|
102
103
|
}
|
|
104
|
+
// Handle private vulnerability reporting (separate endpoint)
|
|
105
|
+
if (privateVulnerabilityReporting !== undefined) {
|
|
106
|
+
await this.strategy.setPrivateVulnerabilityReporting(repoInfo, privateVulnerabilityReporting, options);
|
|
107
|
+
}
|
|
103
108
|
// Handle automated security fixes (separate endpoint)
|
|
109
|
+
// Done last to ensure vulnerability alerts have been fully processed
|
|
104
110
|
if (automatedSecurityFixes !== undefined) {
|
|
105
111
|
await this.strategy.setAutomatedSecurityFixes(repoInfo, automatedSecurityFixes, options);
|
|
106
112
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Resource } from "./plan-formatter.js";
|
|
2
|
+
import type { RulesetProcessorResult } from "./ruleset-processor.js";
|
|
3
|
+
import type { ProcessorResult } from "./repository-processor.js";
|
|
4
|
+
import type { RepoConfig } from "./config.js";
|
|
5
|
+
/**
|
|
6
|
+
* Convert RulesetProcessorResult planOutput entries to Resource objects.
|
|
7
|
+
*/
|
|
8
|
+
export declare function rulesetResultToResources(repoName: string, result: RulesetProcessorResult): Resource[];
|
|
9
|
+
/**
|
|
10
|
+
* Convert sync ProcessorResult diffStats to Resource objects.
|
|
11
|
+
* Since we don't have per-file details, we represent each file from config
|
|
12
|
+
* with the aggregate action based on diffStats.
|
|
13
|
+
*/
|
|
14
|
+
export declare function syncResultToResources(repoName: string, repoConfig: Pick<RepoConfig, "files">, result: ProcessorResult): Resource[];
|
|
15
|
+
/**
|
|
16
|
+
* Convert repo settings processor planOutput entries to Resource objects.
|
|
17
|
+
*/
|
|
18
|
+
export declare function repoSettingsResultToResources(repoName: string, result: {
|
|
19
|
+
planOutput?: {
|
|
20
|
+
entries?: Array<{
|
|
21
|
+
property: string;
|
|
22
|
+
action: string;
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
25
|
+
}): Resource[];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert RulesetProcessorResult planOutput entries to Resource objects.
|
|
3
|
+
*/
|
|
4
|
+
export function rulesetResultToResources(repoName, result) {
|
|
5
|
+
const resources = [];
|
|
6
|
+
if (result.planOutput?.entries) {
|
|
7
|
+
for (const entry of result.planOutput.entries) {
|
|
8
|
+
let action;
|
|
9
|
+
switch (entry.action) {
|
|
10
|
+
case "create":
|
|
11
|
+
action = "create";
|
|
12
|
+
break;
|
|
13
|
+
case "update":
|
|
14
|
+
action = "update";
|
|
15
|
+
break;
|
|
16
|
+
case "delete":
|
|
17
|
+
action = "delete";
|
|
18
|
+
break;
|
|
19
|
+
default:
|
|
20
|
+
action = "unchanged";
|
|
21
|
+
}
|
|
22
|
+
resources.push({
|
|
23
|
+
type: "ruleset",
|
|
24
|
+
repo: repoName,
|
|
25
|
+
name: entry.name,
|
|
26
|
+
action,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return resources;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Convert sync ProcessorResult diffStats to Resource objects.
|
|
34
|
+
* Since we don't have per-file details, we represent each file from config
|
|
35
|
+
* with the aggregate action based on diffStats.
|
|
36
|
+
*/
|
|
37
|
+
export function syncResultToResources(repoName, repoConfig, result) {
|
|
38
|
+
const resources = [];
|
|
39
|
+
if (result.skipped) {
|
|
40
|
+
// Mark all files as unchanged when skipped
|
|
41
|
+
for (const file of repoConfig.files) {
|
|
42
|
+
resources.push({
|
|
43
|
+
type: "file",
|
|
44
|
+
repo: repoName,
|
|
45
|
+
name: file.fileName,
|
|
46
|
+
action: "unchanged",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return resources;
|
|
50
|
+
}
|
|
51
|
+
if (!result.diffStats) {
|
|
52
|
+
return resources;
|
|
53
|
+
}
|
|
54
|
+
// With aggregate stats, we can show repo-level summary
|
|
55
|
+
// For now, create one resource per file in config with best-effort action
|
|
56
|
+
// Note: This is approximate since we don't have per-file tracking
|
|
57
|
+
const { newCount, modifiedCount, deletedCount } = result.diffStats;
|
|
58
|
+
for (const file of repoConfig.files) {
|
|
59
|
+
// Determine action based on aggregate stats - this is a simplification
|
|
60
|
+
let action = "unchanged";
|
|
61
|
+
if (newCount > 0) {
|
|
62
|
+
action = "create";
|
|
63
|
+
}
|
|
64
|
+
else if (modifiedCount > 0) {
|
|
65
|
+
action = "update";
|
|
66
|
+
}
|
|
67
|
+
else if (deletedCount > 0) {
|
|
68
|
+
action = "delete";
|
|
69
|
+
}
|
|
70
|
+
resources.push({
|
|
71
|
+
type: "file",
|
|
72
|
+
repo: repoName,
|
|
73
|
+
name: file.fileName,
|
|
74
|
+
action,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return resources;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Convert repo settings processor planOutput entries to Resource objects.
|
|
81
|
+
*/
|
|
82
|
+
export function repoSettingsResultToResources(repoName, result) {
|
|
83
|
+
const resources = [];
|
|
84
|
+
if (result.planOutput?.entries) {
|
|
85
|
+
for (const entry of result.planOutput.entries) {
|
|
86
|
+
resources.push({
|
|
87
|
+
type: "setting",
|
|
88
|
+
repo: repoName,
|
|
89
|
+
name: entry.property,
|
|
90
|
+
action: entry.action === "add" ? "create" : "update",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return resources;
|
|
95
|
+
}
|
|
@@ -15,6 +15,10 @@ export declare class GitHubRepoSettingsStrategy implements IRepoSettingsStrategy
|
|
|
15
15
|
updateSettings(repoInfo: RepoInfo, settings: GitHubRepoSettings, options?: RepoSettingsStrategyOptions): Promise<void>;
|
|
16
16
|
setVulnerabilityAlerts(repoInfo: RepoInfo, enable: boolean, options?: RepoSettingsStrategyOptions): Promise<void>;
|
|
17
17
|
setAutomatedSecurityFixes(repoInfo: RepoInfo, enable: boolean, options?: RepoSettingsStrategyOptions): Promise<void>;
|
|
18
|
+
setPrivateVulnerabilityReporting(repoInfo: RepoInfo, enable: boolean, options?: RepoSettingsStrategyOptions): Promise<void>;
|
|
19
|
+
private getVulnerabilityAlerts;
|
|
20
|
+
private getAutomatedSecurityFixes;
|
|
21
|
+
private getPrivateVulnerabilityReporting;
|
|
18
22
|
private validateGitHub;
|
|
19
23
|
private ghApi;
|
|
20
24
|
}
|
|
@@ -76,7 +76,14 @@ export class GitHubRepoSettingsStrategy {
|
|
|
76
76
|
const github = repoInfo;
|
|
77
77
|
const endpoint = `/repos/${github.owner}/${github.repo}`;
|
|
78
78
|
const result = await this.ghApi("GET", endpoint, undefined, options);
|
|
79
|
-
|
|
79
|
+
const settings = JSON.parse(result);
|
|
80
|
+
// Fetch security settings from separate endpoints
|
|
81
|
+
settings.vulnerability_alerts = await this.getVulnerabilityAlerts(github, options);
|
|
82
|
+
// Pass vulnerability_alerts state - automated security fixes requires it enabled
|
|
83
|
+
settings.automated_security_fixes = await this.getAutomatedSecurityFixes(github, options, settings.vulnerability_alerts);
|
|
84
|
+
settings.private_vulnerability_reporting =
|
|
85
|
+
await this.getPrivateVulnerabilityReporting(github, options);
|
|
86
|
+
return settings;
|
|
80
87
|
}
|
|
81
88
|
async updateSettings(repoInfo, settings, options) {
|
|
82
89
|
this.validateGitHub(repoInfo);
|
|
@@ -103,6 +110,54 @@ export class GitHubRepoSettingsStrategy {
|
|
|
103
110
|
const method = enable ? "PUT" : "DELETE";
|
|
104
111
|
await this.ghApi(method, endpoint, undefined, options);
|
|
105
112
|
}
|
|
113
|
+
async setPrivateVulnerabilityReporting(repoInfo, enable, options) {
|
|
114
|
+
this.validateGitHub(repoInfo);
|
|
115
|
+
const github = repoInfo;
|
|
116
|
+
const endpoint = `/repos/${github.owner}/${github.repo}/private-vulnerability-reporting`;
|
|
117
|
+
const method = enable ? "PUT" : "DELETE";
|
|
118
|
+
await this.ghApi(method, endpoint, undefined, options);
|
|
119
|
+
}
|
|
120
|
+
async getVulnerabilityAlerts(github, options) {
|
|
121
|
+
const endpoint = `/repos/${github.owner}/${github.repo}/vulnerability-alerts`;
|
|
122
|
+
try {
|
|
123
|
+
await this.ghApi("GET", endpoint, undefined, options);
|
|
124
|
+
return true; // 204 = enabled
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
128
|
+
if (message.includes("HTTP 404")) {
|
|
129
|
+
return false; // 404 = disabled
|
|
130
|
+
}
|
|
131
|
+
throw error; // Re-throw other errors
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async getAutomatedSecurityFixes(github, options, _vulnerabilityAlertsEnabled) {
|
|
135
|
+
// Note: GitHub returns JSON with {enabled: boolean} for this endpoint
|
|
136
|
+
const endpoint = `/repos/${github.owner}/${github.repo}/automated-security-fixes`;
|
|
137
|
+
try {
|
|
138
|
+
const result = await this.ghApi("GET", endpoint, undefined, options);
|
|
139
|
+
// Parse JSON response - GitHub returns {"enabled": true/false}
|
|
140
|
+
if (result) {
|
|
141
|
+
const data = JSON.parse(result);
|
|
142
|
+
return data.enabled === true;
|
|
143
|
+
}
|
|
144
|
+
// Empty response (204) means enabled
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
149
|
+
if (message.includes("HTTP 404")) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async getPrivateVulnerabilityReporting(github, options) {
|
|
156
|
+
const endpoint = `/repos/${github.owner}/${github.repo}/private-vulnerability-reporting`;
|
|
157
|
+
const result = await this.ghApi("GET", endpoint, undefined, options);
|
|
158
|
+
const data = JSON.parse(result);
|
|
159
|
+
return data.enabled === true;
|
|
160
|
+
}
|
|
106
161
|
validateGitHub(repoInfo) {
|
|
107
162
|
if (!isGitHubRepo(repoInfo)) {
|
|
108
163
|
throw new Error(`GitHub Repo Settings strategy requires GitHub repositories. Got: ${repoInfo.type}`);
|
|
@@ -39,6 +39,9 @@ export interface CurrentRepoSettings {
|
|
|
39
39
|
status: string;
|
|
40
40
|
};
|
|
41
41
|
};
|
|
42
|
+
vulnerability_alerts?: boolean;
|
|
43
|
+
automated_security_fixes?: boolean;
|
|
44
|
+
private_vulnerability_reporting?: boolean;
|
|
42
45
|
}
|
|
43
46
|
export interface IRepoSettingsStrategy {
|
|
44
47
|
/**
|
|
@@ -57,6 +60,10 @@ export interface IRepoSettingsStrategy {
|
|
|
57
60
|
* Enables or disables automated security fixes.
|
|
58
61
|
*/
|
|
59
62
|
setAutomatedSecurityFixes(repoInfo: RepoInfo, enable: boolean, options?: RepoSettingsStrategyOptions): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Enables or disables private vulnerability reporting.
|
|
65
|
+
*/
|
|
66
|
+
setPrivateVulnerabilityReporting(repoInfo: RepoInfo, enable: boolean, options?: RepoSettingsStrategyOptions): Promise<void>;
|
|
60
67
|
}
|
|
61
68
|
/**
|
|
62
69
|
* Type guard to check if an object implements IRepoSettingsStrategy.
|
|
@@ -9,5 +9,6 @@ export function isRepoSettingsStrategy(obj) {
|
|
|
9
9
|
return (typeof strategy.getSettings === "function" &&
|
|
10
10
|
typeof strategy.updateSettings === "function" &&
|
|
11
11
|
typeof strategy.setVulnerabilityAlerts === "function" &&
|
|
12
|
-
typeof strategy.setAutomatedSecurityFixes === "function"
|
|
12
|
+
typeof strategy.setAutomatedSecurityFixes === "function" &&
|
|
13
|
+
typeof strategy.setPrivateVulnerabilityReporting === "function");
|
|
13
14
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aspruyt/xfg",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.4",
|
|
4
4
|
"description": "CLI tool for repository-as-code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"start": "node dist/cli.js",
|
|
28
28
|
"dev": "ts-node src/cli.ts",
|
|
29
29
|
"test": "node --import tsx scripts/run-tests.js",
|
|
30
|
-
"test:coverage": "c8 --check-coverage --lines 95 --reporter=text --reporter=lcov --all --src=src --exclude='test/**/*.test.ts' --exclude='scripts/**' npm test",
|
|
30
|
+
"test:coverage": "c8 --check-coverage --lines 95 --reporter=text --reporter=lcov --all --src=src --exclude='test/**/*.test.ts' --exclude='scripts/**' --exclude='src/strategies/commit-strategy.ts' --exclude='src/strategies/ruleset-strategy.ts' --exclude='test/mocks/**' npm test",
|
|
31
31
|
"test:integration:github": "npm run build && node --import tsx --test test/integration/github.test.ts",
|
|
32
32
|
"test:integration:ado": "npm run build && node --import tsx --test test/integration/ado.test.ts",
|
|
33
33
|
"test:integration:gitlab": "npm run build && node --import tsx --test test/integration/gitlab.test.ts",
|