@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.
- package/README.md +1 -4
- package/dist/cli/index.d.ts +1 -4
- package/dist/cli/index.js +0 -2
- package/dist/cli/program.js +7 -14
- package/dist/cli/{settings/results-collector.d.ts → results-collector.d.ts} +1 -1
- package/dist/cli/{settings/results-collector.js → results-collector.js} +2 -1
- package/dist/cli/settings-report-builder.d.ts +1 -3
- package/dist/cli/sync-command.d.ts +3 -26
- package/dist/cli/sync-command.js +312 -179
- package/dist/cli/types.d.ts +68 -41
- package/dist/cli/types.js +1 -12
- package/dist/config/errors.d.ts +9 -0
- package/dist/config/errors.js +11 -0
- package/dist/config/file-reference-resolver.d.ts +2 -1
- package/dist/config/file-reference-resolver.js +10 -8
- package/dist/config/formatter.d.ts +3 -2
- package/dist/config/index.d.ts +4 -6
- package/dist/config/index.js +4 -8
- package/dist/config/loader.js +4 -2
- package/dist/config/merge.d.ts +0 -9
- package/dist/config/merge.js +2 -7
- package/dist/config/normalizer.d.ts +4 -0
- package/dist/config/normalizer.js +61 -110
- package/dist/config/types.d.ts +15 -19
- package/dist/config/types.js +1 -1
- package/dist/config/validator.d.ts +0 -9
- package/dist/config/validator.js +297 -391
- package/dist/config/validators/file-validator.d.ts +2 -8
- package/dist/config/validators/file-validator.js +6 -17
- package/dist/config/validators/index.d.ts +3 -3
- package/dist/config/validators/index.js +3 -3
- package/dist/config/validators/repo-settings-validator.d.ts +0 -6
- package/dist/config/validators/repo-settings-validator.js +9 -9
- package/dist/config/validators/ruleset-validator.d.ts +0 -14
- package/dist/config/validators/ruleset-validator.js +28 -28
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lifecycle/ado-migration-source.js +2 -1
- package/dist/lifecycle/github-lifecycle-provider.d.ts +15 -5
- package/dist/lifecycle/github-lifecycle-provider.js +101 -81
- package/dist/lifecycle/index.d.ts +2 -6
- package/dist/lifecycle/index.js +0 -4
- package/dist/lifecycle/lifecycle-formatter.d.ts +2 -1
- package/dist/lifecycle/lifecycle-formatter.js +4 -0
- package/dist/lifecycle/lifecycle-helpers.d.ts +3 -2
- package/dist/lifecycle/repo-lifecycle-manager.js +4 -11
- package/dist/lifecycle/types.d.ts +0 -8
- package/dist/output/github-summary.d.ts +5 -0
- package/dist/output/github-summary.js +9 -2
- package/dist/output/index.d.ts +2 -2
- package/dist/output/index.js +1 -1
- package/dist/output/lifecycle-report.js +5 -23
- package/dist/output/settings-report.d.ts +14 -3
- package/dist/output/settings-report.js +137 -197
- package/dist/output/summary-utils.d.ts +1 -1
- package/dist/output/summary-utils.js +2 -1
- package/dist/output/sync-report.js +5 -8
- package/dist/output/unified-summary.d.ts +2 -1
- package/dist/output/unified-summary.js +71 -133
- package/dist/settings/base-processor.d.ts +67 -0
- package/dist/settings/base-processor.js +91 -0
- package/dist/settings/index.d.ts +4 -3
- package/dist/settings/index.js +3 -3
- package/dist/settings/labels/converter.d.ts +2 -1
- package/dist/settings/labels/diff.d.ts +2 -2
- package/dist/settings/labels/diff.js +15 -19
- package/dist/settings/labels/github-labels-strategy.d.ts +9 -18
- package/dist/settings/labels/github-labels-strategy.js +17 -73
- package/dist/settings/labels/index.d.ts +2 -6
- package/dist/settings/labels/index.js +1 -9
- package/dist/settings/labels/processor.d.ts +6 -40
- package/dist/settings/labels/processor.js +62 -165
- package/dist/settings/labels/types.d.ts +5 -8
- package/dist/settings/repo-settings/formatter.d.ts +2 -2
- package/dist/settings/repo-settings/formatter.js +6 -6
- package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +11 -12
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +32 -79
- package/dist/settings/repo-settings/index.d.ts +2 -5
- package/dist/settings/repo-settings/index.js +1 -9
- package/dist/settings/repo-settings/processor.d.ts +6 -27
- package/dist/settings/repo-settings/processor.js +51 -104
- package/dist/settings/repo-settings/types.d.ts +7 -9
- package/dist/settings/rulesets/diff-algorithm.d.ts +0 -4
- package/dist/settings/rulesets/diff-algorithm.js +1 -10
- package/dist/settings/rulesets/diff.d.ts +3 -3
- package/dist/settings/rulesets/diff.js +8 -29
- package/dist/settings/rulesets/formatter.d.ts +1 -3
- package/dist/settings/rulesets/formatter.js +1 -8
- package/dist/settings/rulesets/github-ruleset-strategy.d.ts +11 -51
- package/dist/settings/rulesets/github-ruleset-strategy.js +24 -85
- package/dist/settings/rulesets/index.d.ts +3 -6
- package/dist/settings/rulesets/index.js +5 -9
- package/dist/settings/rulesets/processor.d.ts +8 -43
- package/dist/settings/rulesets/processor.js +58 -166
- package/dist/settings/rulesets/types.d.ts +35 -6
- package/dist/shared/command-executor.d.ts +2 -22
- package/dist/shared/command-executor.js +8 -7
- package/dist/shared/env.d.ts +0 -8
- package/dist/shared/env.js +14 -70
- package/dist/shared/file-status.d.ts +2 -0
- package/dist/shared/file-status.js +13 -0
- package/dist/shared/gh-api-utils.d.ts +46 -0
- package/dist/shared/gh-api-utils.js +107 -0
- package/dist/shared/index.d.ts +5 -5
- package/dist/shared/index.js +3 -3
- package/dist/shared/interpolation-engine.d.ts +31 -0
- package/dist/shared/interpolation-engine.js +50 -0
- package/dist/shared/logger.d.ts +3 -7
- package/dist/shared/logger.js +4 -1
- package/dist/shared/repo-detector.d.ts +17 -2
- package/dist/shared/repo-detector.js +27 -0
- package/dist/shared/retry-utils.d.ts +9 -17
- package/dist/shared/retry-utils.js +22 -28
- package/dist/shared/sanitize-utils.d.ts +0 -7
- package/dist/shared/sanitize-utils.js +0 -7
- package/dist/shared/shell-utils.d.ts +1 -0
- package/dist/shared/shell-utils.js +3 -0
- package/dist/shared/string-utils.d.ts +4 -0
- package/dist/shared/string-utils.js +6 -0
- package/dist/shared/type-guards.d.ts +17 -0
- package/dist/shared/type-guards.js +26 -0
- package/dist/shared/workspace-utils.d.ts +0 -4
- package/dist/shared/workspace-utils.js +0 -4
- package/dist/{sync → shared}/xfg-template.d.ts +3 -2
- package/dist/{sync → shared}/xfg-template.js +13 -54
- package/dist/sync/auth-options-builder.d.ts +4 -5
- package/dist/sync/auth-options-builder.js +15 -26
- package/dist/sync/branch-manager.d.ts +5 -0
- package/dist/sync/branch-manager.js +12 -10
- package/dist/sync/commit-push-manager.d.ts +1 -1
- package/dist/sync/commit-push-manager.js +22 -18
- package/dist/sync/diff-utils.d.ts +4 -9
- package/dist/sync/diff-utils.js +2 -19
- package/dist/sync/file-sync-orchestrator.js +9 -8
- package/dist/sync/file-writer.d.ts +2 -1
- package/dist/sync/file-writer.js +3 -6
- package/dist/sync/index.d.ts +2 -16
- package/dist/sync/index.js +0 -20
- package/dist/sync/manifest-manager.d.ts +4 -0
- package/dist/sync/manifest-manager.js +5 -1
- package/dist/sync/manifest.d.ts +11 -84
- package/dist/sync/manifest.js +50 -215
- package/dist/sync/pr-merge-handler.d.ts +2 -6
- package/dist/sync/pr-merge-handler.js +6 -3
- package/dist/sync/repository-processor.d.ts +2 -8
- package/dist/sync/repository-processor.js +21 -63
- package/dist/sync/repository-session.js +5 -14
- package/dist/sync/sync-workflow.js +31 -38
- package/dist/sync/types.d.ts +43 -182
- package/dist/vcs/authenticated-git-ops.d.ts +27 -70
- package/dist/vcs/authenticated-git-ops.js +70 -96
- package/dist/vcs/azure-pr-strategy.d.ts +6 -4
- package/dist/vcs/azure-pr-strategy.js +34 -82
- package/dist/vcs/branch-utils.d.ts +6 -0
- package/dist/vcs/branch-utils.js +29 -0
- package/dist/vcs/commit-strategy-selector.d.ts +5 -0
- package/dist/vcs/commit-strategy-selector.js +10 -0
- package/dist/vcs/git-commit-strategy.js +1 -2
- package/dist/vcs/git-ops.d.ts +15 -59
- package/dist/vcs/git-ops.js +46 -110
- package/dist/vcs/github-app-token-manager.d.ts +0 -6
- package/dist/vcs/github-app-token-manager.js +5 -12
- package/dist/vcs/github-pr-strategy.d.ts +5 -5
- package/dist/vcs/github-pr-strategy.js +44 -122
- package/dist/vcs/gitlab-pr-strategy.d.ts +6 -4
- package/dist/vcs/gitlab-pr-strategy.js +39 -87
- package/dist/vcs/graphql-commit-strategy.d.ts +3 -4
- package/dist/vcs/graphql-commit-strategy.js +45 -63
- package/dist/vcs/index.d.ts +3 -16
- package/dist/vcs/index.js +2 -33
- package/dist/vcs/pr-creator.d.ts +9 -9
- package/dist/vcs/pr-creator.js +11 -10
- package/dist/vcs/pr-strategy-factory.d.ts +5 -0
- package/dist/vcs/pr-strategy-factory.js +17 -0
- package/dist/vcs/pr-strategy.d.ts +13 -26
- package/dist/vcs/pr-strategy.js +20 -25
- package/dist/vcs/types.d.ts +87 -21
- package/package.json +2 -1
- package/dist/cli/settings/lifecycle-checks.d.ts +0 -11
- package/dist/cli/settings/lifecycle-checks.js +0 -64
- package/dist/cli/settings/process-labels.d.ts +0 -9
- package/dist/cli/settings/process-labels.js +0 -125
- package/dist/cli/settings/process-repo-settings.d.ts +0 -9
- package/dist/cli/settings/process-repo-settings.js +0 -80
- package/dist/cli/settings/process-rulesets.d.ts +0 -9
- package/dist/cli/settings/process-rulesets.js +0 -118
- package/dist/cli/settings-command.d.ts +0 -11
- package/dist/cli/settings-command.js +0 -90
- package/dist/sync/manifest-strategy.d.ts +0 -21
- 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
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
1
|
export { runSync } from "./sync-command.js";
|
|
2
|
-
export {
|
|
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 {
|
package/dist/cli/program.js
CHANGED
|
@@ -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
|
-
|
|
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 "
|
|
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 =
|
|
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/
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
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>;
|
package/dist/cli/sync-command.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { resolve, join } from "node:path";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { loadRawConfig, normalizeConfig
|
|
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/
|
|
7
|
-
import {
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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,
|
|
226
|
-
|
|
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
|
-
|
|
146
|
+
}
|
|
147
|
+
function displayReports(reportResults, lifecycleReportInputs, settingsCollector, dryRun) {
|
|
235
148
|
const lifecycleReport = buildLifecycleReport(lifecycleReportInputs);
|
|
236
149
|
if (hasLifecycleChanges(lifecycleReport)) {
|
|
237
|
-
|
|
150
|
+
logger.log("");
|
|
238
151
|
for (const line of formatLifecycleReportCLI(lifecycleReport)) {
|
|
239
|
-
|
|
152
|
+
logger.log(line);
|
|
240
153
|
}
|
|
241
154
|
}
|
|
242
|
-
// Build and display sync report
|
|
243
155
|
const report = buildSyncReport(reportResults);
|
|
244
|
-
|
|
156
|
+
logger.log("");
|
|
245
157
|
for (const line of formatSyncReportCLI(report)) {
|
|
246
|
-
|
|
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
|
-
|
|
177
|
+
settings: settingsReport,
|
|
178
|
+
dryRun,
|
|
253
179
|
});
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
}
|