@aspruyt/xfg 6.0.3 → 6.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/dist/cli/lifecycle-report-builder.d.ts +2 -2
- package/dist/cli/lifecycle-report-builder.js +3 -11
- package/dist/cli/program.d.ts +2 -1
- package/dist/cli/program.js +2 -3
- package/dist/cli/repo-sync-runner.d.ts +24 -0
- package/dist/cli/repo-sync-runner.js +156 -0
- package/dist/cli/results-collector.d.ts +1 -1
- package/dist/cli/results-collector.js +2 -2
- package/dist/cli/settings-factories.d.ts +7 -0
- package/dist/cli/settings-factories.js +27 -0
- package/dist/cli/settings-report-builder.d.ts +1 -1
- package/dist/cli/settings-report-builder.js +12 -23
- package/dist/cli/settings-runner.d.ts +2 -0
- package/dist/cli/settings-runner.js +87 -0
- package/dist/cli/sync-command.d.ts +1 -1
- package/dist/cli/sync-command.js +31 -372
- package/dist/cli/sync-report-builder.d.ts +1 -1
- package/dist/cli/sync-utils.d.ts +8 -0
- package/dist/cli/sync-utils.js +36 -0
- package/dist/cli/types.d.ts +5 -7
- package/dist/cli/unified-summary.d.ts +1 -3
- package/dist/cli/unified-summary.js +7 -5
- package/dist/cli.js +2 -1
- package/dist/{shared → config}/env.js +2 -2
- package/dist/config/extends-resolver.js +4 -3
- package/dist/config/file-reference-resolver.js +4 -2
- package/dist/config/formatter.js +18 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/loader.js +30 -6
- package/dist/config/merge.d.ts +11 -1
- package/dist/config/merge.js +78 -6
- package/dist/config/normalizer.js +53 -38
- package/dist/config/validator.d.ts +1 -4
- package/dist/config/validator.js +13 -599
- package/dist/config/validators/file-validator.d.ts +2 -1
- package/dist/config/validators/file-validator.js +9 -1
- package/dist/config/validators/group-validator.d.ts +3 -0
- package/dist/config/validators/group-validator.js +167 -0
- package/dist/config/validators/repo-entry-validator.d.ts +2 -0
- package/dist/config/validators/repo-entry-validator.js +165 -0
- package/dist/config/validators/repo-settings-validator.js +18 -7
- package/dist/config/validators/ruleset-validator.js +2 -5
- package/dist/config/validators/shared.d.ts +11 -0
- package/dist/config/validators/shared.js +242 -0
- package/dist/lifecycle/ado-migration-source.js +2 -4
- package/dist/lifecycle/github-lifecycle-provider.d.ts +7 -11
- package/dist/lifecycle/github-lifecycle-provider.js +125 -136
- package/dist/lifecycle/{lifecycle-helpers.d.ts → helpers.d.ts} +5 -1
- package/dist/lifecycle/{lifecycle-helpers.js → helpers.js} +9 -8
- package/dist/lifecycle/index.d.ts +2 -2
- package/dist/lifecycle/index.js +1 -1
- package/dist/lifecycle/repo-lifecycle-factory.d.ts +2 -2
- package/dist/output/github-summary.js +2 -3
- package/dist/output/index.d.ts +4 -0
- package/dist/output/index.js +4 -0
- package/dist/output/lifecycle-report.d.ts +1 -1
- package/dist/output/lifecycle-report.js +5 -0
- package/dist/output/sync-report.d.ts +25 -3
- package/dist/output/sync-report.js +11 -11
- package/dist/settings/base-processor.d.ts +18 -7
- package/dist/settings/base-processor.js +26 -5
- package/dist/settings/code-scanning/diff.js +2 -2
- package/dist/settings/code-scanning/formatter.d.ts +2 -6
- package/dist/settings/code-scanning/formatter.js +2 -25
- package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +3 -7
- package/dist/settings/code-scanning/github-code-scanning-strategy.js +2 -2
- package/dist/settings/code-scanning/processor.js +6 -4
- package/dist/settings/code-scanning/types.d.ts +10 -8
- package/dist/settings/labels/github-labels-strategy.d.ts +3 -11
- package/dist/settings/labels/types.d.ts +12 -10
- package/dist/settings/repo-settings/diff.d.ts +1 -1
- package/dist/settings/repo-settings/diff.js +1 -1
- package/dist/settings/repo-settings/formatter.d.ts +2 -6
- package/dist/settings/repo-settings/formatter.js +4 -23
- package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -2
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +8 -7
- package/dist/settings/repo-settings/processor.js +11 -11
- package/dist/settings/repo-settings/types.d.ts +2 -2
- package/dist/settings/rulesets/diff-algorithm.js +4 -2
- package/dist/settings/rulesets/diff.js +2 -51
- package/dist/settings/rulesets/formatter.js +4 -0
- package/dist/settings/rulesets/github-ruleset-strategy.d.ts +3 -3
- package/dist/settings/rulesets/github-ruleset-strategy.js +4 -6
- package/dist/settings/rulesets/index.d.ts +1 -1
- package/dist/settings/rulesets/index.js +0 -2
- package/dist/settings/rulesets/processor.js +1 -1
- package/dist/settings/rulesets/types.d.ts +6 -2
- package/dist/shared/command-executor.d.ts +4 -4
- package/dist/shared/command-executor.js +9 -7
- package/dist/shared/diff-format.d.ts +1 -0
- package/dist/shared/diff-format.js +10 -0
- package/dist/shared/errors.d.ts +7 -4
- package/dist/shared/errors.js +8 -8
- package/dist/shared/gh-api-utils.d.ts +3 -34
- package/dist/shared/gh-api-utils.js +23 -53
- package/dist/shared/gh-token-utils.d.ts +26 -0
- package/dist/shared/gh-token-utils.js +32 -0
- package/dist/shared/json-utils.js +1 -1
- package/dist/shared/regex-utils.d.ts +1 -0
- package/dist/shared/regex-utils.js +3 -0
- package/dist/shared/retry-utils.d.ts +1 -0
- package/dist/shared/retry-utils.js +13 -7
- package/dist/sync/auth-options-builder.js +1 -1
- package/dist/sync/branch-manager.js +5 -3
- package/dist/sync/commit-push-manager.js +2 -3
- package/dist/sync/diff-utils.d.ts +0 -1
- package/dist/sync/diff-utils.js +5 -10
- package/dist/sync/file-sync-orchestrator.js +0 -2
- package/dist/sync/file-writer.d.ts +3 -0
- package/dist/sync/file-writer.js +84 -81
- package/dist/sync/index.d.ts +0 -1
- package/dist/sync/index.js +0 -1
- package/dist/sync/manifest.js +1 -1
- package/dist/sync/pr-merge-handler.js +6 -6
- package/dist/sync/sync-workflow.js +1 -1
- package/dist/sync/types.d.ts +2 -2
- package/dist/vcs/ado-pr-strategy.d.ts +3 -5
- package/dist/vcs/ado-pr-strategy.js +131 -33
- package/dist/vcs/authenticated-git-ops.js +45 -23
- package/dist/vcs/git-commit-strategy.js +10 -6
- package/dist/vcs/git-ops.js +30 -24
- package/dist/vcs/github-pr-strategy.d.ts +3 -2
- package/dist/vcs/github-pr-strategy.js +80 -30
- package/dist/vcs/gitlab-pr-strategy.d.ts +2 -5
- package/dist/vcs/gitlab-pr-strategy.js +88 -87
- package/dist/vcs/graphql-commit-strategy.d.ts +1 -5
- package/dist/vcs/graphql-commit-strategy.js +21 -37
- package/dist/vcs/pr-creator.js +9 -2
- package/dist/vcs/pr-strategy.d.ts +2 -3
- package/dist/vcs/pr-strategy.js +0 -1
- package/dist/vcs/types.d.ts +9 -5
- package/package.json +5 -5
- package/dist/config/validators/index.d.ts +0 -3
- package/dist/config/validators/index.js +0 -6
- package/dist/output/types.d.ts +0 -20
- package/dist/output/types.js +0 -1
- package/dist/shared/shell-utils.d.ts +0 -6
- package/dist/shared/shell-utils.js +0 -17
- /package/dist/{shared → config}/env.d.ts +0 -0
- /package/dist/lifecycle/{lifecycle-formatter.d.ts → formatter.d.ts} +0 -0
- /package/dist/lifecycle/{lifecycle-formatter.js → formatter.js} +0 -0
- /package/dist/{vcs → shared}/sanitize-utils.d.ts +0 -0
- /package/dist/{vcs → shared}/sanitize-utils.js +0 -0
package/dist/config/formatter.js
CHANGED
|
@@ -42,6 +42,11 @@ function buildCommentOnlyYaml(header, schemaUrl) {
|
|
|
42
42
|
return undefined;
|
|
43
43
|
return lines.join("\n") + "\n";
|
|
44
44
|
}
|
|
45
|
+
function buildJson5HeaderComment(header) {
|
|
46
|
+
if (!header || header.length === 0)
|
|
47
|
+
return undefined;
|
|
48
|
+
return header.map((h) => `// ${h}`).join("\n") + "\n";
|
|
49
|
+
}
|
|
45
50
|
/**
|
|
46
51
|
* Converts content to string in the appropriate format.
|
|
47
52
|
* Handles null content (empty files), text content (string/string[]), and object content (JSON/YAML).
|
|
@@ -55,6 +60,12 @@ export function convertContentToString(content, fileName, options) {
|
|
|
55
60
|
return commentOnly;
|
|
56
61
|
}
|
|
57
62
|
}
|
|
63
|
+
if (format === "json5" && options) {
|
|
64
|
+
const commentOnly = buildJson5HeaderComment(options.header);
|
|
65
|
+
if (commentOnly) {
|
|
66
|
+
return commentOnly;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
58
69
|
return "";
|
|
59
70
|
}
|
|
60
71
|
if (typeof content === "string") {
|
|
@@ -92,8 +103,14 @@ export function convertContentToString(content, fileName, options) {
|
|
|
92
103
|
indent: 2,
|
|
93
104
|
defaultStringType: "QUOTE_DOUBLE",
|
|
94
105
|
defaultKeyType: "PLAIN",
|
|
106
|
+
lineWidth: 0,
|
|
95
107
|
});
|
|
96
108
|
}
|
|
97
|
-
|
|
109
|
+
if (format === "json5" && options) {
|
|
110
|
+
const headerComment = buildJson5HeaderComment(options.header);
|
|
111
|
+
if (headerComment) {
|
|
112
|
+
return headerComment + JSON.stringify(content, null, 2) + "\n";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
98
115
|
return JSON.stringify(content, null, 2) + "\n";
|
|
99
116
|
}
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { MergeMode, MergeStrategy, BypassActor, StatusCheckConfig, CodeScanningTool, PullRequestRuleParameters, RulesetRule, Ruleset, GitHubRepoSettings, RepoVisibility, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage, Label, CodeScanningSettings, CodeScanningState, CodeScanningQuerySuite, CodeScanningLanguage, RepoSettings, RawFileConfig, RawRepoFileOverride, RawGroupConfig, RawRepoSettings, RawRepoConfig, RawConfig, RawConditionalGroupWhen, RawConditionalGroupConfig, RepoConfig, Config, FileContent, ContentValue, } from "./types.js";
|
|
1
|
+
export type { PRMergeOptions, MergeMode, MergeStrategy, BypassActor, StatusCheckConfig, CodeScanningTool, PullRequestRuleParameters, RulesetRule, Ruleset, GitHubRepoSettings, RepoVisibility, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage, Label, CodeScanningSettings, CodeScanningState, CodeScanningQuerySuite, CodeScanningLanguage, RepoSettings, RawFileConfig, RawRepoFileOverride, RawGroupConfig, RawRepoSettings, RawRepoConfig, RawConfig, RawConditionalGroupWhen, RawConditionalGroupConfig, RepoConfig, Config, FileContent, ContentValue, } from "./types.js";
|
|
2
2
|
export { RULESET_COMPARABLE_FIELDS } from "./types.js";
|
|
3
3
|
export { loadRawConfig, loadConfig, normalizeConfig } from "./loader.js";
|
|
4
4
|
export { convertContentToString } from "./formatter.js";
|
package/dist/config/loader.js
CHANGED
|
@@ -13,7 +13,13 @@ export { normalizeConfigInternal as normalizeConfig };
|
|
|
13
13
|
* Use this when you need to perform command-specific validation before normalizing.
|
|
14
14
|
*/
|
|
15
15
|
export function loadRawConfig(configPath) {
|
|
16
|
-
|
|
16
|
+
let stat;
|
|
17
|
+
try {
|
|
18
|
+
stat = statSync(configPath);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
throw new ValidationError(`Failed to read config at ${configPath}: ${toErrorMessage(error)}`, { cause: error });
|
|
22
|
+
}
|
|
17
23
|
if (stat.isDirectory()) {
|
|
18
24
|
return loadRawConfigFromDirectory(configPath);
|
|
19
25
|
}
|
|
@@ -24,7 +30,13 @@ export function loadConfig(configPath, env) {
|
|
|
24
30
|
return normalizeConfigInternal(rawConfig, env);
|
|
25
31
|
}
|
|
26
32
|
function loadRawConfigFromFile(filePath) {
|
|
27
|
-
|
|
33
|
+
let content;
|
|
34
|
+
try {
|
|
35
|
+
content = readFileSync(filePath, "utf-8");
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new ValidationError(`Failed to read config file ${filePath}: ${toErrorMessage(error)}`, { cause: error });
|
|
39
|
+
}
|
|
28
40
|
const configDir = dirname(filePath);
|
|
29
41
|
let rawConfig;
|
|
30
42
|
try {
|
|
@@ -32,7 +44,7 @@ function loadRawConfigFromFile(filePath) {
|
|
|
32
44
|
}
|
|
33
45
|
catch (error) {
|
|
34
46
|
const message = toErrorMessage(error);
|
|
35
|
-
throw new ValidationError(`Failed to parse YAML config at ${filePath}: ${message}
|
|
47
|
+
throw new ValidationError(`Failed to parse YAML config at ${filePath}: ${message}`, { cause: error });
|
|
36
48
|
}
|
|
37
49
|
// Resolve file references before validation so content type checking works
|
|
38
50
|
rawConfig = resolveFileReferencesInConfig(rawConfig, { configDir });
|
|
@@ -40,7 +52,13 @@ function loadRawConfigFromFile(filePath) {
|
|
|
40
52
|
return rawConfig;
|
|
41
53
|
}
|
|
42
54
|
function loadRawConfigFromDirectory(dirPath) {
|
|
43
|
-
|
|
55
|
+
let entries;
|
|
56
|
+
try {
|
|
57
|
+
entries = readdirSync(dirPath, { withFileTypes: true });
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
throw new ValidationError(`Failed to read config directory ${dirPath}: ${toErrorMessage(error)}`, { cause: error });
|
|
61
|
+
}
|
|
44
62
|
const yamlFiles = entries
|
|
45
63
|
.filter((entry) => entry.isFile() &&
|
|
46
64
|
[".yaml", ".yml"].includes(extname(entry.name).toLowerCase()))
|
|
@@ -51,7 +69,13 @@ function loadRawConfigFromDirectory(dirPath) {
|
|
|
51
69
|
}
|
|
52
70
|
const fragments = yamlFiles.map((fileName) => {
|
|
53
71
|
const filePath = join(dirPath, fileName);
|
|
54
|
-
|
|
72
|
+
let content;
|
|
73
|
+
try {
|
|
74
|
+
content = readFileSync(filePath, "utf-8");
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
throw new ValidationError(`Failed to read config file ${filePath}: ${toErrorMessage(error)}`, { cause: error });
|
|
78
|
+
}
|
|
55
79
|
const configDir = dirname(filePath);
|
|
56
80
|
let config;
|
|
57
81
|
try {
|
|
@@ -59,7 +83,7 @@ function loadRawConfigFromDirectory(dirPath) {
|
|
|
59
83
|
}
|
|
60
84
|
catch (error) {
|
|
61
85
|
const message = toErrorMessage(error);
|
|
62
|
-
throw new ValidationError(`Failed to parse YAML config at ${filePath}: ${message}
|
|
86
|
+
throw new ValidationError(`Failed to parse YAML config at ${filePath}: ${message}`, { cause: error });
|
|
63
87
|
}
|
|
64
88
|
if (!config || typeof config !== "object") {
|
|
65
89
|
throw new ValidationError(`Config file ${fileName} is empty or invalid — expected a YAML mapping`);
|
package/dist/config/merge.d.ts
CHANGED
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
* Deep merge utilities for JSON configuration objects.
|
|
3
3
|
* Supports per-field array merge strategies via $arrayMerge + $values directives.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Candidate keys for matching array items by identity rather than index.
|
|
7
|
+
* Order matters — first key found across all items wins.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MATCH_KEY_CANDIDATES: readonly ["type", "actor_id"];
|
|
10
|
+
/**
|
|
11
|
+
* Finds a key that uniquely identifies items in both arrays.
|
|
12
|
+
* Returns the first candidate key present in every item of both arrays, or undefined.
|
|
13
|
+
*/
|
|
14
|
+
export declare function findMatchKey(base: unknown[], overlay: unknown[]): string | undefined;
|
|
15
|
+
export type ArrayMergeStrategy = "replace" | "append" | "prepend" | "merge";
|
|
6
16
|
export interface MergeContext {
|
|
7
17
|
defaultArrayStrategy: ArrayMergeStrategy;
|
|
8
18
|
}
|
package/dist/config/merge.js
CHANGED
|
@@ -3,16 +3,87 @@
|
|
|
3
3
|
* Supports per-field array merge strategies via $arrayMerge + $values directives.
|
|
4
4
|
*/
|
|
5
5
|
import { isPlainObject } from "../shared/type-guards.js";
|
|
6
|
+
/**
|
|
7
|
+
* Candidate keys for matching array items by identity rather than index.
|
|
8
|
+
* Order matters — first key found across all items wins.
|
|
9
|
+
*/
|
|
10
|
+
export const MATCH_KEY_CANDIDATES = ["type", "actor_id"];
|
|
11
|
+
/**
|
|
12
|
+
* Finds a key that uniquely identifies items in both arrays.
|
|
13
|
+
* Returns the first candidate key present in every item of both arrays, or undefined.
|
|
14
|
+
*/
|
|
15
|
+
export function findMatchKey(base, overlay) {
|
|
16
|
+
if (base.length === 0 && overlay.length === 0)
|
|
17
|
+
return undefined;
|
|
18
|
+
const hasKey = (item, key) => isPlainObject(item) && key in item;
|
|
19
|
+
for (const candidate of MATCH_KEY_CANDIDATES) {
|
|
20
|
+
if (base.every((item) => hasKey(item, candidate)) &&
|
|
21
|
+
overlay.every((item) => hasKey(item, candidate))) {
|
|
22
|
+
return candidate;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
6
27
|
/**
|
|
7
28
|
* Keys reserved for xfg merge directives.
|
|
8
29
|
* Only these are stripped during merge — standard $-prefixed keys
|
|
9
30
|
* like $schema, $id, $ref, $generated are preserved.
|
|
10
31
|
*/
|
|
11
32
|
const XFG_DIRECTIVES = new Set(["$arrayMerge", "$values"]);
|
|
33
|
+
function mergeByKey(base, overlay, matchKey, ctx) {
|
|
34
|
+
const baseByKey = new Map();
|
|
35
|
+
// findMatchKey guarantees every item in both arrays is a plain object with matchKey
|
|
36
|
+
for (let i = 0; i < base.length; i++) {
|
|
37
|
+
const item = base[i];
|
|
38
|
+
const keyValue = item[matchKey];
|
|
39
|
+
if (keyValue !== undefined && !baseByKey.has(keyValue)) {
|
|
40
|
+
baseByKey.set(keyValue, { item, index: i });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const appended = [];
|
|
44
|
+
for (const overlayItem of overlay) {
|
|
45
|
+
const item = overlayItem;
|
|
46
|
+
const keyValue = item[matchKey];
|
|
47
|
+
const baseEntry = baseByKey.get(keyValue);
|
|
48
|
+
if (baseEntry) {
|
|
49
|
+
baseByKey.set(keyValue, {
|
|
50
|
+
item: deepMerge(baseEntry.item, item, ctx),
|
|
51
|
+
index: baseEntry.index,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
appended.push(overlayItem);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const result = [];
|
|
59
|
+
for (let i = 0; i < base.length; i++) {
|
|
60
|
+
const item = base[i];
|
|
61
|
+
const keyValue = item[matchKey];
|
|
62
|
+
const entry = baseByKey.get(keyValue);
|
|
63
|
+
if (entry && entry.index === i) {
|
|
64
|
+
result.push(entry.item);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
result.push(item);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
result.push(...appended);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
12
73
|
const arrayMergeStrategies = new Map([
|
|
13
74
|
["replace", (_base, overlay) => overlay],
|
|
14
75
|
["append", (base, overlay) => [...base, ...overlay]],
|
|
15
76
|
["prepend", (base, overlay) => [...overlay, ...base]],
|
|
77
|
+
[
|
|
78
|
+
"merge",
|
|
79
|
+
(base, overlay, ctx) => {
|
|
80
|
+
const matchKey = findMatchKey(base, overlay);
|
|
81
|
+
if (!matchKey) {
|
|
82
|
+
return [...base, ...overlay];
|
|
83
|
+
}
|
|
84
|
+
return mergeByKey(base, overlay, matchKey, ctx);
|
|
85
|
+
},
|
|
86
|
+
],
|
|
16
87
|
]);
|
|
17
88
|
/**
|
|
18
89
|
* Checks if a value is an unresolved $arrayMerge directive object
|
|
@@ -28,12 +99,11 @@ function isUnresolvedDirective(value) {
|
|
|
28
99
|
arrayMergeStrategies.has(value.$arrayMerge) &&
|
|
29
100
|
Array.isArray(value.$values));
|
|
30
101
|
}
|
|
31
|
-
function mergeArrays(base, overlay, strategy) {
|
|
102
|
+
function mergeArrays(base, overlay, strategy, ctx) {
|
|
32
103
|
const handler = arrayMergeStrategies.get(strategy);
|
|
33
104
|
if (handler) {
|
|
34
|
-
return handler(base, overlay);
|
|
105
|
+
return handler(base, overlay, ctx);
|
|
35
106
|
}
|
|
36
|
-
// Fallback to replace for unknown strategies
|
|
37
107
|
return overlay;
|
|
38
108
|
}
|
|
39
109
|
/**
|
|
@@ -61,16 +131,17 @@ export function deepMerge(base, overlay, ctx) {
|
|
|
61
131
|
const values = overlayValue.$values;
|
|
62
132
|
if ((strategy === "replace" ||
|
|
63
133
|
strategy === "append" ||
|
|
64
|
-
strategy === "prepend"
|
|
134
|
+
strategy === "prepend" ||
|
|
135
|
+
strategy === "merge") &&
|
|
65
136
|
Array.isArray(values) &&
|
|
66
137
|
Array.isArray(resolvedBase)) {
|
|
67
|
-
result[key] = mergeArrays(resolvedBase, values, strategy);
|
|
138
|
+
result[key] = mergeArrays(resolvedBase, values, strategy, ctx);
|
|
68
139
|
continue;
|
|
69
140
|
}
|
|
70
141
|
}
|
|
71
142
|
// Both are arrays — use default strategy
|
|
72
143
|
if (Array.isArray(resolvedBase) && Array.isArray(overlayValue)) {
|
|
73
|
-
result[key] = mergeArrays(resolvedBase, overlayValue, ctx.defaultArrayStrategy);
|
|
144
|
+
result[key] = mergeArrays(resolvedBase, overlayValue, ctx.defaultArrayStrategy, ctx);
|
|
74
145
|
continue;
|
|
75
146
|
}
|
|
76
147
|
// Both are plain objects — recurse
|
|
@@ -144,6 +215,7 @@ export function mergeTextContent(base, overlay, strategy = "replace") {
|
|
|
144
215
|
if (Array.isArray(base)) {
|
|
145
216
|
switch (strategy) {
|
|
146
217
|
case "append":
|
|
218
|
+
case "merge":
|
|
147
219
|
return [...base, ...overlay];
|
|
148
220
|
case "prepend":
|
|
149
221
|
return [...overlay, ...base];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { deepMerge, stripMergeDirectives, createMergeContext, isTextContent, mergeTextContent, } from "./merge.js";
|
|
2
|
-
import { interpolateContent } from "
|
|
2
|
+
import { interpolateContent } from "./env.js";
|
|
3
3
|
import { expandRepoGroups } from "./extends-resolver.js";
|
|
4
4
|
/**
|
|
5
5
|
* Clone content, stripping merge directives from object content.
|
|
@@ -79,22 +79,8 @@ function mergePROptions(global, perRepo) {
|
|
|
79
79
|
return perRepo;
|
|
80
80
|
if (!perRepo)
|
|
81
81
|
return global;
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const mergeStrategy = perRepo.mergeStrategy ?? global.mergeStrategy;
|
|
85
|
-
const deleteBranch = perRepo.deleteBranch ?? global.deleteBranch;
|
|
86
|
-
const bypassReason = perRepo.bypassReason ?? global.bypassReason;
|
|
87
|
-
const labels = perRepo.labels ?? global.labels;
|
|
88
|
-
if (merge !== undefined)
|
|
89
|
-
result.merge = merge;
|
|
90
|
-
if (mergeStrategy !== undefined)
|
|
91
|
-
result.mergeStrategy = mergeStrategy;
|
|
92
|
-
if (deleteBranch !== undefined)
|
|
93
|
-
result.deleteBranch = deleteBranch;
|
|
94
|
-
if (bypassReason !== undefined)
|
|
95
|
-
result.bypassReason = bypassReason;
|
|
96
|
-
if (labels !== undefined)
|
|
97
|
-
result.labels = labels;
|
|
82
|
+
const merged = { ...global, ...perRepo };
|
|
83
|
+
const result = Object.fromEntries(Object.entries(merged).filter(([, v]) => v !== undefined));
|
|
98
84
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
99
85
|
}
|
|
100
86
|
/**
|
|
@@ -454,6 +440,35 @@ function resolveFileEntry(fileName, fileConfig, repoOverride, inheritFiles, glob
|
|
|
454
440
|
globalDeleteOrphaned,
|
|
455
441
|
};
|
|
456
442
|
}
|
|
443
|
+
function normalizeRepoEntry(ctx) {
|
|
444
|
+
const files = [];
|
|
445
|
+
const inheritFiles = shouldInherit(ctx.rawRepo.files);
|
|
446
|
+
for (const fileName of ctx.fileNames) {
|
|
447
|
+
if (fileName === "inherit")
|
|
448
|
+
continue;
|
|
449
|
+
const entry = resolveFileEntry(fileName, ctx.effectiveRootFiles[fileName], ctx.rawRepo.files?.[fileName], inheritFiles, ctx.globalDeleteOrphaned, ctx.env);
|
|
450
|
+
if (entry)
|
|
451
|
+
files.push(entry);
|
|
452
|
+
}
|
|
453
|
+
for (const fileName of ctx.repoOnlyFileNames) {
|
|
454
|
+
const repoOverride = ctx.rawRepo.files[fileName];
|
|
455
|
+
if (repoOverride === false)
|
|
456
|
+
continue;
|
|
457
|
+
const entry = resolveFileEntry(fileName, {}, repoOverride, true, ctx.globalDeleteOrphaned, ctx.env);
|
|
458
|
+
if (entry)
|
|
459
|
+
files.push(entry);
|
|
460
|
+
}
|
|
461
|
+
const prOptions = mergePROptions(ctx.effectivePROptions, ctx.rawRepo.prOptions);
|
|
462
|
+
const settings = mergeSettings(ctx.effectiveSettings, ctx.rawRepo.settings);
|
|
463
|
+
return {
|
|
464
|
+
git: ctx.gitUrl,
|
|
465
|
+
files,
|
|
466
|
+
prOptions,
|
|
467
|
+
settings,
|
|
468
|
+
upstream: ctx.rawRepo.upstream,
|
|
469
|
+
source: ctx.rawRepo.source,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
457
472
|
/**
|
|
458
473
|
* Normalizes raw config into expanded, merged config.
|
|
459
474
|
* Pipeline: expand git arrays -> merge content -> interpolate env vars
|
|
@@ -485,29 +500,29 @@ export function normalizeConfig(raw, env) {
|
|
|
485
500
|
effectiveSettings = merged.settings;
|
|
486
501
|
}
|
|
487
502
|
const fileNames = Object.keys(effectiveRootFiles);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
for (const
|
|
492
|
-
|
|
493
|
-
if (fileName === "inherit")
|
|
503
|
+
// Collect repo-only file names (defined at repo level but not in root/groups)
|
|
504
|
+
const repoOnlyFileNames = [];
|
|
505
|
+
if (rawRepo.files) {
|
|
506
|
+
for (const name of Object.keys(rawRepo.files)) {
|
|
507
|
+
if (name === "inherit")
|
|
494
508
|
continue;
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
509
|
+
if (!effectiveRootFiles[name]) {
|
|
510
|
+
repoOnlyFileNames.push(name);
|
|
511
|
+
}
|
|
498
512
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
513
|
+
}
|
|
514
|
+
for (const gitUrl of gitUrls) {
|
|
515
|
+
expandedRepos.push(normalizeRepoEntry({
|
|
516
|
+
gitUrl,
|
|
517
|
+
rawRepo,
|
|
518
|
+
effectiveRootFiles,
|
|
519
|
+
fileNames,
|
|
520
|
+
repoOnlyFileNames,
|
|
521
|
+
effectivePROptions,
|
|
522
|
+
effectiveSettings,
|
|
523
|
+
globalDeleteOrphaned: raw.deleteOrphaned,
|
|
524
|
+
env,
|
|
525
|
+
}));
|
|
511
526
|
}
|
|
512
527
|
}
|
|
513
528
|
// Normalize root settings by reusing mergeSettings with no per-repo overlay.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RawConfig,
|
|
1
|
+
import type { RawConfig, RawRootSettings, RawRepoSettings } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Validates raw config structure before normalization.
|
|
4
4
|
* @throws ValidationError if validation fails
|
|
@@ -9,7 +9,4 @@ export declare function validateRawConfig(config: RawConfig): void;
|
|
|
9
9
|
* @throws ValidationError if neither files nor settings are present
|
|
10
10
|
*/
|
|
11
11
|
export declare function validateForSync(config: RawConfig): void;
|
|
12
|
-
/**
|
|
13
|
-
* Checks if settings contain actionable configuration.
|
|
14
|
-
*/
|
|
15
12
|
export declare function hasActionableSettings(settings: RawRootSettings | RawRepoSettings | undefined): boolean;
|