@aspruyt/xfg 6.1.0 → 6.3.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.
Files changed (174) hide show
  1. package/dist/cli/lifecycle-report-builder.d.ts +2 -2
  2. package/dist/cli/lifecycle-report-builder.js +3 -11
  3. package/dist/cli/program.d.ts +2 -1
  4. package/dist/cli/program.js +43 -6
  5. package/dist/cli/repo-sync-runner.d.ts +24 -0
  6. package/dist/cli/repo-sync-runner.js +156 -0
  7. package/dist/cli/results-collector.d.ts +1 -1
  8. package/dist/cli/results-collector.js +2 -2
  9. package/dist/cli/secrets-command.d.ts +25 -0
  10. package/dist/cli/secrets-command.js +75 -0
  11. package/dist/cli/settings-factories.d.ts +8 -0
  12. package/dist/cli/settings-factories.js +32 -0
  13. package/dist/cli/settings-report-builder.d.ts +7 -2
  14. package/dist/cli/settings-report-builder.js +28 -20
  15. package/dist/cli/settings-runner.d.ts +2 -0
  16. package/dist/cli/settings-runner.js +94 -0
  17. package/dist/cli/sync-command.d.ts +1 -1
  18. package/dist/cli/sync-command.js +31 -372
  19. package/dist/cli/sync-report-builder.d.ts +1 -1
  20. package/dist/cli/sync-utils.d.ts +8 -0
  21. package/dist/cli/sync-utils.js +36 -0
  22. package/dist/cli/types.d.ts +8 -8
  23. package/dist/cli/unified-summary.d.ts +1 -3
  24. package/dist/cli/unified-summary.js +7 -5
  25. package/dist/cli.js +2 -1
  26. package/dist/{shared → config}/env.js +2 -2
  27. package/dist/config/extends-resolver.js +4 -3
  28. package/dist/config/file-reference-resolver.js +4 -2
  29. package/dist/config/formatter.js +1 -0
  30. package/dist/config/index.d.ts +2 -2
  31. package/dist/config/index.js +1 -1
  32. package/dist/config/loader.js +30 -6
  33. package/dist/config/merge.d.ts +11 -1
  34. package/dist/config/merge.js +78 -6
  35. package/dist/config/normalizer.js +129 -49
  36. package/dist/config/types.d.ts +20 -0
  37. package/dist/config/validator.d.ts +5 -4
  38. package/dist/config/validator.js +187 -614
  39. package/dist/config/validators/file-validator.d.ts +2 -1
  40. package/dist/config/validators/file-validator.js +9 -1
  41. package/dist/config/validators/group-validator.d.ts +3 -0
  42. package/dist/config/validators/group-validator.js +167 -0
  43. package/dist/config/validators/repo-entry-validator.d.ts +2 -0
  44. package/dist/config/validators/repo-entry-validator.js +165 -0
  45. package/dist/config/validators/repo-settings-validator.js +18 -7
  46. package/dist/config/validators/ruleset-validator.js +2 -5
  47. package/dist/config/validators/shared.d.ts +11 -0
  48. package/dist/config/validators/shared.js +242 -0
  49. package/dist/lifecycle/ado-migration-source.js +2 -4
  50. package/dist/lifecycle/github-lifecycle-provider.d.ts +7 -11
  51. package/dist/lifecycle/github-lifecycle-provider.js +125 -136
  52. package/dist/lifecycle/{lifecycle-helpers.d.ts → helpers.d.ts} +5 -1
  53. package/dist/lifecycle/{lifecycle-helpers.js → helpers.js} +9 -8
  54. package/dist/lifecycle/index.d.ts +2 -2
  55. package/dist/lifecycle/index.js +1 -1
  56. package/dist/lifecycle/repo-lifecycle-factory.d.ts +2 -2
  57. package/dist/output/github-summary.js +2 -3
  58. package/dist/output/index.d.ts +4 -0
  59. package/dist/output/index.js +4 -0
  60. package/dist/output/lifecycle-report.d.ts +1 -1
  61. package/dist/output/lifecycle-report.js +5 -0
  62. package/dist/output/settings-report.d.ts +11 -0
  63. package/dist/output/settings-report.js +24 -0
  64. package/dist/output/sync-report.d.ts +25 -3
  65. package/dist/output/sync-report.js +11 -11
  66. package/dist/secrets/encryption.d.ts +9 -0
  67. package/dist/secrets/encryption.js +29 -0
  68. package/dist/secrets/github-secrets-strategy.d.ts +17 -0
  69. package/dist/secrets/github-secrets-strategy.js +38 -0
  70. package/dist/secrets/index.d.ts +5 -0
  71. package/dist/secrets/index.js +3 -0
  72. package/dist/secrets/processor.d.ts +31 -0
  73. package/dist/secrets/processor.js +115 -0
  74. package/dist/secrets/types.d.ts +21 -0
  75. package/dist/settings/base-processor.d.ts +18 -7
  76. package/dist/settings/base-processor.js +26 -5
  77. package/dist/settings/code-scanning/diff.js +2 -2
  78. package/dist/settings/code-scanning/formatter.d.ts +2 -6
  79. package/dist/settings/code-scanning/formatter.js +2 -25
  80. package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +3 -7
  81. package/dist/settings/code-scanning/github-code-scanning-strategy.js +2 -2
  82. package/dist/settings/code-scanning/processor.js +6 -4
  83. package/dist/settings/code-scanning/types.d.ts +10 -8
  84. package/dist/settings/index.d.ts +1 -0
  85. package/dist/settings/index.js +2 -0
  86. package/dist/settings/labels/github-labels-strategy.d.ts +3 -11
  87. package/dist/settings/labels/types.d.ts +12 -10
  88. package/dist/settings/repo-settings/diff.d.ts +1 -1
  89. package/dist/settings/repo-settings/diff.js +1 -1
  90. package/dist/settings/repo-settings/formatter.d.ts +2 -6
  91. package/dist/settings/repo-settings/formatter.js +4 -23
  92. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -2
  93. package/dist/settings/repo-settings/github-repo-settings-strategy.js +8 -7
  94. package/dist/settings/repo-settings/processor.js +11 -11
  95. package/dist/settings/repo-settings/types.d.ts +2 -2
  96. package/dist/settings/rulesets/diff-algorithm.js +4 -2
  97. package/dist/settings/rulesets/diff.js +2 -51
  98. package/dist/settings/rulesets/formatter.js +4 -0
  99. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +3 -3
  100. package/dist/settings/rulesets/github-ruleset-strategy.js +4 -6
  101. package/dist/settings/rulesets/index.d.ts +1 -1
  102. package/dist/settings/rulesets/index.js +0 -2
  103. package/dist/settings/rulesets/processor.js +1 -1
  104. package/dist/settings/rulesets/types.d.ts +6 -2
  105. package/dist/settings/variables/diff.d.ts +10 -0
  106. package/dist/settings/variables/diff.js +39 -0
  107. package/dist/settings/variables/formatter.d.ts +16 -0
  108. package/dist/settings/variables/formatter.js +70 -0
  109. package/dist/settings/variables/github-variables-strategy.d.ts +17 -0
  110. package/dist/settings/variables/github-variables-strategy.js +40 -0
  111. package/dist/settings/variables/index.d.ts +4 -0
  112. package/dist/settings/variables/index.js +2 -0
  113. package/dist/settings/variables/processor.d.ts +19 -0
  114. package/dist/settings/variables/processor.js +60 -0
  115. package/dist/settings/variables/types.d.ts +18 -0
  116. package/dist/settings/variables/types.js +1 -0
  117. package/dist/shared/command-executor.d.ts +4 -4
  118. package/dist/shared/command-executor.js +9 -7
  119. package/dist/shared/diff-format.d.ts +1 -0
  120. package/dist/shared/diff-format.js +10 -0
  121. package/dist/shared/env-resolver.d.ts +16 -0
  122. package/dist/shared/env-resolver.js +33 -0
  123. package/dist/shared/errors.d.ts +7 -4
  124. package/dist/shared/errors.js +8 -8
  125. package/dist/shared/gh-api-utils.d.ts +3 -34
  126. package/dist/shared/gh-api-utils.js +23 -53
  127. package/dist/shared/gh-token-utils.d.ts +26 -0
  128. package/dist/shared/gh-token-utils.js +32 -0
  129. package/dist/shared/json-utils.js +1 -1
  130. package/dist/shared/regex-utils.d.ts +1 -0
  131. package/dist/shared/regex-utils.js +3 -0
  132. package/dist/shared/retry-utils.d.ts +1 -0
  133. package/dist/shared/retry-utils.js +13 -7
  134. package/dist/sync/auth-options-builder.js +1 -1
  135. package/dist/sync/branch-manager.js +5 -3
  136. package/dist/sync/commit-push-manager.js +2 -3
  137. package/dist/sync/diff-utils.d.ts +0 -1
  138. package/dist/sync/diff-utils.js +5 -10
  139. package/dist/sync/file-sync-orchestrator.js +0 -2
  140. package/dist/sync/file-writer.d.ts +3 -0
  141. package/dist/sync/file-writer.js +84 -81
  142. package/dist/sync/index.d.ts +0 -1
  143. package/dist/sync/index.js +0 -1
  144. package/dist/sync/manifest.js +1 -1
  145. package/dist/sync/pr-merge-handler.js +6 -6
  146. package/dist/sync/sync-workflow.js +1 -1
  147. package/dist/sync/types.d.ts +2 -2
  148. package/dist/vcs/ado-pr-strategy.d.ts +3 -5
  149. package/dist/vcs/ado-pr-strategy.js +131 -33
  150. package/dist/vcs/authenticated-git-ops.js +45 -23
  151. package/dist/vcs/git-commit-strategy.js +10 -6
  152. package/dist/vcs/git-ops.js +30 -24
  153. package/dist/vcs/github-pr-strategy.d.ts +3 -2
  154. package/dist/vcs/github-pr-strategy.js +80 -30
  155. package/dist/vcs/gitlab-pr-strategy.d.ts +2 -5
  156. package/dist/vcs/gitlab-pr-strategy.js +88 -87
  157. package/dist/vcs/graphql-commit-strategy.d.ts +1 -5
  158. package/dist/vcs/graphql-commit-strategy.js +21 -37
  159. package/dist/vcs/pr-creator.js +9 -2
  160. package/dist/vcs/pr-strategy.d.ts +2 -3
  161. package/dist/vcs/pr-strategy.js +0 -1
  162. package/dist/vcs/types.d.ts +9 -5
  163. package/package.json +7 -5
  164. package/dist/config/validators/index.d.ts +0 -3
  165. package/dist/config/validators/index.js +0 -6
  166. package/dist/output/types.d.ts +0 -20
  167. package/dist/shared/shell-utils.d.ts +0 -6
  168. package/dist/shared/shell-utils.js +0 -17
  169. /package/dist/{shared → config}/env.d.ts +0 -0
  170. /package/dist/lifecycle/{lifecycle-formatter.d.ts → formatter.d.ts} +0 -0
  171. /package/dist/lifecycle/{lifecycle-formatter.js → formatter.js} +0 -0
  172. /package/dist/{output → secrets}/types.js +0 -0
  173. /package/dist/{vcs → shared}/sanitize-utils.d.ts +0 -0
  174. /package/dist/{vcs → shared}/sanitize-utils.js +0 -0
@@ -0,0 +1,94 @@
1
+ import { isGitHubRepo } from "../repo/index.js";
2
+ import { toErrorMessage } from "../shared/type-guards.js";
3
+ function logSettingsResult(logger, result, label, repoNumber, repoName, settingsCollector) {
4
+ if (result.planOutput?.lines?.length) {
5
+ logger.info("");
6
+ logger.info(`${repoName} - ${label}:`);
7
+ for (const line of result.planOutput.lines) {
8
+ logger.info(line);
9
+ }
10
+ if (result.warnings?.length) {
11
+ for (const warning of result.warnings) {
12
+ logger.warn(warning);
13
+ }
14
+ }
15
+ }
16
+ else if (!result.skipped && result.success) {
17
+ logger.success(repoNumber, repoName, `${label}: ${result.message}`);
18
+ }
19
+ if (!result.success && !result.skipped) {
20
+ logger.error(repoNumber, repoName, `${label}: ${result.message}`);
21
+ settingsCollector.appendError(repoName, result.message);
22
+ }
23
+ }
24
+ async function runAndStoreResult(factory, repoConfig, repoInfo, opts, repoName, settingsCollector, assign) {
25
+ const result = await factory().process(repoConfig, repoInfo, opts);
26
+ if (!result.skipped) {
27
+ assign(settingsCollector.findOrCreate(repoName), result);
28
+ }
29
+ return result;
30
+ }
31
+ function buildSettingsDescriptors(ctx) {
32
+ const { repoConfig, repoInfo, options, token, repoName, settingsCollector } = ctx;
33
+ const { factories } = ctx;
34
+ const sharedOpts = {
35
+ dryRun: options.dryRun,
36
+ noDelete: options.noDelete,
37
+ token,
38
+ };
39
+ return [
40
+ {
41
+ key: "rulesets",
42
+ label: "Rulesets",
43
+ run: () => runAndStoreResult(factories.rulesets, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
44
+ e.rulesetResult = r;
45
+ }),
46
+ },
47
+ {
48
+ key: "labels",
49
+ label: "Labels",
50
+ run: () => runAndStoreResult(factories.labels, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
51
+ e.labelsResult = r;
52
+ }),
53
+ },
54
+ {
55
+ key: "repo",
56
+ label: "Repo Settings",
57
+ run: () => runAndStoreResult(factories.repo, repoConfig, repoInfo, { dryRun: options.dryRun, token }, repoName, settingsCollector, (e, r) => {
58
+ e.settingsResult = r;
59
+ }),
60
+ },
61
+ {
62
+ key: "codeScanning",
63
+ label: "Code Scanning",
64
+ run: () => runAndStoreResult(factories.codeScanning, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
65
+ e.codeScanningResult = r;
66
+ }),
67
+ },
68
+ {
69
+ key: "variables",
70
+ label: "Variables",
71
+ run: () => runAndStoreResult(factories.variables, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
72
+ e.variablesResult = r;
73
+ }),
74
+ },
75
+ ];
76
+ }
77
+ export async function applyRepoSettings(ctx) {
78
+ const { repoConfig, repoInfo, repoName, repoNumber, settingsCollector, logger, } = ctx;
79
+ if (!repoConfig.settings || !isGitHubRepo(repoInfo))
80
+ return;
81
+ for (const desc of buildSettingsDescriptors(ctx)) {
82
+ const settingsValue = repoConfig.settings[desc.key];
83
+ if (!settingsValue || Object.keys(settingsValue).length === 0)
84
+ continue;
85
+ try {
86
+ const result = await desc.run();
87
+ logSettingsResult(logger, result, desc.label, repoNumber, repoName, settingsCollector);
88
+ }
89
+ catch (error) {
90
+ logger.error(repoNumber, repoName, `${desc.label}: ${toErrorMessage(error)}`);
91
+ settingsCollector.appendError(repoName, error);
92
+ }
93
+ }
94
+ }
@@ -1,3 +1,3 @@
1
- import { type SyncDependencies, type SyncOptions } from "./types.js";
1
+ import type { SyncDependencies, SyncOptions } from "./types.js";
2
2
  export type { SharedOptions, SyncOptions } from "./types.js";
3
3
  export declare function runSync(options: SyncOptions, deps?: SyncDependencies): Promise<void>;
@@ -1,209 +1,47 @@
1
- import { resolve, join } from "node:path";
1
+ import { resolve } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
3
  import { loadRawConfig, normalizeConfig, validateForSync, } from "../config/index.js";
4
4
  import { ValidationError, SyncError } from "../shared/errors.js";
5
- import { parseGitUrl, getRepoDisplayName, isGitHubRepo, } from "../repo/index.js";
6
- import { sanitizeBranchName, validateBranchName } from "./branch-utils.js";
5
+ import { validateBranchName } from "./branch-utils.js";
7
6
  import { createTokenManager } from "../vcs/index.js";
8
7
  import { RepositoryProcessor } from "../sync/index.js";
9
- import { RulesetProcessor, RepoSettingsProcessor, LabelsProcessor, CodeScanningProcessor, GitHubRulesetStrategy, GitHubRepoSettingsStrategy, GitHubLabelsStrategy, GitHubCodeScanningStrategy, } from "../settings/index.js";
10
- import { GitHubRepoMetadataProvider } from "../repo/index.js";
11
- import { ShellCommandExecutor } from "../shared/command-executor.js";
8
+ import { ProcessExecutor } from "../shared/command-executor.js";
12
9
  import { Logger } from "../shared/logger.js";
13
- import { generateWorkspaceName } from "../shared/workspace-utils.js";
14
- let _defaultExecutor;
15
- let _logger;
16
- function getDefaultExecutor() {
17
- return (_defaultExecutor ??= new ShellCommandExecutor(process.env));
18
- }
19
- function getLogger() {
20
- return (_logger ??= new Logger(!!(process.env.DEBUG || process.env.XFG_DEBUG)));
21
- }
22
- function createDefaultRulesetProcessorFactory() {
23
- const cwd = process.cwd();
24
- return () => new RulesetProcessor(new GitHubRulesetStrategy(getDefaultExecutor(), { cwd }));
25
- }
26
- function createDefaultRepoSettingsProcessorFactory() {
27
- const cwd = process.cwd();
28
- const executor = getDefaultExecutor();
29
- return () => new RepoSettingsProcessor(new GitHubRepoSettingsStrategy(executor, { cwd }), new GitHubRepoMetadataProvider(executor, { cwd }));
30
- }
31
- function createDefaultLabelsProcessorFactory() {
32
- const cwd = process.cwd();
33
- return () => new LabelsProcessor(new GitHubLabelsStrategy(getDefaultExecutor(), { cwd }));
34
- }
35
- function createDefaultCodeScanningProcessorFactory() {
36
- const cwd = process.cwd();
37
- const executor = getDefaultExecutor();
38
- return () => new CodeScanningProcessor(new GitHubCodeScanningStrategy(executor, { cwd }), new GitHubRepoMetadataProvider(executor, { cwd }));
39
- }
40
10
  import { ResultsCollector } from "./results-collector.js";
41
- import { buildSettingsReport, } from "./settings-report-builder.js";
42
- import { formatSettingsReportCLI } from "../output/settings-report.js";
11
+ import { buildSettingsReport } from "./settings-report-builder.js";
12
+ import { formatSettingsReportCLI, formatSyncReportCLI, formatLifecycleReportCLI, hasLifecycleChanges, } from "../output/index.js";
43
13
  import { buildSyncReport } from "./sync-report-builder.js";
44
- import { formatSyncReportCLI } from "../output/sync-report.js";
45
14
  import { buildLifecycleReport } from "./lifecycle-report-builder.js";
46
- import { formatLifecycleReportCLI, hasLifecycleChanges, } from "../output/lifecycle-report.js";
47
15
  import { writeUnifiedSummary } from "./unified-summary.js";
48
- import { toErrorMessage } from "../shared/type-guards.js";
49
- import { resolveGitHubToken } from "../shared/gh-api-utils.js";
50
- import { RepoLifecycleManager, runLifecycleCheck, } from "../lifecycle/index.js";
51
- function getUniqueFileNames(config) {
52
- const fileNames = new Set();
53
- for (const repo of config.repos) {
54
- for (const file of repo.files) {
55
- fileNames.add(file.fileName);
56
- }
57
- }
58
- return Array.from(fileNames);
59
- }
60
- function generateBranchName(fileNames) {
61
- if (fileNames.length === 1) {
62
- return `chore/sync-${sanitizeBranchName(fileNames[0])}`;
63
- }
64
- return "chore/sync-config";
65
- }
66
- function formatFileNames(fileNames) {
67
- if (fileNames.length === 1) {
68
- return fileNames[0];
69
- }
70
- if (fileNames.length <= 3) {
71
- return fileNames.join(", ");
72
- }
73
- return `${fileNames.length} files`;
74
- }
75
- function determineMergeOutcome(result) {
76
- if (!result.success)
77
- return undefined;
78
- if (!result.prUrl)
79
- return "direct";
80
- if (result.mergeResult?.merged)
81
- return "force";
82
- if (result.mergeResult?.autoMergeEnabled)
83
- return "auto";
84
- return "manual";
85
- }
86
- function logSettingsResult(result, label, repoNumber, repoName, settingsCollector) {
87
- if (result.planOutput?.lines?.length) {
88
- getLogger().info("");
89
- getLogger().info(`${repoName} - ${label}:`);
90
- for (const line of result.planOutput.lines) {
91
- getLogger().info(line);
92
- }
93
- if (result.warnings?.length) {
94
- for (const warning of result.warnings) {
95
- getLogger().warn(warning);
96
- }
97
- }
98
- }
99
- else if (!result.skipped && result.success) {
100
- getLogger().success(repoNumber, repoName, `${label}: ${result.message}`);
101
- }
102
- if (!result.success && !result.skipped) {
103
- getLogger().error(repoNumber, repoName, `${label}: ${result.message}`);
104
- settingsCollector.appendError(repoName, result.message);
105
- }
106
- }
107
- // Each processor returns a subtype of BaseProcessorResult whose planOutput
108
- // contains both `lines` (for CLI display) and `entries` (for report building).
109
- // ProcessorResults fields capture only the `entries` slice; the runtime object
110
- // satisfies both views, so we assign with an explicit per-field cast.
111
- async function runAndStoreResult(factory, repoConfig, repoInfo, opts, repoName, settingsCollector, assign) {
112
- const result = await runSettingsProcessor(factory, repoConfig, repoInfo, opts);
113
- if (!result.skipped) {
114
- assign(settingsCollector.getOrCreate(repoName), result);
115
- }
116
- return result;
117
- }
118
- function buildSettingsDescriptors(ctx) {
119
- const { repoConfig, repoInfo, options, token, repoName, settingsCollector } = ctx;
120
- const { factories } = ctx;
121
- const sharedOpts = {
122
- dryRun: options.dryRun,
123
- noDelete: options.noDelete,
124
- token,
125
- };
126
- return [
127
- {
128
- key: "rulesets",
129
- label: "Rulesets",
130
- run: () => runAndStoreResult(factories.rulesets, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
131
- e.rulesetResult = r;
132
- }),
133
- },
134
- {
135
- key: "labels",
136
- label: "Labels",
137
- run: () => runAndStoreResult(factories.labels, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
138
- e.labelsResult = r;
139
- }),
140
- },
141
- {
142
- key: "repo",
143
- label: "Repo Settings",
144
- run: () => runAndStoreResult(factories.repo, repoConfig, repoInfo, { dryRun: options.dryRun, token }, repoName, settingsCollector, (e, r) => {
145
- e.settingsResult = r;
146
- }),
147
- },
148
- {
149
- key: "codeScanning",
150
- label: "Code Scanning",
151
- run: () => runAndStoreResult(factories.codeScanning, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
152
- e.codeScanningResult = r;
153
- }),
154
- },
155
- ];
156
- }
157
- function runSettingsProcessor(factory, repoConfig, repoInfo, processOptions) {
158
- return factory()
159
- .process(repoConfig, repoInfo, processOptions)
160
- .then((result) => result);
161
- }
162
- async function applyRepoSettings(ctx) {
163
- const { repoConfig, repoInfo, repoName, repoNumber, settingsCollector } = ctx;
164
- if (!repoConfig.settings || !isGitHubRepo(repoInfo))
165
- return;
166
- for (const desc of buildSettingsDescriptors(ctx)) {
167
- const settingsValue = repoConfig.settings[desc.key];
168
- if (!settingsValue || Object.keys(settingsValue).length === 0)
169
- continue;
170
- try {
171
- const result = await desc.run();
172
- logSettingsResult(result, desc.label, repoNumber, repoName, settingsCollector);
173
- }
174
- catch (error) {
175
- getLogger().error(repoNumber, repoName, `${desc.label}: ${toErrorMessage(error)}`);
176
- settingsCollector.appendError(repoName, error);
177
- }
178
- }
179
- }
180
- function displayReports(reportResults, lifecycleReportInputs, settingsCollector, dryRun) {
16
+ import { RepoLifecycleManager } from "../lifecycle/index.js";
17
+ import { createDefaultFactories } from "./settings-factories.js";
18
+ import { getUniqueFileNames, generateBranchName, formatFileNames, } from "./sync-utils.js";
19
+ import { runSingleRepo, } from "./repo-sync-runner.js";
20
+ function displayReports(logger, reportResults, lifecycleReportInputs, settingsCollector, dryRun) {
181
21
  const lifecycleReport = buildLifecycleReport(lifecycleReportInputs);
182
22
  if (hasLifecycleChanges(lifecycleReport)) {
183
- getLogger().log("");
23
+ logger.log("");
184
24
  for (const line of formatLifecycleReportCLI(lifecycleReport)) {
185
- getLogger().log(line);
25
+ logger.log(line);
186
26
  }
187
27
  }
188
28
  const report = buildSyncReport(reportResults);
189
- getLogger().log("");
29
+ logger.log("");
190
30
  for (const line of formatSyncReportCLI(report)) {
191
- getLogger().log(line);
31
+ logger.log(line);
192
32
  }
193
- // Build and display settings report (if any settings were processed)
194
33
  const settingsResults = settingsCollector.getAll();
195
34
  let settingsReport;
196
35
  if (settingsResults.length > 0) {
197
36
  settingsReport = buildSettingsReport(settingsResults);
198
37
  const settingsLines = formatSettingsReportCLI(settingsReport);
199
38
  if (settingsLines.length > 0) {
200
- getLogger().log("");
39
+ logger.log("");
201
40
  for (const line of settingsLines) {
202
- getLogger().log(line);
41
+ logger.log(line);
203
42
  }
204
43
  }
205
44
  }
206
- // Write unified summary to GITHUB_STEP_SUMMARY
207
45
  writeUnifiedSummary({
208
46
  lifecycle: lifecycleReport,
209
47
  sync: report,
@@ -212,198 +50,18 @@ function displayReports(reportResults, lifecycleReportInputs, settingsCollector,
212
50
  summaryPath: process.env.GITHUB_STEP_SUMMARY,
213
51
  });
214
52
  }
215
- function pushFailure(results, repoName, error) {
216
- results.push({
217
- repoName,
218
- success: false,
219
- fileChanges: [],
220
- error: toErrorMessage(error),
221
- });
222
- }
223
- /**
224
- * Process a single repository: resolve URL, run lifecycle check, sync files, apply settings.
225
- * Pushes results into ctx.reportResults, ctx.lifecycleReportInputs, and ctx.settingsCollector.
226
- */
227
- async function processSingleRepo(repoConfig, index, ctx) {
228
- const { config, options } = ctx;
229
- const repoNumber = index + 1;
230
- // Apply CLI-level PR option overrides
231
- if (options.merge || options.mergeStrategy || options.deleteBranch) {
232
- repoConfig.prOptions = {
233
- ...repoConfig.prOptions,
234
- merge: options.merge ?? repoConfig.prOptions?.merge,
235
- mergeStrategy: options.mergeStrategy ?? repoConfig.prOptions?.mergeStrategy,
236
- deleteBranch: options.deleteBranch ?? repoConfig.prOptions?.deleteBranch,
237
- };
238
- }
239
- const mergeMode = repoConfig.prOptions?.merge ?? "auto";
240
- if (mergeMode === "direct" && repoConfig.prOptions?.mergeStrategy) {
241
- getLogger().warn(`mergeStrategy '${repoConfig.prOptions.mergeStrategy}' is ignored in direct mode for ${repoConfig.git}`);
242
- }
243
- let repoInfo;
244
- try {
245
- repoInfo = parseGitUrl(repoConfig.git, {
246
- githubHosts: config.githubHosts,
247
- });
248
- }
249
- catch (error) {
250
- getLogger().error(repoNumber, repoConfig.git, toErrorMessage(error));
251
- pushFailure(ctx.reportResults, repoConfig.git, error);
252
- return;
253
- }
254
- const repoName = getRepoDisplayName(repoInfo);
255
- const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(index)));
256
- const repoToken = isGitHubRepo(repoInfo)
257
- ? (await resolveGitHubToken({
258
- repoInfo: repoInfo,
259
- tokenManager: ctx.tokenManager,
260
- context: repoName,
261
- log: getLogger(),
262
- envToken: process.env.GH_TOKEN,
263
- })).token
264
- : undefined;
265
- const repo = {
266
- repoConfig,
267
- repoInfo,
268
- repoName,
269
- index,
270
- workDir,
271
- token: repoToken,
272
- };
273
- const skipFileSync = await runLifecyclePhase(repo, ctx);
274
- if (skipFileSync)
275
- return;
276
- // Sync files via processor
277
- await runFileSyncPhase(repo, ctx);
278
- // Apply settings via API (GitHub-only — ADO and GitLab repos are skipped)
279
- await applyRepoSettings({
280
- repoConfig,
281
- repoInfo,
282
- repoName,
283
- repoNumber,
284
- options,
285
- token: repoToken,
286
- settingsCollector: ctx.settingsCollector,
287
- factories: ctx.factories,
288
- });
289
- }
290
- /**
291
- * Run lifecycle check (repo existence, creation, forking).
292
- * Returns true if the main loop should skip file sync for this repo.
293
- */
294
- async function runLifecyclePhase(repo, ctx) {
295
- const repoNumber = repo.index + 1;
296
- try {
297
- const { outputLines, lifecycleResult, createSettings } = await runLifecycleCheck(repo.repoConfig, repo.repoInfo, {
298
- dryRun: ctx.options.dryRun ?? false,
299
- resolvedWorkDir: repo.workDir,
300
- githubHosts: ctx.config.githubHosts,
301
- token: repo.token,
302
- repoIndex: repo.index,
303
- lifecycleManager: ctx.lifecycleManager,
304
- repoSettings: ctx.config.settings?.repo,
305
- });
306
- for (const line of outputLines) {
307
- getLogger().info(line);
308
- }
309
- ctx.lifecycleReportInputs.push({
310
- repoName: repo.repoName,
311
- action: lifecycleResult.action,
312
- upstream: repo.repoConfig.upstream,
313
- source: repo.repoConfig.source,
314
- settings: createSettings
315
- ? {
316
- visibility: createSettings.visibility,
317
- description: createSettings.description,
318
- }
319
- : undefined,
320
- });
321
- // In dry-run, skip processing repos that don't exist yet
322
- if (ctx.options.dryRun && lifecycleResult.action !== "existed") {
323
- ctx.reportResults.push({
324
- repoName: repo.repoName,
325
- success: true,
326
- fileChanges: [],
327
- });
328
- return true;
329
- }
330
- return false;
331
- }
332
- catch (error) {
333
- getLogger().error(repoNumber, repo.repoName, `Lifecycle error: ${toErrorMessage(error)}`);
334
- pushFailure(ctx.reportResults, repo.repoName, error);
335
- return true;
336
- }
337
- }
338
- /**
339
- * Run the file sync processor for a single repo and collect results.
340
- */
341
- async function runFileSyncPhase(repo, ctx) {
342
- const repoNumber = repo.index + 1;
343
- try {
344
- getLogger().progress(repoNumber, repo.repoName, "Processing...");
345
- const result = await ctx.processor.process(repo.repoConfig, repo.repoInfo, {
346
- branchName: ctx.branchName,
347
- workDir: repo.workDir,
348
- configId: ctx.config.id,
349
- dryRun: ctx.options.dryRun,
350
- retries: ctx.options.retries,
351
- executor: getDefaultExecutor(),
352
- prTemplate: ctx.config.prTemplate,
353
- noDelete: ctx.options.noDelete,
354
- token: repo.token,
355
- hasAppCredentials: isGitHubRepo(repo.repoInfo) && ctx.tokenManager !== null,
356
- });
357
- const mergeOutcome = determineMergeOutcome(result);
358
- ctx.reportResults.push({
359
- repoName: repo.repoName,
360
- success: result.success,
361
- fileChanges: (result.fileChanges ?? []).map((f) => ({
362
- path: f.path,
363
- action: f.action,
364
- ...(f.diffLines ? { diffLines: f.diffLines } : {}),
365
- })),
366
- prUrl: result.prUrl,
367
- mergeOutcome,
368
- error: result.success ? undefined : result.message,
369
- });
370
- if (result.skipped) {
371
- getLogger().skip(repoNumber, repo.repoName, result.message);
372
- }
373
- else if (result.success) {
374
- getLogger().success(repoNumber, repo.repoName, result.message);
375
- }
376
- else {
377
- getLogger().error(repoNumber, repo.repoName, result.message);
378
- }
379
- }
380
- catch (error) {
381
- getLogger().error(repoNumber, repo.repoName, toErrorMessage(error));
382
- pushFailure(ctx.reportResults, repo.repoName, error);
383
- }
384
- }
385
53
  export async function runSync(options, deps = {}) {
386
- // Reset module-level singletons to ensure fresh state per invocation
387
- _defaultExecutor = undefined;
388
- _logger = undefined;
54
+ const executor = new ProcessExecutor(process.env);
55
+ const logger = new Logger(!!(process.env.DEBUG || process.env.XFG_DEBUG));
389
56
  const { lifecycleManager, settingsProcessorFactories } = deps;
390
- const factories = {
391
- rulesets: settingsProcessorFactories?.rulesets ??
392
- createDefaultRulesetProcessorFactory(),
393
- labels: settingsProcessorFactories?.labels ??
394
- createDefaultLabelsProcessorFactory(),
395
- repo: settingsProcessorFactories?.repo ??
396
- createDefaultRepoSettingsProcessorFactory(),
397
- codeScanning: settingsProcessorFactories?.codeScanning ??
398
- createDefaultCodeScanningProcessorFactory(),
399
- };
57
+ const factories = createDefaultFactories(executor, settingsProcessorFactories);
400
58
  const configPath = resolve(options.config);
401
59
  if (!existsSync(configPath)) {
402
60
  throw new ValidationError(`Config path not found: ${configPath}`);
403
61
  }
404
- getLogger().log(`Loading config from: ${configPath}`);
62
+ logger.log(`Loading config from: ${configPath}`);
405
63
  if (options.dryRun) {
406
- getLogger().log("Running in DRY RUN mode - no changes will be made\n");
64
+ logger.log("Running in DRY RUN mode - no changes will be made\n");
407
65
  }
408
66
  const rawConfig = loadRawConfig(configPath);
409
67
  validateForSync(rawConfig);
@@ -417,10 +75,10 @@ export async function runSync(options, deps = {}) {
417
75
  else {
418
76
  branchName = generateBranchName(fileNames);
419
77
  }
420
- getLogger().setTotal(config.repos.length);
421
- getLogger().log(`Found ${config.repos.length} repositories to process`);
422
- getLogger().log(`Target files: ${formatFileNames(fileNames)}`);
423
- getLogger().log(`Branch: ${branchName}\n`);
78
+ logger.setTotal(config.repos.length);
79
+ logger.log(`Found ${config.repos.length} repositories to process`);
80
+ logger.log(`Target files: ${formatFileNames(fileNames)}`);
81
+ logger.log(`Branch: ${branchName}\n`);
424
82
  const tokenManager = createTokenManager(process.env.XFG_GITHUB_CLIENT_ID && process.env.XFG_GITHUB_APP_PRIVATE_KEY
425
83
  ? {
426
84
  clientId: process.env.XFG_GITHUB_CLIENT_ID,
@@ -429,7 +87,7 @@ export async function runSync(options, deps = {}) {
429
87
  : undefined);
430
88
  const processor = deps.processorFactory
431
89
  ? deps.processorFactory()
432
- : new RepositoryProcessor(undefined, getLogger(), {
90
+ : new RepositoryProcessor(undefined, logger, {
433
91
  tokenManager,
434
92
  envToken: process.env.GH_TOKEN,
435
93
  });
@@ -439,18 +97,19 @@ export async function runSync(options, deps = {}) {
439
97
  branchName,
440
98
  processor,
441
99
  lifecycleManager: lifecycleManager ??
442
- new RepoLifecycleManager(undefined, getDefaultExecutor(), options.retries, process.cwd(), getLogger()),
100
+ new RepoLifecycleManager(undefined, executor, options.retries, process.cwd(), logger),
443
101
  tokenManager,
444
102
  reportResults: [],
445
103
  lifecycleReportInputs: [],
446
104
  settingsCollector: new ResultsCollector(),
447
105
  factories,
106
+ logger,
107
+ executor,
448
108
  };
449
109
  for (let i = 0; i < config.repos.length; i++) {
450
- await processSingleRepo(config.repos[i], i, ctx);
110
+ await runSingleRepo(config.repos[i], i, ctx);
451
111
  }
452
- displayReports(ctx.reportResults, ctx.lifecycleReportInputs, ctx.settingsCollector, options.dryRun ?? false);
453
- // Propagate failures to caller (CLI entry handles process.exit)
112
+ displayReports(logger, ctx.reportResults, ctx.lifecycleReportInputs, ctx.settingsCollector, options.dryRun ?? false);
454
113
  const settingsResults = ctx.settingsCollector.getAll();
455
114
  const hasErrors = ctx.reportResults.some((r) => r.error);
456
115
  const hasSettingsErrors = settingsResults.some((r) => r.error);
@@ -1,5 +1,5 @@
1
1
  import type { MergeMode } from "../config/index.js";
2
- import type { SyncReport, ReportFileChange } from "../output/sync-report.js";
2
+ import type { SyncReport, ReportFileChange } from "../output/index.js";
3
3
  interface SyncResultInput {
4
4
  repoName: string;
5
5
  success: boolean;
@@ -0,0 +1,8 @@
1
+ import type { RepoConfig, MergeMode } from "../config/index.js";
2
+ import type { ProcessorResult } from "../sync/index.js";
3
+ export declare function getUniqueFileNames(config: {
4
+ repos: RepoConfig[];
5
+ }): string[];
6
+ export declare function generateBranchName(fileNames: string[]): string;
7
+ export declare function formatFileNames(fileNames: string[]): string;
8
+ export declare function determineMergeOutcome(result: ProcessorResult): MergeMode | undefined;
@@ -0,0 +1,36 @@
1
+ import { sanitizeBranchName } from "./branch-utils.js";
2
+ export function getUniqueFileNames(config) {
3
+ const fileNames = new Set();
4
+ for (const repo of config.repos) {
5
+ for (const file of repo.files) {
6
+ fileNames.add(file.fileName);
7
+ }
8
+ }
9
+ return Array.from(fileNames);
10
+ }
11
+ export function generateBranchName(fileNames) {
12
+ if (fileNames.length === 1) {
13
+ return `chore/sync-${sanitizeBranchName(fileNames[0])}`;
14
+ }
15
+ return "chore/sync-config";
16
+ }
17
+ export function formatFileNames(fileNames) {
18
+ if (fileNames.length === 1) {
19
+ return fileNames[0];
20
+ }
21
+ if (fileNames.length <= 3) {
22
+ return fileNames.join(", ");
23
+ }
24
+ return `${fileNames.length} files`;
25
+ }
26
+ export function determineMergeOutcome(result) {
27
+ if (!result.success)
28
+ return undefined;
29
+ if (!result.prUrl)
30
+ return "direct";
31
+ if (result.mergeResult?.merged)
32
+ return "force";
33
+ if (result.mergeResult?.autoMergeEnabled)
34
+ return "auto";
35
+ return "manual";
36
+ }
@@ -1,21 +1,24 @@
1
1
  import type { MergeMode, MergeStrategy, RepoConfig } from "../config/index.js";
2
2
  import type { IRepoLifecycleManager } from "../lifecycle/index.js";
3
- import type { IRepositoryProcessor } from "../sync/index.js";
4
- import type { ISettingsProcessor, IRulesetProcessor, IRepoSettingsProcessor, ILabelsProcessor, ICodeScanningProcessor, BaseProcessorResult, ActiveAction } from "../settings/index.js";
3
+ import type { IRepositoryProcessor, FileChangeDetail } from "../sync/index.js";
4
+ import type { ISettingsProcessor, IRulesetProcessor, IRepoSettingsProcessor, ILabelsProcessor, ICodeScanningProcessor, IVariablesProcessor, BaseProcessorResult } from "../settings/index.js";
5
5
  import type { RepoInfo } from "../repo/index.js";
6
6
  import type { ResultsCollector } from "./results-collector.js";
7
+ import type { Logger } from "../shared/logger.js";
7
8
  export type ProcessorFactory = () => IRepositoryProcessor;
8
9
  export type SettingsProcessorFactory<T extends ISettingsProcessor> = () => T;
9
10
  export type RulesetProcessorFactory = SettingsProcessorFactory<IRulesetProcessor>;
10
11
  export type RepoSettingsProcessorFactory = SettingsProcessorFactory<IRepoSettingsProcessor>;
11
12
  export type LabelsProcessorFactory = SettingsProcessorFactory<ILabelsProcessor>;
12
13
  export type CodeScanningProcessorFactory = SettingsProcessorFactory<ICodeScanningProcessor>;
13
- export type SettingsKind = "rulesets" | "labels" | "repo" | "codeScanning";
14
+ export type VariablesProcessorFactory = SettingsProcessorFactory<IVariablesProcessor>;
15
+ export type SettingsKind = "rulesets" | "labels" | "repo" | "codeScanning" | "variables";
14
16
  export interface SettingsProcessorFactories {
15
17
  rulesets: RulesetProcessorFactory;
16
18
  labels: LabelsProcessorFactory;
17
19
  repo: RepoSettingsProcessorFactory;
18
20
  codeScanning: CodeScanningProcessorFactory;
21
+ variables: VariablesProcessorFactory;
19
22
  }
20
23
  /**
21
24
  * Dependencies for the sync command (dependency injection).
@@ -41,11 +44,7 @@ export interface SyncOptions extends SharedOptions {
41
44
  export interface SyncResultEntry {
42
45
  repoName: string;
43
46
  success: boolean;
44
- fileChanges: Array<{
45
- path: string;
46
- action: ActiveAction;
47
- diffLines?: string[];
48
- }>;
47
+ fileChanges: FileChangeDetail[];
49
48
  prUrl?: string;
50
49
  mergeOutcome?: MergeMode;
51
50
  error?: string;
@@ -69,4 +68,5 @@ export interface ApplyRepoSettingsContext {
69
68
  token: string | undefined;
70
69
  settingsCollector: ResultsCollector;
71
70
  factories: SettingsProcessorFactories;
71
+ logger: Logger;
72
72
  }