@aspruyt/xfg 3.13.1 → 4.0.1

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 (190) hide show
  1. package/README.md +1 -4
  2. package/dist/cli/index.d.ts +1 -4
  3. package/dist/cli/index.js +0 -2
  4. package/dist/cli/program.js +7 -14
  5. package/dist/cli/{settings/results-collector.d.ts → results-collector.d.ts} +1 -1
  6. package/dist/cli/{settings/results-collector.js → results-collector.js} +2 -1
  7. package/dist/cli/settings-report-builder.d.ts +1 -3
  8. package/dist/cli/sync-command.d.ts +3 -26
  9. package/dist/cli/sync-command.js +312 -179
  10. package/dist/cli/types.d.ts +68 -41
  11. package/dist/cli/types.js +1 -12
  12. package/dist/config/errors.d.ts +9 -0
  13. package/dist/config/errors.js +11 -0
  14. package/dist/config/file-reference-resolver.d.ts +2 -1
  15. package/dist/config/file-reference-resolver.js +10 -8
  16. package/dist/config/formatter.d.ts +3 -2
  17. package/dist/config/index.d.ts +4 -6
  18. package/dist/config/index.js +4 -8
  19. package/dist/config/loader.js +4 -2
  20. package/dist/config/merge.d.ts +0 -9
  21. package/dist/config/merge.js +2 -7
  22. package/dist/config/normalizer.d.ts +4 -0
  23. package/dist/config/normalizer.js +61 -110
  24. package/dist/config/types.d.ts +15 -19
  25. package/dist/config/types.js +1 -1
  26. package/dist/config/validator.d.ts +0 -9
  27. package/dist/config/validator.js +297 -391
  28. package/dist/config/validators/file-validator.d.ts +2 -8
  29. package/dist/config/validators/file-validator.js +6 -17
  30. package/dist/config/validators/index.d.ts +3 -3
  31. package/dist/config/validators/index.js +3 -3
  32. package/dist/config/validators/repo-settings-validator.d.ts +0 -6
  33. package/dist/config/validators/repo-settings-validator.js +9 -9
  34. package/dist/config/validators/ruleset-validator.d.ts +0 -14
  35. package/dist/config/validators/ruleset-validator.js +28 -28
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.js +1 -1
  38. package/dist/lifecycle/ado-migration-source.js +2 -1
  39. package/dist/lifecycle/github-lifecycle-provider.d.ts +15 -5
  40. package/dist/lifecycle/github-lifecycle-provider.js +101 -81
  41. package/dist/lifecycle/index.d.ts +2 -6
  42. package/dist/lifecycle/index.js +0 -4
  43. package/dist/lifecycle/lifecycle-formatter.d.ts +2 -1
  44. package/dist/lifecycle/lifecycle-formatter.js +4 -0
  45. package/dist/lifecycle/lifecycle-helpers.d.ts +3 -2
  46. package/dist/lifecycle/repo-lifecycle-manager.js +4 -11
  47. package/dist/lifecycle/types.d.ts +0 -8
  48. package/dist/output/github-summary.d.ts +5 -0
  49. package/dist/output/github-summary.js +9 -2
  50. package/dist/output/index.d.ts +2 -2
  51. package/dist/output/index.js +1 -1
  52. package/dist/output/lifecycle-report.js +5 -23
  53. package/dist/output/settings-report.d.ts +14 -3
  54. package/dist/output/settings-report.js +137 -197
  55. package/dist/output/summary-utils.d.ts +1 -1
  56. package/dist/output/summary-utils.js +2 -1
  57. package/dist/output/sync-report.js +5 -8
  58. package/dist/output/unified-summary.d.ts +2 -1
  59. package/dist/output/unified-summary.js +71 -133
  60. package/dist/settings/base-processor.d.ts +67 -0
  61. package/dist/settings/base-processor.js +91 -0
  62. package/dist/settings/index.d.ts +4 -3
  63. package/dist/settings/index.js +3 -3
  64. package/dist/settings/labels/converter.d.ts +2 -1
  65. package/dist/settings/labels/diff.d.ts +2 -2
  66. package/dist/settings/labels/diff.js +15 -19
  67. package/dist/settings/labels/github-labels-strategy.d.ts +9 -18
  68. package/dist/settings/labels/github-labels-strategy.js +17 -73
  69. package/dist/settings/labels/index.d.ts +2 -6
  70. package/dist/settings/labels/index.js +1 -9
  71. package/dist/settings/labels/processor.d.ts +6 -40
  72. package/dist/settings/labels/processor.js +62 -165
  73. package/dist/settings/labels/types.d.ts +5 -8
  74. package/dist/settings/repo-settings/formatter.d.ts +2 -2
  75. package/dist/settings/repo-settings/formatter.js +6 -6
  76. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +11 -12
  77. package/dist/settings/repo-settings/github-repo-settings-strategy.js +32 -79
  78. package/dist/settings/repo-settings/index.d.ts +2 -5
  79. package/dist/settings/repo-settings/index.js +1 -9
  80. package/dist/settings/repo-settings/processor.d.ts +6 -27
  81. package/dist/settings/repo-settings/processor.js +51 -104
  82. package/dist/settings/repo-settings/types.d.ts +7 -9
  83. package/dist/settings/rulesets/diff-algorithm.d.ts +0 -4
  84. package/dist/settings/rulesets/diff-algorithm.js +1 -10
  85. package/dist/settings/rulesets/diff.d.ts +3 -3
  86. package/dist/settings/rulesets/diff.js +8 -29
  87. package/dist/settings/rulesets/formatter.d.ts +1 -3
  88. package/dist/settings/rulesets/formatter.js +1 -8
  89. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +11 -51
  90. package/dist/settings/rulesets/github-ruleset-strategy.js +24 -85
  91. package/dist/settings/rulesets/index.d.ts +3 -6
  92. package/dist/settings/rulesets/index.js +5 -9
  93. package/dist/settings/rulesets/processor.d.ts +8 -43
  94. package/dist/settings/rulesets/processor.js +58 -166
  95. package/dist/settings/rulesets/types.d.ts +35 -6
  96. package/dist/shared/command-executor.d.ts +2 -22
  97. package/dist/shared/command-executor.js +8 -7
  98. package/dist/shared/env.d.ts +0 -8
  99. package/dist/shared/env.js +14 -70
  100. package/dist/shared/file-status.d.ts +2 -0
  101. package/dist/shared/file-status.js +13 -0
  102. package/dist/shared/gh-api-utils.d.ts +46 -0
  103. package/dist/shared/gh-api-utils.js +107 -0
  104. package/dist/shared/index.d.ts +5 -5
  105. package/dist/shared/index.js +3 -3
  106. package/dist/shared/interpolation-engine.d.ts +31 -0
  107. package/dist/shared/interpolation-engine.js +50 -0
  108. package/dist/shared/logger.d.ts +3 -7
  109. package/dist/shared/logger.js +4 -1
  110. package/dist/shared/repo-detector.d.ts +17 -2
  111. package/dist/shared/repo-detector.js +27 -0
  112. package/dist/shared/retry-utils.d.ts +9 -17
  113. package/dist/shared/retry-utils.js +22 -28
  114. package/dist/shared/sanitize-utils.d.ts +0 -7
  115. package/dist/shared/sanitize-utils.js +0 -7
  116. package/dist/shared/shell-utils.d.ts +1 -0
  117. package/dist/shared/shell-utils.js +3 -0
  118. package/dist/shared/string-utils.d.ts +4 -0
  119. package/dist/shared/string-utils.js +6 -0
  120. package/dist/shared/type-guards.d.ts +17 -0
  121. package/dist/shared/type-guards.js +26 -0
  122. package/dist/shared/workspace-utils.d.ts +0 -4
  123. package/dist/shared/workspace-utils.js +0 -4
  124. package/dist/{sync → shared}/xfg-template.d.ts +3 -2
  125. package/dist/{sync → shared}/xfg-template.js +13 -54
  126. package/dist/sync/auth-options-builder.d.ts +4 -5
  127. package/dist/sync/auth-options-builder.js +15 -26
  128. package/dist/sync/branch-manager.d.ts +5 -0
  129. package/dist/sync/branch-manager.js +12 -10
  130. package/dist/sync/commit-push-manager.d.ts +1 -1
  131. package/dist/sync/commit-push-manager.js +22 -18
  132. package/dist/sync/diff-utils.d.ts +4 -9
  133. package/dist/sync/diff-utils.js +2 -19
  134. package/dist/sync/file-sync-orchestrator.js +9 -8
  135. package/dist/sync/file-writer.d.ts +2 -1
  136. package/dist/sync/file-writer.js +3 -6
  137. package/dist/sync/index.d.ts +2 -16
  138. package/dist/sync/index.js +0 -20
  139. package/dist/sync/manifest-manager.d.ts +4 -0
  140. package/dist/sync/manifest-manager.js +5 -1
  141. package/dist/sync/manifest.d.ts +11 -84
  142. package/dist/sync/manifest.js +50 -215
  143. package/dist/sync/pr-merge-handler.d.ts +2 -6
  144. package/dist/sync/pr-merge-handler.js +6 -3
  145. package/dist/sync/repository-processor.d.ts +2 -8
  146. package/dist/sync/repository-processor.js +21 -63
  147. package/dist/sync/repository-session.js +5 -14
  148. package/dist/sync/sync-workflow.js +31 -38
  149. package/dist/sync/types.d.ts +43 -182
  150. package/dist/vcs/authenticated-git-ops.d.ts +27 -70
  151. package/dist/vcs/authenticated-git-ops.js +70 -96
  152. package/dist/vcs/azure-pr-strategy.d.ts +6 -4
  153. package/dist/vcs/azure-pr-strategy.js +34 -82
  154. package/dist/vcs/branch-utils.d.ts +6 -0
  155. package/dist/vcs/branch-utils.js +29 -0
  156. package/dist/vcs/commit-strategy-selector.d.ts +5 -0
  157. package/dist/vcs/commit-strategy-selector.js +10 -0
  158. package/dist/vcs/git-commit-strategy.js +1 -2
  159. package/dist/vcs/git-ops.d.ts +15 -59
  160. package/dist/vcs/git-ops.js +46 -110
  161. package/dist/vcs/github-app-token-manager.d.ts +0 -6
  162. package/dist/vcs/github-app-token-manager.js +5 -12
  163. package/dist/vcs/github-pr-strategy.d.ts +5 -5
  164. package/dist/vcs/github-pr-strategy.js +44 -122
  165. package/dist/vcs/gitlab-pr-strategy.d.ts +6 -4
  166. package/dist/vcs/gitlab-pr-strategy.js +39 -87
  167. package/dist/vcs/graphql-commit-strategy.d.ts +3 -4
  168. package/dist/vcs/graphql-commit-strategy.js +45 -63
  169. package/dist/vcs/index.d.ts +3 -16
  170. package/dist/vcs/index.js +2 -33
  171. package/dist/vcs/pr-creator.d.ts +9 -9
  172. package/dist/vcs/pr-creator.js +11 -10
  173. package/dist/vcs/pr-strategy-factory.d.ts +5 -0
  174. package/dist/vcs/pr-strategy-factory.js +17 -0
  175. package/dist/vcs/pr-strategy.d.ts +13 -26
  176. package/dist/vcs/pr-strategy.js +20 -25
  177. package/dist/vcs/types.d.ts +87 -21
  178. package/package.json +2 -1
  179. package/dist/cli/settings/lifecycle-checks.d.ts +0 -11
  180. package/dist/cli/settings/lifecycle-checks.js +0 -64
  181. package/dist/cli/settings/process-labels.d.ts +0 -9
  182. package/dist/cli/settings/process-labels.js +0 -125
  183. package/dist/cli/settings/process-repo-settings.d.ts +0 -9
  184. package/dist/cli/settings/process-repo-settings.js +0 -80
  185. package/dist/cli/settings/process-rulesets.d.ts +0 -9
  186. package/dist/cli/settings/process-rulesets.js +0 -118
  187. package/dist/cli/settings-command.d.ts +0 -11
  188. package/dist/cli/settings-command.js +0 -90
  189. package/dist/sync/manifest-strategy.d.ts +0 -21
  190. package/dist/sync/manifest-strategy.js +0 -67
package/README.md CHANGED
@@ -47,11 +47,8 @@ npm install -g @aspruyt/xfg
47
47
  # Authenticate (GitHub)
48
48
  gh auth login
49
49
 
50
- # Sync files across repos
50
+ # Sync files, settings, rulesets, and labels across repos
51
51
  xfg sync --config ./config.yaml
52
-
53
- # Apply repository settings and rulesets
54
- xfg settings --config ./config.yaml
55
52
  ```
56
53
 
57
54
  ### Example Config
@@ -1,6 +1,3 @@
1
1
  export { runSync } from "./sync-command.js";
2
- export { runSettings } from "./settings-command.js";
3
- export { program } from "./program.js";
4
- export { type IRepositoryProcessor, type ProcessorFactory, type IRulesetProcessor, type RulesetProcessorFactory, type RepoSettingsProcessorFactory, type IRepoSettingsProcessor, type ILabelsProcessor, type LabelsProcessorFactory, defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, defaultLabelsProcessorFactory, } from "./types.js";
2
+ export { type IRepositoryProcessor, type ProcessorFactory, type IRulesetProcessor, type RulesetProcessorFactory, type RepoSettingsProcessorFactory, type ILabelsProcessor, type LabelsProcessorFactory, defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, defaultLabelsProcessorFactory, } from "./types.js";
5
3
  export type { SyncOptions, SharedOptions } from "./sync-command.js";
6
- export type { SettingsOptions } from "./settings-command.js";
package/dist/cli/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  // CLI command implementations
2
2
  export { runSync } from "./sync-command.js";
3
- export { runSettings } from "./settings-command.js";
4
- export { program } from "./program.js";
5
3
  // Export types - using 'export type' for type aliases, but interfaces need special handling
6
4
  // For ESM compatibility, re-export everything from types.js
7
5
  export {
@@ -3,11 +3,15 @@ import { dirname, join } from "node:path";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { runSync } from "./sync-command.js";
6
- import { runSettings } from "./settings-command.js";
7
- // Get version from package.json
8
6
  const __filename = fileURLToPath(import.meta.url);
9
7
  const __dirname = dirname(__filename);
10
- const packageJson = JSON.parse(readFileSync(join(__dirname, "../..", "package.json"), "utf-8"));
8
+ let packageJson;
9
+ try {
10
+ packageJson = JSON.parse(readFileSync(join(__dirname, "../..", "package.json"), "utf-8"));
11
+ }
12
+ catch {
13
+ packageJson = { version: "0.0.0" };
14
+ }
11
15
  // =============================================================================
12
16
  // Shared CLI Options
13
17
  // =============================================================================
@@ -56,15 +60,4 @@ const syncCommand = new Command("sync")
56
60
  });
57
61
  addSharedOptions(syncCommand);
58
62
  program.addCommand(syncCommand);
59
- // Settings command (repository settings and rulesets)
60
- const settingsCommand = new Command("settings")
61
- .description("Manage GitHub repository settings and rulesets")
62
- .action((opts) => {
63
- runSettings(opts).catch((error) => {
64
- console.error("Fatal error:", error);
65
- process.exit(1);
66
- });
67
- });
68
- addSharedOptions(settingsCommand);
69
- program.addCommand(settingsCommand);
70
63
  export { program };
@@ -1,4 +1,4 @@
1
- import type { ProcessorResults } from "../settings-report-builder.js";
1
+ import type { ProcessorResults } from "./settings-report-builder.js";
2
2
  /**
3
3
  * Collects processing results for the SettingsReport.
4
4
  * Provides a centralized way to track results across rulesets, repo settings, and labels.
@@ -1,3 +1,4 @@
1
+ import { toErrorMessage } from "../shared/type-guards.js";
1
2
  /**
2
3
  * Collects processing results for the SettingsReport.
3
4
  * Provides a centralized way to track results across rulesets, repo settings, and labels.
@@ -14,7 +15,7 @@ export class ResultsCollector {
14
15
  }
15
16
  appendError(repoName, error) {
16
17
  const existing = this.getOrCreate(repoName);
17
- const errorMsg = error instanceof Error ? error.message : String(error);
18
+ const errorMsg = toErrorMessage(error);
18
19
  if (existing.error) {
19
20
  existing.error += `; ${errorMsg}`;
20
21
  }
@@ -1,7 +1,5 @@
1
1
  import type { SettingsReport } from "../output/settings-report.js";
2
- import type { RepoSettingsPlanEntry } from "../settings/repo-settings/formatter.js";
3
- import type { RulesetPlanEntry } from "../settings/rulesets/formatter.js";
4
- import type { LabelsPlanEntry } from "../settings/labels/formatter.js";
2
+ import type { RepoSettingsPlanEntry, RulesetPlanEntry, LabelsPlanEntry } from "../settings/index.js";
5
3
  /**
6
4
  * Result from processing a repository's settings and rulesets.
7
5
  * Used to collect results during settings command execution.
@@ -1,26 +1,3 @@
1
- import { MergeMode, MergeStrategy } from "../config/index.js";
2
- import { ProcessorFactory } from "./types.js";
3
- import { type IRepoLifecycleManager } from "../lifecycle/index.js";
4
- /**
5
- * Shared options common to all commands.
6
- */
7
- export interface SharedOptions {
8
- config: string;
9
- dryRun?: boolean;
10
- workDir?: string;
11
- retries?: number;
12
- noDelete?: boolean;
13
- }
14
- /**
15
- * Options specific to the sync command.
16
- */
17
- export interface SyncOptions extends SharedOptions {
18
- branch?: string;
19
- merge?: MergeMode;
20
- mergeStrategy?: MergeStrategy;
21
- deleteBranch?: boolean;
22
- }
23
- /**
24
- * Run the sync command - synchronizes files across repositories.
25
- */
26
- export declare function runSync(options: SyncOptions, processorFactory?: ProcessorFactory, lifecycleManager?: IRepoLifecycleManager): Promise<void>;
1
+ import { type SyncDependencies, type SyncOptions } from "./types.js";
2
+ export type { SharedOptions, SyncOptions } from "./types.js";
3
+ export declare function runSync(options: SyncOptions, deps?: SyncDependencies): Promise<void>;
@@ -1,17 +1,22 @@
1
1
  import { resolve, join } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
- import { loadRawConfig, normalizeConfig, } from "../config/index.js";
3
+ import { loadRawConfig, normalizeConfig } from "../config/index.js";
4
4
  import { validateForSync } from "../config/validator.js";
5
5
  import { parseGitUrl, getRepoDisplayName, isGitHubRepo, } from "../shared/repo-detector.js";
6
- import { sanitizeBranchName, validateBranchName } from "../vcs/git-ops.js";
7
- import { hasGitHubAppCredentials, GitHubAppTokenManager, } from "../vcs/index.js";
6
+ import { sanitizeBranchName, validateBranchName } from "../vcs/branch-utils.js";
7
+ import { createTokenManager } from "../vcs/index.js";
8
8
  import { logger } from "../shared/logger.js";
9
9
  import { generateWorkspaceName } from "../shared/workspace-utils.js";
10
- import { defaultProcessorFactory } from "./types.js";
10
+ import { defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, defaultLabelsProcessorFactory, } from "./types.js";
11
+ import { ResultsCollector } from "./results-collector.js";
12
+ import { buildSettingsReport } from "./settings-report-builder.js";
13
+ import { formatSettingsReportCLI } from "../output/settings-report.js";
11
14
  import { buildSyncReport } from "./sync-report-builder.js";
12
15
  import { formatSyncReportCLI } from "../output/sync-report.js";
13
16
  import { buildLifecycleReport, formatLifecycleReportCLI, hasLifecycleChanges, } from "../output/lifecycle-report.js";
14
17
  import { writeUnifiedSummary } from "../output/unified-summary.js";
18
+ import { toErrorMessage } from "../shared/type-guards.js";
19
+ import { resolveGitHubToken } from "../shared/gh-api-utils.js";
15
20
  import { RepoLifecycleManager, runLifecycleCheck, toCreateRepoSettings, } from "../lifecycle/index.js";
16
21
  /**
17
22
  * Get unique file names from all repos in the config
@@ -46,9 +51,6 @@ function formatFileNames(fileNames) {
46
51
  }
47
52
  return `${fileNames.length} files`;
48
53
  }
49
- /**
50
- * Determine merge outcome from processor result
51
- */
52
54
  function determineMergeOutcome(result) {
53
55
  if (!result.success)
54
56
  return undefined;
@@ -60,200 +62,331 @@ function determineMergeOutcome(result) {
60
62
  return "auto";
61
63
  return "manual";
62
64
  }
63
- /**
64
- * Run the sync command - synchronizes files across repositories.
65
- */
66
- export async function runSync(options, processorFactory = defaultProcessorFactory, lifecycleManager) {
67
- const configPath = resolve(options.config);
68
- if (!existsSync(configPath)) {
69
- console.error(`Config file not found: ${configPath}`);
70
- process.exit(1);
71
- }
72
- console.log(`Loading config from: ${configPath}`);
73
- if (options.dryRun) {
74
- console.log("Running in DRY RUN mode - no changes will be made\n");
75
- }
76
- const rawConfig = loadRawConfig(configPath);
77
- try {
78
- validateForSync(rawConfig);
79
- }
80
- catch (error) {
81
- console.error(error instanceof Error ? error.message : String(error));
82
- process.exit(1);
83
- }
84
- const config = normalizeConfig(rawConfig);
85
- const fileNames = getUniqueFileNames(config);
86
- let branchName;
87
- if (options.branch) {
88
- validateBranchName(options.branch);
89
- branchName = options.branch;
90
- }
91
- else {
92
- branchName = generateBranchName(fileNames);
93
- }
94
- logger.setTotal(config.repos.length);
95
- console.log(`Found ${config.repos.length} repositories to process`);
96
- console.log(`Target files: ${formatFileNames(fileNames)}`);
97
- console.log(`Branch: ${branchName}\n`);
98
- const processor = processorFactory();
99
- const lm = lifecycleManager ?? new RepoLifecycleManager(undefined, options.retries);
100
- const tokenManager = hasGitHubAppCredentials()
101
- ? new GitHubAppTokenManager(process.env.XFG_GITHUB_APP_ID, process.env.XFG_GITHUB_APP_PRIVATE_KEY)
102
- : null;
103
- const reportResults = [];
104
- const lifecycleReportInputs = [];
105
- for (let i = 0; i < config.repos.length; i++) {
106
- const repoConfig = config.repos[i];
107
- if (options.merge || options.mergeStrategy || options.deleteBranch) {
108
- repoConfig.prOptions = {
109
- ...repoConfig.prOptions,
110
- merge: options.merge ?? repoConfig.prOptions?.merge,
111
- mergeStrategy: options.mergeStrategy ?? repoConfig.prOptions?.mergeStrategy,
112
- deleteBranch: options.deleteBranch ?? repoConfig.prOptions?.deleteBranch,
113
- };
114
- }
115
- const current = i + 1;
116
- let repoInfo;
117
- try {
118
- repoInfo = parseGitUrl(repoConfig.git, {
119
- githubHosts: config.githubHosts,
120
- });
65
+ function logSettingsResult(result, label, current, repoName, settingsCollector) {
66
+ if (result.planOutput?.lines?.length) {
67
+ logger.info("");
68
+ logger.info(`${repoName} - ${label}:`);
69
+ for (const line of result.planOutput.lines) {
70
+ logger.info(line);
121
71
  }
122
- catch (error) {
123
- logger.error(current, repoConfig.git, String(error));
124
- reportResults.push({
125
- repoName: repoConfig.git,
126
- success: false,
127
- fileChanges: [],
128
- error: error instanceof Error ? error.message : String(error),
129
- });
130
- continue;
131
- }
132
- const repoName = getRepoDisplayName(repoInfo);
133
- const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(i)));
134
- // Resolve auth token for lifecycle gh commands
135
- let lifecycleToken;
136
- if (isGitHubRepo(repoInfo)) {
137
- try {
138
- lifecycleToken =
139
- (await tokenManager?.getTokenForRepo(repoInfo)) ??
140
- process.env.GH_TOKEN;
141
- }
142
- catch {
143
- lifecycleToken = process.env.GH_TOKEN;
72
+ if (result.warnings?.length) {
73
+ for (const warning of result.warnings) {
74
+ logger.warn(warning);
144
75
  }
145
76
  }
146
- // Check if repo exists, create/fork/migrate if needed
147
- try {
148
- const { outputLines, lifecycleResult } = await runLifecycleCheck(repoConfig, repoInfo, i, {
149
- dryRun: options.dryRun ?? false,
150
- resolvedWorkDir: workDir,
151
- githubHosts: config.githubHosts,
152
- token: lifecycleToken,
153
- }, lm, config.settings?.repo);
154
- for (const line of outputLines) {
155
- logger.info(line);
156
- }
157
- // Collect lifecycle result for report
158
- const createSettings = toCreateRepoSettings(config.settings?.repo);
159
- lifecycleReportInputs.push({
160
- repoName,
161
- action: lifecycleResult.action,
162
- upstream: repoConfig.upstream,
163
- source: repoConfig.source,
164
- settings: createSettings
165
- ? {
166
- visibility: createSettings.visibility,
167
- description: createSettings.description,
168
- }
169
- : undefined,
170
- });
171
- // In dry-run, skip processing repos that don't exist yet
172
- if (options.dryRun && lifecycleResult.action !== "existed") {
173
- reportResults.push({
174
- repoName,
175
- success: true,
176
- fileChanges: [],
77
+ }
78
+ else if (!result.skipped && result.success) {
79
+ logger.success(current, repoName, `${label}: ${result.message}`);
80
+ }
81
+ if (!result.success && !result.skipped) {
82
+ logger.error(current, repoName, `${label}: ${result.message}`);
83
+ settingsCollector.appendError(repoName, result.message);
84
+ }
85
+ }
86
+ async function applyRepoSettings(ctx) {
87
+ const { repoConfig, repoInfo, repoName, current, options, token, settingsCollector, rulesetProcessorFactory, repoSettingsProcessorFactory, labelsProcessorFactory, } = ctx;
88
+ if (!repoConfig.settings || !isGitHubRepo(repoInfo))
89
+ return;
90
+ const settingsDescriptors = [
91
+ {
92
+ key: "rulesets",
93
+ label: "Rulesets",
94
+ run: async () => {
95
+ const result = await rulesetProcessorFactory().process(repoConfig, repoInfo, {
96
+ dryRun: options.dryRun,
97
+ noDelete: options.noDelete,
98
+ token,
177
99
  });
178
- continue;
179
- }
180
- }
181
- catch (error) {
182
- logger.error(current, repoName, `Lifecycle error: ${error instanceof Error ? error.message : String(error)}`);
183
- reportResults.push({
184
- repoName,
185
- success: false,
186
- fileChanges: [],
187
- error: error instanceof Error ? error.message : String(error),
188
- });
100
+ if (!result.skipped) {
101
+ settingsCollector.getOrCreate(repoName).rulesetResult = result;
102
+ }
103
+ return result;
104
+ },
105
+ },
106
+ {
107
+ key: "labels",
108
+ label: "Labels",
109
+ run: async () => {
110
+ const result = await labelsProcessorFactory().process(repoConfig, repoInfo, {
111
+ dryRun: options.dryRun,
112
+ noDelete: options.noDelete,
113
+ token,
114
+ });
115
+ if (!result.skipped) {
116
+ settingsCollector.getOrCreate(repoName).labelsResult = result;
117
+ }
118
+ return result;
119
+ },
120
+ },
121
+ {
122
+ key: "repo",
123
+ label: "Repo Settings",
124
+ run: async () => {
125
+ const result = await repoSettingsProcessorFactory().process(repoConfig, repoInfo, { dryRun: options.dryRun, token });
126
+ if (!result.skipped) {
127
+ settingsCollector.getOrCreate(repoName).settingsResult = result;
128
+ }
129
+ return result;
130
+ },
131
+ },
132
+ ];
133
+ for (const desc of settingsDescriptors) {
134
+ const settingsValue = repoConfig.settings[desc.key];
135
+ if (!settingsValue || Object.keys(settingsValue).length === 0)
189
136
  continue;
190
- }
191
137
  try {
192
- logger.progress(current, repoName, "Processing...");
193
- const result = await processor.process(repoConfig, repoInfo, {
194
- branchName,
195
- workDir,
196
- configId: config.id,
197
- dryRun: options.dryRun,
198
- retries: options.retries,
199
- prTemplate: config.prTemplate,
200
- noDelete: options.noDelete,
201
- });
202
- const mergeOutcome = determineMergeOutcome(result);
203
- reportResults.push({
204
- repoName,
205
- success: result.success,
206
- fileChanges: (result.fileChanges ?? []).map((f) => ({
207
- path: f.path,
208
- action: f.action,
209
- })),
210
- prUrl: result.prUrl,
211
- mergeOutcome,
212
- error: result.success ? undefined : result.message,
213
- });
214
- if (result.skipped) {
215
- logger.skip(current, repoName, result.message);
216
- }
217
- else if (result.success) {
218
- logger.success(current, repoName, result.message);
219
- }
220
- else {
221
- logger.error(current, repoName, result.message);
222
- }
138
+ const result = await desc.run();
139
+ logSettingsResult(result, desc.label, current, repoName, settingsCollector);
223
140
  }
224
141
  catch (error) {
225
- logger.error(current, repoName, String(error));
226
- reportResults.push({
227
- repoName,
228
- success: false,
229
- fileChanges: [],
230
- error: error instanceof Error ? error.message : String(error),
231
- });
142
+ logger.error(current, repoName, `${desc.label}: ${toErrorMessage(error)}`);
143
+ settingsCollector.appendError(repoName, error);
232
144
  }
233
145
  }
234
- // Build and display lifecycle report (before sync report)
146
+ }
147
+ function displayReports(reportResults, lifecycleReportInputs, settingsCollector, dryRun) {
235
148
  const lifecycleReport = buildLifecycleReport(lifecycleReportInputs);
236
149
  if (hasLifecycleChanges(lifecycleReport)) {
237
- console.log("");
150
+ logger.log("");
238
151
  for (const line of formatLifecycleReportCLI(lifecycleReport)) {
239
- console.log(line);
152
+ logger.log(line);
240
153
  }
241
154
  }
242
- // Build and display sync report
243
155
  const report = buildSyncReport(reportResults);
244
- console.log("");
156
+ logger.log("");
245
157
  for (const line of formatSyncReportCLI(report)) {
246
- console.log(line);
158
+ logger.log(line);
159
+ }
160
+ // Build and display settings report (if any settings were processed)
161
+ const settingsResults = settingsCollector.getAll();
162
+ let settingsReport;
163
+ if (settingsResults.length > 0) {
164
+ settingsReport = buildSettingsReport(settingsResults);
165
+ const settingsLines = formatSettingsReportCLI(settingsReport);
166
+ if (settingsLines.length > 0) {
167
+ logger.log("");
168
+ for (const line of settingsLines) {
169
+ logger.log(line);
170
+ }
171
+ }
247
172
  }
248
173
  // Write unified summary to GITHUB_STEP_SUMMARY
249
174
  writeUnifiedSummary({
250
175
  lifecycle: lifecycleReport,
251
176
  sync: report,
252
- dryRun: options.dryRun ?? false,
177
+ settings: settingsReport,
178
+ dryRun,
253
179
  });
254
- // Exit with error if any failures
255
- const hasErrors = reportResults.some((r) => r.error);
256
- if (hasErrors) {
257
- process.exit(1);
180
+ }
181
+ /**
182
+ * Process a single repository: resolve URL, run lifecycle check, sync files, apply settings.
183
+ * Pushes results into ctx.reportResults, ctx.lifecycleReportInputs, and ctx.settingsCollector.
184
+ */
185
+ async function processSingleRepo(repoConfig, index, ctx) {
186
+ const { config, options } = ctx;
187
+ const current = index + 1;
188
+ // Apply CLI-level PR option overrides
189
+ if (options.merge || options.mergeStrategy || options.deleteBranch) {
190
+ repoConfig.prOptions = {
191
+ ...repoConfig.prOptions,
192
+ merge: options.merge ?? repoConfig.prOptions?.merge,
193
+ mergeStrategy: options.mergeStrategy ?? repoConfig.prOptions?.mergeStrategy,
194
+ deleteBranch: options.deleteBranch ?? repoConfig.prOptions?.deleteBranch,
195
+ };
196
+ }
197
+ const mergeMode = repoConfig.prOptions?.merge ?? "auto";
198
+ if (mergeMode === "direct" && repoConfig.prOptions?.mergeStrategy) {
199
+ logger.warn(`mergeStrategy '${repoConfig.prOptions.mergeStrategy}' is ignored in direct mode for ${repoConfig.git}`);
200
+ }
201
+ let repoInfo;
202
+ try {
203
+ repoInfo = parseGitUrl(repoConfig.git, {
204
+ githubHosts: config.githubHosts,
205
+ });
206
+ }
207
+ catch (error) {
208
+ logger.error(current, repoConfig.git, toErrorMessage(error));
209
+ ctx.reportResults.push({
210
+ repoName: repoConfig.git,
211
+ success: false,
212
+ fileChanges: [],
213
+ error: toErrorMessage(error),
214
+ });
215
+ return;
216
+ }
217
+ const repoName = getRepoDisplayName(repoInfo);
218
+ const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(index)));
219
+ const repoToken = isGitHubRepo(repoInfo)
220
+ ? (await resolveGitHubToken(repoInfo, ctx.tokenManager, repoName, logger, process.env.GH_TOKEN)).token
221
+ : undefined;
222
+ const skipFileSync = await runLifecyclePhase(repoConfig, repoInfo, repoName, index, workDir, repoToken, ctx);
223
+ if (skipFileSync)
224
+ return;
225
+ // Sync files via processor
226
+ await runFileSyncPhase(repoConfig, repoInfo, repoName, current, workDir, repoToken, ctx);
227
+ // Apply settings via API (GitHub-only — ADO and GitLab repos are skipped)
228
+ await applyRepoSettings({
229
+ repoConfig,
230
+ repoInfo,
231
+ repoName,
232
+ current,
233
+ options,
234
+ token: repoToken,
235
+ settingsCollector: ctx.settingsCollector,
236
+ rulesetProcessorFactory: ctx.rulesetProcessorFactory,
237
+ repoSettingsProcessorFactory: ctx.repoSettingsProcessorFactory,
238
+ labelsProcessorFactory: ctx.labelsProcessorFactory,
239
+ });
240
+ }
241
+ /**
242
+ * Run lifecycle check (repo existence, creation, forking).
243
+ * Returns true if the main loop should skip file sync for this repo.
244
+ */
245
+ async function runLifecyclePhase(repoConfig, repoInfo, repoName, index, workDir, lifecycleToken, ctx) {
246
+ const current = index + 1;
247
+ try {
248
+ const { outputLines, lifecycleResult } = await runLifecycleCheck(repoConfig, repoInfo, index, {
249
+ dryRun: ctx.options.dryRun ?? false,
250
+ resolvedWorkDir: workDir,
251
+ githubHosts: ctx.config.githubHosts,
252
+ token: lifecycleToken,
253
+ }, ctx.lifecycleManager, ctx.config.settings?.repo);
254
+ for (const line of outputLines) {
255
+ logger.info(line);
256
+ }
257
+ const createSettings = toCreateRepoSettings(ctx.config.settings?.repo);
258
+ ctx.lifecycleReportInputs.push({
259
+ repoName,
260
+ action: lifecycleResult.action,
261
+ upstream: repoConfig.upstream,
262
+ source: repoConfig.source,
263
+ settings: createSettings
264
+ ? {
265
+ visibility: createSettings.visibility,
266
+ description: createSettings.description,
267
+ }
268
+ : undefined,
269
+ });
270
+ // In dry-run, skip processing repos that don't exist yet
271
+ if (ctx.options.dryRun && lifecycleResult.action !== "existed") {
272
+ ctx.reportResults.push({
273
+ repoName,
274
+ success: true,
275
+ fileChanges: [],
276
+ });
277
+ return true;
278
+ }
279
+ return false;
280
+ }
281
+ catch (error) {
282
+ logger.error(current, repoName, `Lifecycle error: ${toErrorMessage(error)}`);
283
+ ctx.reportResults.push({
284
+ repoName,
285
+ success: false,
286
+ fileChanges: [],
287
+ error: toErrorMessage(error),
288
+ });
289
+ return true;
290
+ }
291
+ }
292
+ /**
293
+ * Run the file sync processor for a single repo and collect results.
294
+ */
295
+ async function runFileSyncPhase(repoConfig, repoInfo, repoName, current, workDir, token, ctx) {
296
+ try {
297
+ logger.progress(current, repoName, "Processing...");
298
+ const result = await ctx.processor.process(repoConfig, repoInfo, {
299
+ branchName: ctx.branchName,
300
+ workDir,
301
+ configId: ctx.config.id,
302
+ dryRun: ctx.options.dryRun,
303
+ retries: ctx.options.retries,
304
+ prTemplate: ctx.config.prTemplate,
305
+ noDelete: ctx.options.noDelete,
306
+ token,
307
+ isGraphQLCommitMode: isGitHubRepo(repoInfo) && ctx.tokenManager !== null,
308
+ });
309
+ const mergeOutcome = determineMergeOutcome(result);
310
+ ctx.reportResults.push({
311
+ repoName,
312
+ success: result.success,
313
+ fileChanges: (result.fileChanges ?? []).map((f) => ({
314
+ path: f.path,
315
+ action: f.action,
316
+ })),
317
+ prUrl: result.prUrl,
318
+ mergeOutcome,
319
+ error: result.success ? undefined : result.message,
320
+ });
321
+ if (result.skipped) {
322
+ logger.skip(current, repoName, result.message);
323
+ }
324
+ else if (result.success) {
325
+ logger.success(current, repoName, result.message);
326
+ }
327
+ else {
328
+ logger.error(current, repoName, result.message);
329
+ }
330
+ }
331
+ catch (error) {
332
+ logger.error(current, repoName, toErrorMessage(error));
333
+ ctx.reportResults.push({
334
+ repoName,
335
+ success: false,
336
+ fileChanges: [],
337
+ error: toErrorMessage(error),
338
+ });
339
+ }
340
+ }
341
+ export async function runSync(options, deps = {}) {
342
+ const { processorFactory = defaultProcessorFactory, lifecycleManager, rulesetProcessorFactory = defaultRulesetProcessorFactory, repoSettingsProcessorFactory = defaultRepoSettingsProcessorFactory, labelsProcessorFactory = defaultLabelsProcessorFactory, } = deps;
343
+ const configPath = resolve(options.config);
344
+ if (!existsSync(configPath)) {
345
+ throw new Error(`Config file not found: ${configPath}`);
346
+ }
347
+ logger.log(`Loading config from: ${configPath}`);
348
+ if (options.dryRun) {
349
+ logger.log("Running in DRY RUN mode - no changes will be made\n");
350
+ }
351
+ const rawConfig = loadRawConfig(configPath);
352
+ validateForSync(rawConfig);
353
+ const config = normalizeConfig(rawConfig);
354
+ const fileNames = getUniqueFileNames(config);
355
+ let branchName;
356
+ if (options.branch) {
357
+ validateBranchName(options.branch);
358
+ branchName = options.branch;
359
+ }
360
+ else {
361
+ branchName = generateBranchName(fileNames);
362
+ }
363
+ logger.setTotal(config.repos.length);
364
+ logger.log(`Found ${config.repos.length} repositories to process`);
365
+ logger.log(`Target files: ${formatFileNames(fileNames)}`);
366
+ logger.log(`Branch: ${branchName}\n`);
367
+ const ctx = {
368
+ config,
369
+ options,
370
+ branchName,
371
+ processor: processorFactory(),
372
+ lifecycleManager: lifecycleManager ?? new RepoLifecycleManager(undefined, options.retries),
373
+ tokenManager: createTokenManager(),
374
+ reportResults: [],
375
+ lifecycleReportInputs: [],
376
+ settingsCollector: new ResultsCollector(),
377
+ rulesetProcessorFactory,
378
+ repoSettingsProcessorFactory,
379
+ labelsProcessorFactory,
380
+ };
381
+ for (let i = 0; i < config.repos.length; i++) {
382
+ await processSingleRepo(config.repos[i], i, ctx);
383
+ }
384
+ displayReports(ctx.reportResults, ctx.lifecycleReportInputs, ctx.settingsCollector, options.dryRun ?? false);
385
+ // Propagate failures to caller (CLI entry handles process.exit)
386
+ const settingsResults = ctx.settingsCollector.getAll();
387
+ const hasErrors = ctx.reportResults.some((r) => r.error);
388
+ const hasSettingsErrors = settingsResults.some((r) => r.error);
389
+ if (hasErrors || hasSettingsErrors) {
390
+ throw new Error("One or more repositories had errors during sync");
258
391
  }
259
392
  }