@aspruyt/xfg 3.1.5 → 3.3.2
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/command-executor.js +11 -2
- package/dist/config-normalizer.js +52 -4
- package/dist/config-validator.d.ts +1 -1
- package/dist/config-validator.js +31 -2
- package/dist/config.d.ts +6 -2
- package/dist/index.js +3 -7
- package/dist/retry-utils.js +2 -1
- package/dist/sanitize-utils.d.ts +8 -0
- package/dist/sanitize-utils.js +20 -0
- package/dist/strategies/azure-pr-strategy.js +2 -1
- package/dist/strategies/github-pr-strategy.js +2 -1
- package/dist/strategies/gitlab-pr-strategy.js +2 -1
- package/package.json +1 -1
package/dist/command-executor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
+
import { sanitizeCredentials } from "./sanitize-utils.js";
|
|
2
3
|
/**
|
|
3
4
|
* Default implementation that uses Node.js child_process.execSync.
|
|
4
5
|
* Note: Commands are escaped using escapeShellArg before being passed here.
|
|
@@ -18,9 +19,17 @@ export class ShellCommandExecutor {
|
|
|
18
19
|
if (execError.stderr && typeof execError.stderr !== "string") {
|
|
19
20
|
execError.stderr = execError.stderr.toString();
|
|
20
21
|
}
|
|
21
|
-
//
|
|
22
|
+
// Sanitize credentials from stderr before including in error
|
|
23
|
+
if (execError.stderr) {
|
|
24
|
+
execError.stderr = sanitizeCredentials(execError.stderr);
|
|
25
|
+
}
|
|
26
|
+
// Include sanitized stderr in error message for better debugging
|
|
22
27
|
if (execError.stderr && execError.message) {
|
|
23
|
-
execError.message =
|
|
28
|
+
execError.message =
|
|
29
|
+
sanitizeCredentials(execError.message) + "\n" + execError.stderr;
|
|
30
|
+
}
|
|
31
|
+
else if (execError.message) {
|
|
32
|
+
execError.message = sanitizeCredentials(execError.message);
|
|
24
33
|
}
|
|
25
34
|
throw error;
|
|
26
35
|
}
|
|
@@ -60,14 +60,30 @@ export function mergeSettings(root, perRepo) {
|
|
|
60
60
|
// Merge rulesets by name - each ruleset is deep merged
|
|
61
61
|
const rootRulesets = root?.rulesets ?? {};
|
|
62
62
|
const repoRulesets = perRepo?.rulesets ?? {};
|
|
63
|
+
// Check if repo opts out of all inherited rulesets
|
|
64
|
+
const inheritRulesets = repoRulesets?.inherit !== false;
|
|
63
65
|
const allRulesetNames = new Set([
|
|
64
|
-
...Object.keys(rootRulesets),
|
|
65
|
-
...Object.keys(repoRulesets),
|
|
66
|
+
...Object.keys(rootRulesets).filter((name) => name !== "inherit"),
|
|
67
|
+
...Object.keys(repoRulesets).filter((name) => name !== "inherit"),
|
|
66
68
|
]);
|
|
67
69
|
if (allRulesetNames.size > 0) {
|
|
68
70
|
result.rulesets = {};
|
|
69
71
|
for (const name of allRulesetNames) {
|
|
70
|
-
|
|
72
|
+
const rootRuleset = rootRulesets[name];
|
|
73
|
+
const repoRuleset = repoRulesets[name];
|
|
74
|
+
// Skip if repo explicitly opts out of this ruleset
|
|
75
|
+
if (repoRuleset === false) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Skip root rulesets if inherit: false (unless repo has override)
|
|
79
|
+
if (!inheritRulesets && !repoRuleset && rootRuleset) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
result.rulesets[name] = mergeRuleset(rootRuleset, repoRuleset);
|
|
83
|
+
}
|
|
84
|
+
// Clean up empty rulesets object
|
|
85
|
+
if (Object.keys(result.rulesets).length === 0) {
|
|
86
|
+
delete result.rulesets;
|
|
71
87
|
}
|
|
72
88
|
}
|
|
73
89
|
// deleteOrphaned: per-repo overrides root
|
|
@@ -89,13 +105,23 @@ export function normalizeConfig(raw) {
|
|
|
89
105
|
const gitUrls = Array.isArray(rawRepo.git) ? rawRepo.git : [rawRepo.git];
|
|
90
106
|
for (const gitUrl of gitUrls) {
|
|
91
107
|
const files = [];
|
|
108
|
+
// Check if repo opts out of all inherited files
|
|
109
|
+
const inheritFiles = rawRepo.files?.inherit !==
|
|
110
|
+
false;
|
|
92
111
|
// Step 2: Process each file definition
|
|
93
112
|
for (const fileName of fileNames) {
|
|
113
|
+
// Skip reserved key
|
|
114
|
+
if (fileName === "inherit")
|
|
115
|
+
continue;
|
|
94
116
|
const repoOverride = rawRepo.files?.[fileName];
|
|
95
117
|
// Skip excluded files (set to false)
|
|
96
118
|
if (repoOverride === false) {
|
|
97
119
|
continue;
|
|
98
120
|
}
|
|
121
|
+
// Skip if inherit: false and no repo-specific override
|
|
122
|
+
if (!inheritFiles && !repoOverride) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
99
125
|
const fileConfig = raw.files[fileName];
|
|
100
126
|
const fileStrategy = fileConfig.mergeStrategy ?? "replace";
|
|
101
127
|
// Step 3: Compute merged content for this file
|
|
@@ -190,12 +216,34 @@ export function normalizeConfig(raw) {
|
|
|
190
216
|
});
|
|
191
217
|
}
|
|
192
218
|
}
|
|
219
|
+
// Normalize root settings (filter out inherit key if present)
|
|
220
|
+
let normalizedRootSettings;
|
|
221
|
+
if (raw.settings) {
|
|
222
|
+
normalizedRootSettings = {};
|
|
223
|
+
if (raw.settings.rulesets) {
|
|
224
|
+
const filteredRulesets = {};
|
|
225
|
+
for (const [name, ruleset] of Object.entries(raw.settings.rulesets)) {
|
|
226
|
+
if (name === "inherit" || ruleset === false)
|
|
227
|
+
continue;
|
|
228
|
+
filteredRulesets[name] = ruleset;
|
|
229
|
+
}
|
|
230
|
+
if (Object.keys(filteredRulesets).length > 0) {
|
|
231
|
+
normalizedRootSettings.rulesets = filteredRulesets;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (raw.settings.deleteOrphaned !== undefined) {
|
|
235
|
+
normalizedRootSettings.deleteOrphaned = raw.settings.deleteOrphaned;
|
|
236
|
+
}
|
|
237
|
+
if (Object.keys(normalizedRootSettings).length === 0) {
|
|
238
|
+
normalizedRootSettings = undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
193
241
|
return {
|
|
194
242
|
id: raw.id,
|
|
195
243
|
repos: expandedRepos,
|
|
196
244
|
prTemplate: raw.prTemplate,
|
|
197
245
|
githubHosts: raw.githubHosts,
|
|
198
246
|
deleteOrphaned: raw.deleteOrphaned,
|
|
199
|
-
settings:
|
|
247
|
+
settings: normalizedRootSettings,
|
|
200
248
|
};
|
|
201
249
|
}
|
|
@@ -7,7 +7,7 @@ export declare function validateRawConfig(config: RawConfig): void;
|
|
|
7
7
|
/**
|
|
8
8
|
* Validates settings object containing rulesets.
|
|
9
9
|
*/
|
|
10
|
-
export declare function validateSettings(settings: unknown, context: string): void;
|
|
10
|
+
export declare function validateSettings(settings: unknown, context: string, rootRulesetNames?: string[]): void;
|
|
11
11
|
/**
|
|
12
12
|
* Validates that config is suitable for the sync command.
|
|
13
13
|
* @throws Error if files section is missing or empty
|
package/dist/config-validator.js
CHANGED
|
@@ -49,6 +49,10 @@ export function validateRawConfig(config) {
|
|
|
49
49
|
"Use 'files' to sync configuration files, or 'settings' to manage repository settings.");
|
|
50
50
|
}
|
|
51
51
|
const fileNames = hasFiles ? Object.keys(config.files) : [];
|
|
52
|
+
// Check for reserved key 'inherit' at root files level
|
|
53
|
+
if (hasFiles && "inherit" in config.files) {
|
|
54
|
+
throw new Error("'inherit' is a reserved key and cannot be used as a filename");
|
|
55
|
+
}
|
|
52
56
|
// Validate each file definition
|
|
53
57
|
for (const fileName of fileNames) {
|
|
54
58
|
validateFileName(fileName);
|
|
@@ -127,6 +131,10 @@ export function validateRawConfig(config) {
|
|
|
127
131
|
// Validate root settings
|
|
128
132
|
if (config.settings !== undefined) {
|
|
129
133
|
validateSettings(config.settings, "Root");
|
|
134
|
+
// Check for reserved key 'inherit' at root rulesets level
|
|
135
|
+
if (config.settings.rulesets && "inherit" in config.settings.rulesets) {
|
|
136
|
+
throw new Error("'inherit' is a reserved key and cannot be used as a ruleset name");
|
|
137
|
+
}
|
|
130
138
|
}
|
|
131
139
|
// Validate githubHosts if provided
|
|
132
140
|
if (config.githubHosts !== undefined) {
|
|
@@ -161,6 +169,14 @@ export function validateRawConfig(config) {
|
|
|
161
169
|
throw new Error(`Repo at index ${i}: files must be an object`);
|
|
162
170
|
}
|
|
163
171
|
for (const fileName of Object.keys(repo.files)) {
|
|
172
|
+
// Skip reserved key 'inherit'
|
|
173
|
+
if (fileName === "inherit") {
|
|
174
|
+
const inheritValue = repo.files.inherit;
|
|
175
|
+
if (typeof inheritValue !== "boolean") {
|
|
176
|
+
throw new Error(`Repo at index ${i}: files.inherit must be a boolean`);
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
164
180
|
// Ensure the file is defined at root level
|
|
165
181
|
if (!config.files || !config.files[fileName]) {
|
|
166
182
|
throw new Error(`Repo at index ${i} references undefined file '${fileName}'. File must be defined in root 'files' object.`);
|
|
@@ -233,7 +249,10 @@ export function validateRawConfig(config) {
|
|
|
233
249
|
}
|
|
234
250
|
// Validate per-repo settings
|
|
235
251
|
if (repo.settings !== undefined) {
|
|
236
|
-
|
|
252
|
+
const rootRulesetNames = config.settings?.rulesets
|
|
253
|
+
? Object.keys(config.settings.rulesets).filter((k) => k !== "inherit")
|
|
254
|
+
: [];
|
|
255
|
+
validateSettings(repo.settings, `Repo ${getGitDisplayName(repo.git)}`, rootRulesetNames);
|
|
237
256
|
}
|
|
238
257
|
}
|
|
239
258
|
}
|
|
@@ -465,7 +484,7 @@ function validateRuleset(ruleset, name, context) {
|
|
|
465
484
|
/**
|
|
466
485
|
* Validates settings object containing rulesets.
|
|
467
486
|
*/
|
|
468
|
-
export function validateSettings(settings, context) {
|
|
487
|
+
export function validateSettings(settings, context, rootRulesetNames) {
|
|
469
488
|
if (typeof settings !== "object" ||
|
|
470
489
|
settings === null ||
|
|
471
490
|
Array.isArray(settings)) {
|
|
@@ -480,6 +499,16 @@ export function validateSettings(settings, context) {
|
|
|
480
499
|
}
|
|
481
500
|
const rulesets = s.rulesets;
|
|
482
501
|
for (const [name, ruleset] of Object.entries(rulesets)) {
|
|
502
|
+
// Skip reserved key
|
|
503
|
+
if (name === "inherit")
|
|
504
|
+
continue;
|
|
505
|
+
// Check for opt-out of non-existent root ruleset
|
|
506
|
+
if (ruleset === false) {
|
|
507
|
+
if (rootRulesetNames && !rootRulesetNames.includes(name)) {
|
|
508
|
+
throw new Error(`${context}: Cannot opt out of '${name}' - not defined in root settings.rulesets`);
|
|
509
|
+
}
|
|
510
|
+
continue; // Skip further validation for false entries
|
|
511
|
+
}
|
|
483
512
|
validateRuleset(ruleset, name, context);
|
|
484
513
|
}
|
|
485
514
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -239,12 +239,16 @@ export interface RawRepoFileOverride {
|
|
|
239
239
|
deleteOrphaned?: boolean;
|
|
240
240
|
}
|
|
241
241
|
export interface RawRepoSettings {
|
|
242
|
-
rulesets?: Record<string, Ruleset
|
|
242
|
+
rulesets?: Record<string, Ruleset | false> & {
|
|
243
|
+
inherit?: boolean;
|
|
244
|
+
};
|
|
243
245
|
deleteOrphaned?: boolean;
|
|
244
246
|
}
|
|
245
247
|
export interface RawRepoConfig {
|
|
246
248
|
git: string | string[];
|
|
247
|
-
files?: Record<string, RawRepoFileOverride | false
|
|
249
|
+
files?: Record<string, RawRepoFileOverride | false> & {
|
|
250
|
+
inherit?: boolean;
|
|
251
|
+
};
|
|
248
252
|
prOptions?: PRMergeOptions;
|
|
249
253
|
settings?: RawRepoSettings;
|
|
250
254
|
}
|
package/dist/index.js
CHANGED
|
@@ -208,19 +208,15 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
208
208
|
return;
|
|
209
209
|
}
|
|
210
210
|
console.log(`Found ${reposWithRulesets.length} repositories with rulesets\n`);
|
|
211
|
+
logger.setTotal(reposWithRulesets.length);
|
|
211
212
|
const processor = processorFactory();
|
|
212
213
|
const repoProcessor = repoProcessorFactory();
|
|
213
214
|
const results = [];
|
|
214
215
|
let successCount = 0;
|
|
215
216
|
let failCount = 0;
|
|
216
217
|
let skipCount = 0;
|
|
217
|
-
for (let i = 0; i <
|
|
218
|
-
const repoConfig =
|
|
219
|
-
// Skip repos without rulesets
|
|
220
|
-
if (!repoConfig.settings?.rulesets ||
|
|
221
|
-
Object.keys(repoConfig.settings.rulesets).length === 0) {
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
218
|
+
for (let i = 0; i < reposWithRulesets.length; i++) {
|
|
219
|
+
const repoConfig = reposWithRulesets[i];
|
|
224
220
|
let repoInfo;
|
|
225
221
|
try {
|
|
226
222
|
repoInfo = parseGitUrl(repoConfig.git, {
|
package/dist/retry-utils.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pRetry, { AbortError } from "p-retry";
|
|
2
2
|
import { logger } from "./logger.js";
|
|
3
|
+
import { sanitizeCredentials } from "./sanitize-utils.js";
|
|
3
4
|
/**
|
|
4
5
|
* Default patterns indicating permanent errors that should NOT be retried.
|
|
5
6
|
* These typically indicate configuration issues, auth failures, or invalid resources.
|
|
@@ -121,7 +122,7 @@ export async function withRetry(fn, options) {
|
|
|
121
122
|
onFailedAttempt: (context) => {
|
|
122
123
|
// Only log if this isn't the last attempt
|
|
123
124
|
if (context.retriesLeft > 0) {
|
|
124
|
-
const msg = context.error.message || "Unknown error";
|
|
125
|
+
const msg = sanitizeCredentials(context.error.message) || "Unknown error";
|
|
125
126
|
logger.info(`Attempt ${context.attemptNumber}/${retries + 1} failed: ${msg}. Retrying...`);
|
|
126
127
|
options?.onRetry?.(context.error, context.attemptNumber);
|
|
127
128
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitizes credentials from error messages and logs.
|
|
3
|
+
* Replaces sensitive tokens/passwords with '***' to prevent leakage.
|
|
4
|
+
*
|
|
5
|
+
* @param message The message that may contain credentials
|
|
6
|
+
* @returns The sanitized message with credentials replaced by '***'
|
|
7
|
+
*/
|
|
8
|
+
export declare function sanitizeCredentials(message: string | undefined | null): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitizes credentials from error messages and logs.
|
|
3
|
+
* Replaces sensitive tokens/passwords with '***' to prevent leakage.
|
|
4
|
+
*
|
|
5
|
+
* @param message The message that may contain credentials
|
|
6
|
+
* @returns The sanitized message with credentials replaced by '***'
|
|
7
|
+
*/
|
|
8
|
+
export function sanitizeCredentials(message) {
|
|
9
|
+
if (!message) {
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
let result = message;
|
|
13
|
+
// Handle URL credentials (most common case)
|
|
14
|
+
// Replace password portion in https://user:password@host patterns
|
|
15
|
+
result = result.replace(/(https:\/\/[^:]+:)([^@]+)(@)/g, "$1***$3");
|
|
16
|
+
// Handle Authorization headers
|
|
17
|
+
result = result.replace(/(Authorization:\s*Bearer\s+)(\S+)/gi, "$1***");
|
|
18
|
+
result = result.replace(/(Authorization:\s*Basic\s+)(\S+)/gi, "$1***");
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
@@ -5,6 +5,7 @@ import { isAzureDevOpsRepo } from "../repo-detector.js";
|
|
|
5
5
|
import { BasePRStrategy, } from "./pr-strategy.js";
|
|
6
6
|
import { logger } from "../logger.js";
|
|
7
7
|
import { withRetry, isPermanentError } from "../retry-utils.js";
|
|
8
|
+
import { sanitizeCredentials } from "../sanitize-utils.js";
|
|
8
9
|
export class AzurePRStrategy extends BasePRStrategy {
|
|
9
10
|
constructor(executor) {
|
|
10
11
|
super(executor);
|
|
@@ -35,7 +36,7 @@ export class AzurePRStrategy extends BasePRStrategy {
|
|
|
35
36
|
}
|
|
36
37
|
const stderr = error.stderr ?? "";
|
|
37
38
|
if (stderr && !stderr.includes("does not exist")) {
|
|
38
|
-
logger.info(`Debug: Azure PR check failed - ${stderr.trim()}`);
|
|
39
|
+
logger.info(`Debug: Azure PR check failed - ${sanitizeCredentials(stderr).trim()}`);
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
return null;
|
|
@@ -5,6 +5,7 @@ import { isGitHubRepo } from "../repo-detector.js";
|
|
|
5
5
|
import { BasePRStrategy, } from "./pr-strategy.js";
|
|
6
6
|
import { logger } from "../logger.js";
|
|
7
7
|
import { withRetry, isPermanentError } from "../retry-utils.js";
|
|
8
|
+
import { sanitizeCredentials } from "../sanitize-utils.js";
|
|
8
9
|
/**
|
|
9
10
|
* Get the repo flag value for gh CLI commands.
|
|
10
11
|
* Returns HOST/OWNER/REPO for GHE, OWNER/REPO for github.com.
|
|
@@ -66,7 +67,7 @@ export class GitHubPRStrategy extends BasePRStrategy {
|
|
|
66
67
|
// Log unexpected errors for debugging (expected: empty result means no PR)
|
|
67
68
|
const stderr = error.stderr ?? "";
|
|
68
69
|
if (stderr && !stderr.includes("no pull requests match")) {
|
|
69
|
-
logger.info(`Debug: GitHub PR check failed - ${stderr.trim()}`);
|
|
70
|
+
logger.info(`Debug: GitHub PR check failed - ${sanitizeCredentials(stderr).trim()}`);
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
return null;
|
|
@@ -5,6 +5,7 @@ import { isGitLabRepo } from "../repo-detector.js";
|
|
|
5
5
|
import { BasePRStrategy, } from "./pr-strategy.js";
|
|
6
6
|
import { logger } from "../logger.js";
|
|
7
7
|
import { withRetry, isPermanentError } from "../retry-utils.js";
|
|
8
|
+
import { sanitizeCredentials } from "../sanitize-utils.js";
|
|
8
9
|
export class GitLabPRStrategy extends BasePRStrategy {
|
|
9
10
|
constructor(executor) {
|
|
10
11
|
super(executor);
|
|
@@ -89,7 +90,7 @@ export class GitLabPRStrategy extends BasePRStrategy {
|
|
|
89
90
|
// Log unexpected errors for debugging
|
|
90
91
|
const stderr = error.stderr ?? "";
|
|
91
92
|
if (stderr && !stderr.includes("no merge requests")) {
|
|
92
|
-
logger.info(`Debug: GitLab MR check failed - ${stderr.trim()}`);
|
|
93
|
+
logger.info(`Debug: GitLab MR check failed - ${sanitizeCredentials(stderr).trim()}`);
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
return null;
|