@aspruyt/xfg 6.0.0 → 6.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/sync-command.js +33 -35
- package/dist/cli/types.d.ts +12 -11
- package/dist/{output → cli}/unified-summary.d.ts +3 -3
- package/dist/{output → cli}/unified-summary.js +4 -4
- package/dist/config/file-reference-resolver.js +24 -56
- package/dist/config/normalizer.js +29 -40
- package/dist/config/validator.js +94 -102
- package/dist/lifecycle/ado-migration-source.d.ts +1 -1
- package/dist/lifecycle/ado-migration-source.js +1 -1
- package/dist/lifecycle/github-lifecycle-provider.d.ts +5 -6
- package/dist/lifecycle/github-lifecycle-provider.js +50 -20
- package/dist/lifecycle/lifecycle-formatter.d.ts +2 -1
- package/dist/lifecycle/lifecycle-formatter.js +1 -1
- package/dist/lifecycle/lifecycle-helpers.d.ts +1 -1
- package/dist/lifecycle/repo-lifecycle-manager.d.ts +1 -1
- package/dist/lifecycle/repo-lifecycle-manager.js +16 -6
- package/dist/lifecycle/types.d.ts +30 -8
- package/dist/output/lifecycle-report.d.ts +4 -2
- package/dist/output/settings-report.d.ts +4 -4
- package/dist/repo/detector.d.ts +8 -0
- package/dist/{shared/repo-detector.js → repo/detector.js} +1 -4
- package/dist/repo/index.d.ts +4 -0
- package/dist/repo/index.js +3 -0
- package/dist/{shared/repo-metadata-provider.d.ts → repo/metadata-provider.d.ts} +3 -3
- package/dist/{shared/repo-metadata-provider.js → repo/metadata-provider.js} +3 -3
- package/dist/{shared/repo-detector.d.ts → repo/types.d.ts} +1 -7
- package/dist/repo/types.js +1 -0
- package/dist/{shared/repo-info-utils.d.ts → repo/utils.d.ts} +1 -1
- package/dist/{shared/repo-info-utils.js → repo/utils.js} +1 -1
- package/dist/settings/base-processor.d.ts +1 -1
- package/dist/settings/base-processor.js +1 -1
- package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +1 -1
- package/dist/settings/code-scanning/github-code-scanning-strategy.js +1 -1
- package/dist/settings/code-scanning/processor.d.ts +2 -2
- package/dist/settings/code-scanning/types.d.ts +1 -1
- package/dist/settings/index.d.ts +1 -1
- package/dist/settings/labels/formatter.js +16 -11
- package/dist/settings/labels/github-labels-strategy.d.ts +1 -1
- package/dist/settings/labels/github-labels-strategy.js +1 -1
- package/dist/settings/labels/processor.d.ts +1 -1
- package/dist/settings/labels/types.d.ts +1 -1
- package/dist/settings/repo-settings/diff.d.ts +1 -1
- package/dist/settings/repo-settings/diff.js +1 -1
- package/dist/settings/repo-settings/formatter.js +2 -4
- package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +4 -4
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +4 -4
- package/dist/settings/repo-settings/processor.d.ts +2 -2
- package/dist/settings/repo-settings/processor.js +5 -5
- package/dist/settings/repo-settings/types.d.ts +4 -4
- package/dist/settings/rulesets/diff-algorithm.js +1 -1
- package/dist/settings/rulesets/formatter.js +0 -3
- package/dist/settings/rulesets/github-ruleset-strategy.d.ts +1 -1
- package/dist/settings/rulesets/github-ruleset-strategy.js +1 -1
- package/dist/settings/rulesets/processor.d.ts +1 -1
- package/dist/settings/rulesets/types.d.ts +1 -1
- package/dist/shared/command-executor.js +3 -3
- package/dist/shared/gh-api-utils.d.ts +7 -4
- package/dist/shared/gh-api-utils.js +2 -2
- package/dist/shared/retry-utils.js +1 -1
- package/dist/shared/xfg-template.d.ts +22 -2
- package/dist/sync/auth-options-builder.d.ts +1 -1
- package/dist/sync/auth-options-builder.js +1 -1
- package/dist/sync/branch-manager.d.ts +1 -1
- package/dist/sync/commit-push-manager.d.ts +2 -2
- package/dist/sync/commit-push-manager.js +5 -3
- package/dist/sync/file-sync-orchestrator.d.ts +1 -1
- package/dist/sync/file-sync-strategy.d.ts +1 -1
- package/dist/sync/file-writer.js +44 -10
- package/dist/sync/repository-processor.d.ts +1 -1
- package/dist/sync/repository-session.d.ts +1 -1
- package/dist/sync/sync-workflow.d.ts +1 -1
- package/dist/sync/sync-workflow.js +2 -1
- package/dist/sync/types.d.ts +7 -4
- package/dist/vcs/{azure-pr-strategy.d.ts → ado-pr-strategy.d.ts} +2 -2
- package/dist/vcs/{azure-pr-strategy.js → ado-pr-strategy.js} +4 -4
- package/dist/vcs/authenticated-git-ops.d.ts +2 -0
- package/dist/vcs/authenticated-git-ops.js +6 -0
- package/dist/vcs/commit-strategy-selector.d.ts +1 -1
- package/dist/vcs/commit-strategy-selector.js +1 -1
- package/dist/vcs/file-mode-fixup-commit-strategy.d.ts +8 -6
- package/dist/vcs/file-mode-fixup-commit-strategy.js +79 -30
- package/dist/vcs/git-ops.d.ts +15 -3
- package/dist/vcs/git-ops.js +57 -24
- package/dist/vcs/github-app-token-manager.d.ts +1 -1
- package/dist/vcs/github-pr-strategy.d.ts +1 -1
- package/dist/vcs/github-pr-strategy.js +4 -4
- package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
- package/dist/vcs/gitlab-pr-strategy.js +4 -4
- package/dist/vcs/graphql-commit-strategy.js +8 -3
- package/dist/vcs/index.d.ts +1 -1
- package/dist/vcs/pr-creator.d.ts +1 -1
- package/dist/vcs/pr-strategy-factory.d.ts +1 -1
- package/dist/vcs/pr-strategy-factory.js +3 -3
- package/dist/vcs/pr-strategy.d.ts +1 -1
- package/dist/vcs/pr-strategy.js +1 -1
- package/dist/vcs/types.d.ts +10 -3
- package/package.json +3 -3
- /package/dist/{shared → vcs}/sanitize-utils.d.ts +0 -0
- /package/dist/{shared → vcs}/sanitize-utils.js +0 -0
package/dist/config/validator.js
CHANGED
|
@@ -134,79 +134,87 @@ function buildRootSettingsContext(config) {
|
|
|
134
134
|
/**
|
|
135
135
|
* Validates settings object containing rulesets, labels, and repo settings.
|
|
136
136
|
*/
|
|
137
|
-
function
|
|
138
|
-
if (
|
|
139
|
-
|
|
137
|
+
function validateSettingsRulesets(settings, context, rootCtx) {
|
|
138
|
+
if (settings.rulesets === undefined)
|
|
139
|
+
return;
|
|
140
|
+
if (!isPlainObject(settings.rulesets)) {
|
|
141
|
+
throw new ValidationError(`${context}: rulesets must be an object`);
|
|
140
142
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (name === "inherit")
|
|
149
|
-
continue;
|
|
150
|
-
if (ruleset === false) {
|
|
151
|
-
if (rootCtx && !rootCtx.rulesetNames.includes(name)) {
|
|
152
|
-
throw new ValidationError(`${context}: Cannot opt out of '${name}' - not defined in root settings.rulesets`);
|
|
153
|
-
}
|
|
154
|
-
continue; // Skip further validation for false entries
|
|
143
|
+
const rulesets = settings.rulesets;
|
|
144
|
+
for (const [name, ruleset] of Object.entries(rulesets)) {
|
|
145
|
+
if (name === "inherit")
|
|
146
|
+
continue;
|
|
147
|
+
if (ruleset === false) {
|
|
148
|
+
if (rootCtx && !rootCtx.rulesetNames.includes(name)) {
|
|
149
|
+
throw new ValidationError(`${context}: Cannot opt out of '${name}' - not defined in root settings.rulesets`);
|
|
155
150
|
}
|
|
156
|
-
|
|
151
|
+
continue;
|
|
157
152
|
}
|
|
153
|
+
validateRuleset(ruleset, name, context);
|
|
158
154
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
155
|
+
}
|
|
156
|
+
function validateSettingsLabels(settings, context, rootCtx) {
|
|
157
|
+
if (settings.labels === undefined)
|
|
158
|
+
return;
|
|
159
|
+
if (!isPlainObject(settings.labels)) {
|
|
160
|
+
throw new ValidationError(`${context}: labels must be an object`);
|
|
161
|
+
}
|
|
162
|
+
const labels = settings.labels;
|
|
163
|
+
for (const [name, label] of Object.entries(labels)) {
|
|
164
|
+
if (name === "inherit")
|
|
165
|
+
continue;
|
|
166
|
+
if (label === false) {
|
|
167
|
+
if (rootCtx && !rootCtx.labelNames.includes(name)) {
|
|
168
|
+
throw new ValidationError(`${context}: Cannot opt out of label '${name}' - not defined in root settings.labels`);
|
|
172
169
|
}
|
|
173
|
-
|
|
170
|
+
continue;
|
|
174
171
|
}
|
|
172
|
+
validateLabel(label, name, context);
|
|
175
173
|
}
|
|
174
|
+
}
|
|
175
|
+
function validateSettingsDeleteOrphaned(settings, context) {
|
|
176
176
|
if (settings.deleteOrphaned !== undefined &&
|
|
177
177
|
typeof settings.deleteOrphaned !== "boolean") {
|
|
178
178
|
throw new ValidationError(`${context}: settings.deleteOrphaned must be a boolean`);
|
|
179
179
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (!rootCtx.hasRepoSettings) {
|
|
188
|
-
throw new ValidationError(`${context}: Cannot opt out of repo settings — not defined in root settings.repo`);
|
|
189
|
-
}
|
|
190
|
-
// Valid opt-out, skip further repo validation
|
|
180
|
+
}
|
|
181
|
+
function validateSettingsRepo(settings, context, rootCtx) {
|
|
182
|
+
if (settings.repo === undefined)
|
|
183
|
+
return;
|
|
184
|
+
if (settings.repo === false) {
|
|
185
|
+
if (!rootCtx) {
|
|
186
|
+
throw new ValidationError(`${context}: repo: false is not valid at root level. Define repo settings or remove the field.`);
|
|
191
187
|
}
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
if (!rootCtx.hasRepoSettings) {
|
|
189
|
+
throw new ValidationError(`${context}: Cannot opt out of repo settings — not defined in root settings.repo`);
|
|
194
190
|
}
|
|
191
|
+
return;
|
|
195
192
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
193
|
+
validateRepoSettings(settings.repo, context);
|
|
194
|
+
}
|
|
195
|
+
function validateSettingsCodeScanning(settings, context, rootCtx) {
|
|
196
|
+
if (settings.codeScanning === undefined)
|
|
197
|
+
return;
|
|
198
|
+
if (settings.codeScanning === false) {
|
|
199
|
+
if (!rootCtx) {
|
|
200
|
+
throw new ValidationError(`${context}: codeScanning: false is not valid at root level. Define codeScanning settings or remove the field.`);
|
|
205
201
|
}
|
|
206
|
-
|
|
207
|
-
|
|
202
|
+
if (!rootCtx.hasCodeScanningSettings) {
|
|
203
|
+
throw new ValidationError(`${context}: Cannot opt out of code scanning settings — not defined in root settings.codeScanning`);
|
|
208
204
|
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
validateCodeScanningSettings(settings.codeScanning, `${context} codeScanning`);
|
|
208
|
+
}
|
|
209
|
+
function validateSettings(settings, context, rootCtx) {
|
|
210
|
+
if (!isPlainObject(settings)) {
|
|
211
|
+
throw new ValidationError(`${context}: settings must be an object`);
|
|
209
212
|
}
|
|
213
|
+
validateSettingsRulesets(settings, context, rootCtx);
|
|
214
|
+
validateSettingsLabels(settings, context, rootCtx);
|
|
215
|
+
validateSettingsDeleteOrphaned(settings, context);
|
|
216
|
+
validateSettingsRepo(settings, context, rootCtx);
|
|
217
|
+
validateSettingsCodeScanning(settings, context, rootCtx);
|
|
210
218
|
}
|
|
211
219
|
const VALID_CODE_SCANNING_STATES = ["configured", "not-configured"];
|
|
212
220
|
const VALID_CODE_SCANNING_QUERY_SUITES = ["default", "extended"];
|
|
@@ -565,9 +573,9 @@ function validateRepoFiles(config, repo, index, repoLabel) {
|
|
|
565
573
|
for (const groupName of expandedGroups) {
|
|
566
574
|
const group = config.groups[groupName];
|
|
567
575
|
if (group?.files) {
|
|
568
|
-
for (const
|
|
569
|
-
if (
|
|
570
|
-
knownFiles.add(
|
|
576
|
+
for (const fileName of Object.keys(group.files)) {
|
|
577
|
+
if (fileName !== "inherit")
|
|
578
|
+
knownFiles.add(fileName);
|
|
571
579
|
}
|
|
572
580
|
}
|
|
573
581
|
}
|
|
@@ -575,9 +583,9 @@ function validateRepoFiles(config, repo, index, repoLabel) {
|
|
|
575
583
|
if (config.conditionalGroups) {
|
|
576
584
|
for (const cg of config.conditionalGroups) {
|
|
577
585
|
if (cg.files) {
|
|
578
|
-
for (const
|
|
579
|
-
if (
|
|
580
|
-
knownFiles.add(
|
|
586
|
+
for (const fileName of Object.keys(cg.files)) {
|
|
587
|
+
if (fileName !== "inherit")
|
|
588
|
+
knownFiles.add(fileName);
|
|
581
589
|
}
|
|
582
590
|
}
|
|
583
591
|
}
|
|
@@ -604,6 +612,28 @@ function validateRepoFiles(config, repo, index, repoLabel) {
|
|
|
604
612
|
validateFileConfigFields(fileOverride, fileName, `Repo ${repoLabel}:`);
|
|
605
613
|
}
|
|
606
614
|
}
|
|
615
|
+
function enrichSettingsContext(rootCtx, settings) {
|
|
616
|
+
if (!settings)
|
|
617
|
+
return;
|
|
618
|
+
if (settings.rulesets) {
|
|
619
|
+
for (const name of Object.keys(settings.rulesets)) {
|
|
620
|
+
if (name !== "inherit")
|
|
621
|
+
rootCtx.rulesetNames.push(name);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (settings.labels) {
|
|
625
|
+
for (const name of Object.keys(settings.labels)) {
|
|
626
|
+
if (name !== "inherit")
|
|
627
|
+
rootCtx.labelNames.push(name);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (settings.repo !== undefined && settings.repo !== false) {
|
|
631
|
+
rootCtx.hasRepoSettings = true;
|
|
632
|
+
}
|
|
633
|
+
if (settings.codeScanning !== undefined && settings.codeScanning !== false) {
|
|
634
|
+
rootCtx.hasCodeScanningSettings = true;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
607
637
|
function validateRepoSettingsEntry(config, repo, repoLabel) {
|
|
608
638
|
if (repo.settings === undefined)
|
|
609
639
|
return;
|
|
@@ -611,50 +641,12 @@ function validateRepoSettingsEntry(config, repo, repoLabel) {
|
|
|
611
641
|
if (repo.groups && config.groups) {
|
|
612
642
|
const expandedGroups = expandRepoGroups(repo.groups, config.groups);
|
|
613
643
|
for (const groupName of expandedGroups) {
|
|
614
|
-
|
|
615
|
-
if (group?.settings?.rulesets) {
|
|
616
|
-
for (const name of Object.keys(group.settings.rulesets)) {
|
|
617
|
-
if (name !== "inherit")
|
|
618
|
-
rootCtx.rulesetNames.push(name);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
if (group?.settings?.labels) {
|
|
622
|
-
for (const name of Object.keys(group.settings.labels)) {
|
|
623
|
-
if (name !== "inherit")
|
|
624
|
-
rootCtx.labelNames.push(name);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
if (group?.settings?.repo !== undefined &&
|
|
628
|
-
group.settings.repo !== false) {
|
|
629
|
-
rootCtx.hasRepoSettings = true;
|
|
630
|
-
}
|
|
631
|
-
if (group?.settings?.codeScanning !== undefined &&
|
|
632
|
-
group.settings.codeScanning !== false) {
|
|
633
|
-
rootCtx.hasCodeScanningSettings = true;
|
|
634
|
-
}
|
|
644
|
+
enrichSettingsContext(rootCtx, config.groups[groupName]?.settings);
|
|
635
645
|
}
|
|
636
646
|
}
|
|
637
647
|
if (config.conditionalGroups) {
|
|
638
648
|
for (const cg of config.conditionalGroups) {
|
|
639
|
-
|
|
640
|
-
for (const name of Object.keys(cg.settings.rulesets)) {
|
|
641
|
-
if (name !== "inherit")
|
|
642
|
-
rootCtx.rulesetNames.push(name);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
if (cg.settings?.labels) {
|
|
646
|
-
for (const name of Object.keys(cg.settings.labels)) {
|
|
647
|
-
if (name !== "inherit")
|
|
648
|
-
rootCtx.labelNames.push(name);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
if (cg.settings?.repo !== undefined && cg.settings.repo !== false) {
|
|
652
|
-
rootCtx.hasRepoSettings = true;
|
|
653
|
-
}
|
|
654
|
-
if (cg.settings?.codeScanning !== undefined &&
|
|
655
|
-
cg.settings.codeScanning !== false) {
|
|
656
|
-
rootCtx.hasCodeScanningSettings = true;
|
|
657
|
-
}
|
|
649
|
+
enrichSettingsContext(rootCtx, cg.settings);
|
|
658
650
|
}
|
|
659
651
|
}
|
|
660
652
|
validateSettings(repo.settings, `Repo ${repoLabel}`, rootCtx);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ICommandExecutor } from "../shared/command-executor.js";
|
|
2
|
-
import { type RepoInfo } from "../
|
|
2
|
+
import { type RepoInfo } from "../repo/index.js";
|
|
3
3
|
import type { IMigrationSource, LifecyclePlatform } from "./types.js";
|
|
4
4
|
/**
|
|
5
5
|
* Azure DevOps implementation of IMigrationSource.
|
|
@@ -2,7 +2,7 @@ import { escapeShellArg } from "../shared/shell-utils.js";
|
|
|
2
2
|
import { withRetry } from "../shared/retry-utils.js";
|
|
3
3
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
4
4
|
import { LifecycleError } from "../shared/errors.js";
|
|
5
|
-
import { isAzureDevOpsRepo, } from "../
|
|
5
|
+
import { isAzureDevOpsRepo, } from "../repo/index.js";
|
|
6
6
|
/**
|
|
7
7
|
* Azure DevOps implementation of IMigrationSource.
|
|
8
8
|
* Uses git clone --mirror to get all refs for migration.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ICommandExecutor } from "../shared/command-executor.js";
|
|
2
|
-
import { type RepoInfo } from "../shared/repo-detector.js";
|
|
3
2
|
import type { DebugWarnLog } from "../shared/logger.js";
|
|
4
|
-
import type { IRepoLifecycleProvider, LifecyclePlatform,
|
|
3
|
+
import type { IRepoLifecycleProvider, LifecyclePlatform, LifecycleExistsParams, LifecycleCreateParams, LifecycleForkParams, LifecycleReceiveMigrationParams } from "./types.js";
|
|
5
4
|
/**
|
|
6
5
|
* GitHub implementation of IRepoLifecycleProvider.
|
|
7
6
|
* Uses gh CLI for all operations.
|
|
@@ -37,9 +36,9 @@ export declare class GitHubLifecycleProvider implements IRepoLifecycleProvider {
|
|
|
37
36
|
* (e.g., "gh api --hostname host repos/owner/repo").
|
|
38
37
|
*/
|
|
39
38
|
private buildGhApiPrefix;
|
|
40
|
-
exists(
|
|
41
|
-
create(
|
|
42
|
-
fork(
|
|
39
|
+
exists(params: LifecycleExistsParams): Promise<boolean>;
|
|
40
|
+
create(params: LifecycleCreateParams): Promise<void>;
|
|
41
|
+
fork(params: LifecycleForkParams): Promise<void>;
|
|
43
42
|
/**
|
|
44
43
|
* Wait for a forked repo to become available via the GitHub API.
|
|
45
44
|
* GitHub forks are created asynchronously; polls exists() with a timeout.
|
|
@@ -49,7 +48,7 @@ export declare class GitHubLifecycleProvider implements IRepoLifecycleProvider {
|
|
|
49
48
|
* Apply settings to an existing repo using gh repo edit.
|
|
50
49
|
*/
|
|
51
50
|
private applyRepoSettings;
|
|
52
|
-
receiveMigration(
|
|
51
|
+
receiveMigration(params: LifecycleReceiveMigrationParams): Promise<void>;
|
|
53
52
|
/**
|
|
54
53
|
* Rename a branch via the GitHub branch rename API.
|
|
55
54
|
* GitHub automatically updates the default branch pointer.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { escapeShellArg } from "../shared/shell-utils.js";
|
|
2
2
|
import { withRetry, isPermanentError, DEFAULT_PERMANENT_ERROR_PATTERNS, } from "../shared/retry-utils.js";
|
|
3
|
-
import { assertGitHubRepo, } from "../
|
|
3
|
+
import { assertGitHubRepo, } from "../repo/index.js";
|
|
4
4
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
5
5
|
import { LifecycleError } from "../shared/errors.js";
|
|
6
6
|
import { buildTokenEnv, getHostnameFlag } from "../shared/gh-api-utils.js";
|
|
@@ -29,8 +29,15 @@ const FORK_READY_TIMEOUT_MS = 60_000;
|
|
|
29
29
|
/**
|
|
30
30
|
* After repo creation, GitHub may return 404 due to eventual consistency.
|
|
31
31
|
* Exclude 404/not-found from permanent errors so withRetry retries them.
|
|
32
|
+
* The test string must trigger all not-found-family patterns in
|
|
33
|
+
* CORE_PERMANENT_ERROR_PATTERNS: /404\b/, /not\s*found/i,
|
|
34
|
+
* /repository\s*not\s*found/i, and /does\s*not\s*exist/i.
|
|
32
35
|
*/
|
|
33
|
-
const
|
|
36
|
+
const POST_CREATE_TEST_STRING = "404 Repository not found. Resource does not exist";
|
|
37
|
+
const POST_CREATE_PERMANENT_PATTERNS = [
|
|
38
|
+
...DEFAULT_PERMANENT_ERROR_PATTERNS.filter((p) => !p.test(POST_CREATE_TEST_STRING)),
|
|
39
|
+
/already\s*exists/i,
|
|
40
|
+
];
|
|
34
41
|
/**
|
|
35
42
|
* Interval between fork readiness checks (2 seconds).
|
|
36
43
|
*/
|
|
@@ -119,7 +126,8 @@ export class GitHubLifecycleProvider {
|
|
|
119
126
|
const apiPath = `repos/${escapeShellArg(repoInfo.owner)}/${escapeShellArg(repoInfo.repo)}`;
|
|
120
127
|
return { tokenEnv, prefix: `gh api ${hostnamePart}`, apiPath };
|
|
121
128
|
}
|
|
122
|
-
async exists(
|
|
129
|
+
async exists(params) {
|
|
130
|
+
const { repo: repoInfo, token } = params;
|
|
123
131
|
this.assertGitHub(repoInfo);
|
|
124
132
|
const { tokenEnv, prefix, apiPath } = this.buildGhApiPrefix(repoInfo, token);
|
|
125
133
|
const command = `${prefix}${apiPath}`;
|
|
@@ -140,7 +148,8 @@ export class GitHubLifecycleProvider {
|
|
|
140
148
|
throw error;
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
|
-
async create(
|
|
151
|
+
async create(params) {
|
|
152
|
+
const { repo: repoInfo, settings, token } = params;
|
|
144
153
|
this.assertGitHub(repoInfo);
|
|
145
154
|
const tokenEnv = buildTokenEnv(token);
|
|
146
155
|
const parts = [
|
|
@@ -175,7 +184,8 @@ export class GitHubLifecycleProvider {
|
|
|
175
184
|
// Delete the README so xfg sync starts from a clean state.
|
|
176
185
|
await this.deleteReadme(repoInfo, token);
|
|
177
186
|
}
|
|
178
|
-
async fork(
|
|
187
|
+
async fork(params) {
|
|
188
|
+
const { upstream, target, settings, token } = params;
|
|
179
189
|
this.assertGitHub(upstream);
|
|
180
190
|
this.assertGitHub(target);
|
|
181
191
|
// Guard: cannot fork a repo to the same owner
|
|
@@ -223,7 +233,7 @@ export class GitHubLifecycleProvider {
|
|
|
223
233
|
const deadline = Date.now() + timeoutMs;
|
|
224
234
|
while (Date.now() < deadline) {
|
|
225
235
|
try {
|
|
226
|
-
const ready = await this.exists(repoInfo, token);
|
|
236
|
+
const ready = await this.exists({ repo: repoInfo, token });
|
|
227
237
|
if (ready) {
|
|
228
238
|
return;
|
|
229
239
|
}
|
|
@@ -259,7 +269,8 @@ export class GitHubLifecycleProvider {
|
|
|
259
269
|
retries: this.retries,
|
|
260
270
|
});
|
|
261
271
|
}
|
|
262
|
-
async receiveMigration(
|
|
272
|
+
async receiveMigration(params) {
|
|
273
|
+
const { repo: repoInfo, sourceDir, settings, token } = params;
|
|
263
274
|
this.assertGitHub(repoInfo);
|
|
264
275
|
const tokenEnv = buildTokenEnv(token);
|
|
265
276
|
// Remove existing "origin" remote if present (e.g., from git clone --mirror).
|
|
@@ -302,21 +313,40 @@ export class GitHubLifecycleProvider {
|
|
|
302
313
|
await this.executor.exec(`git -C ${escapeShellArg(sourceDir)} symbolic-ref HEAD refs/heads/${escapeShellArg(settings.defaultBranch)}`, this.cwd);
|
|
303
314
|
}
|
|
304
315
|
}
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
"
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
// Split create and push into two steps. gh repo create --source --push
|
|
317
|
+
// does both atomically, but if the git backend hasn't propagated after
|
|
318
|
+
// the GraphQL create, the push fails with "Repository not found". A
|
|
319
|
+
// retry then hits "Name already exists" on the create step.
|
|
320
|
+
const repoSlug = `${repoInfo.owner}/${repoInfo.repo}`;
|
|
321
|
+
const createParts = ["gh repo create", escapeShellArg(repoSlug)];
|
|
322
|
+
buildRepoCreateFlags(createParts, settings);
|
|
323
|
+
try {
|
|
324
|
+
await withRetry(() => this.executor.exec(createParts.join(" "), this.cwd, {
|
|
325
|
+
env: tokenEnv,
|
|
326
|
+
}), {
|
|
327
|
+
retries: this.retries,
|
|
328
|
+
permanentErrorPatterns: POST_CREATE_PERMANENT_PATTERNS,
|
|
329
|
+
log: this.log
|
|
330
|
+
? { info: (m) => this.log.warn(m) }
|
|
331
|
+
: undefined,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (!/already\s*exists/i.test(toErrorMessage(error))) {
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Push mirror content via authenticated URL. Retries handle the git
|
|
340
|
+
// backend propagation delay (POST_CREATE_PERMANENT_PATTERNS allows
|
|
341
|
+
// retry on 404/not-found).
|
|
342
|
+
const remoteUrl = token
|
|
343
|
+
? `https://x-access-token:${token}@${repoInfo.host}/${repoSlug}.git`
|
|
344
|
+
: `https://${repoInfo.host}/${repoSlug}.git`;
|
|
345
|
+
await this.executor.exec(`git -C ${escapeShellArg(sourceDir)} remote add origin ${escapeShellArg(remoteUrl)}`, this.cwd);
|
|
346
|
+
await withRetry(() => this.executor.exec(`git -C ${escapeShellArg(sourceDir)} push --mirror origin`, this.cwd, { env: tokenEnv }), {
|
|
318
347
|
retries: this.retries,
|
|
319
348
|
permanentErrorPatterns: POST_CREATE_PERMANENT_PATTERNS,
|
|
349
|
+
log: this.log ? { info: (m) => this.log.warn(m) } : undefined,
|
|
320
350
|
});
|
|
321
351
|
}
|
|
322
352
|
/**
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { LifecycleResult } from "./types.js";
|
|
2
|
+
import type { RepoVisibility } from "../config/index.js";
|
|
2
3
|
interface FormatOptions {
|
|
3
4
|
upstream?: string;
|
|
4
5
|
source?: string;
|
|
5
6
|
settings?: {
|
|
6
|
-
visibility?:
|
|
7
|
+
visibility?: RepoVisibility;
|
|
7
8
|
description?: string;
|
|
8
9
|
};
|
|
9
10
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { getRepoDisplayName } from "../
|
|
2
|
+
import { getRepoDisplayName } from "../repo/index.js";
|
|
3
3
|
import { SyncError } from "../shared/errors.js";
|
|
4
4
|
/**
|
|
5
5
|
* Format lifecycle action for output (used in both dry-run and real execution).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RepoConfig, GitHubRepoSettings } from "../config/index.js";
|
|
2
|
-
import type { RepoInfo } from "../
|
|
2
|
+
import type { RepoInfo } from "../repo/index.js";
|
|
3
3
|
import type { IRepoLifecycleManager, CreateRepoSettings, LifecycleResult } from "./types.js";
|
|
4
4
|
interface LifecycleCheckOptions {
|
|
5
5
|
dryRun: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RepoInfo } from "../
|
|
1
|
+
import { type RepoInfo } from "../repo/index.js";
|
|
2
2
|
import { type DebugInfoWarnLog } from "../shared/logger.js";
|
|
3
3
|
import type { ICommandExecutor } from "../shared/command-executor.js";
|
|
4
4
|
import type { RepoConfig } from "../config/index.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { rm } from "node:fs/promises";
|
|
3
|
-
import { parseGitUrl } from "../
|
|
3
|
+
import { parseGitUrl } from "../repo/index.js";
|
|
4
4
|
import { safeCleanup } from "../shared/cleanup-utils.js";
|
|
5
5
|
import { LifecycleError } from "../shared/errors.js";
|
|
6
6
|
import { NO_OP_DEBUG_LOG } from "../shared/logger.js";
|
|
@@ -31,7 +31,7 @@ export class RepoLifecycleManager {
|
|
|
31
31
|
return { repoInfo, action: "existed" };
|
|
32
32
|
}
|
|
33
33
|
const { token } = options;
|
|
34
|
-
const exists = await provider.exists(repoInfo, token);
|
|
34
|
+
const exists = await provider.exists({ repo: repoInfo, token });
|
|
35
35
|
if (exists) {
|
|
36
36
|
// Repo exists - nothing to do (ignore upstream/source)
|
|
37
37
|
return {
|
|
@@ -59,7 +59,7 @@ export class RepoLifecycleManager {
|
|
|
59
59
|
skipped: true,
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
-
await provider.create(repoInfo, settings, options.token);
|
|
62
|
+
await provider.create({ repo: repoInfo, settings, token: options.token });
|
|
63
63
|
await this.waitForRepoReady(provider, repoInfo, options.token);
|
|
64
64
|
return {
|
|
65
65
|
repoInfo,
|
|
@@ -80,7 +80,12 @@ export class RepoLifecycleManager {
|
|
|
80
80
|
const upstreamInfo = parseGitUrl(repoConfig.upstream, {
|
|
81
81
|
githubHosts: options.githubHosts,
|
|
82
82
|
});
|
|
83
|
-
await provider.fork(
|
|
83
|
+
await provider.fork({
|
|
84
|
+
upstream: upstreamInfo,
|
|
85
|
+
target: repoInfo,
|
|
86
|
+
settings,
|
|
87
|
+
token: options.token,
|
|
88
|
+
});
|
|
84
89
|
await this.waitForRepoReady(provider, repoInfo, options.token);
|
|
85
90
|
return {
|
|
86
91
|
repoInfo,
|
|
@@ -106,7 +111,12 @@ export class RepoLifecycleManager {
|
|
|
106
111
|
await source.cloneForMigration(sourceInfo, sourceDir);
|
|
107
112
|
// Create target and push content
|
|
108
113
|
const provider = this.factory.getProvider(repoInfo.type);
|
|
109
|
-
await provider.receiveMigration(
|
|
114
|
+
await provider.receiveMigration({
|
|
115
|
+
repo: repoInfo,
|
|
116
|
+
sourceDir,
|
|
117
|
+
settings,
|
|
118
|
+
token: options.token,
|
|
119
|
+
});
|
|
110
120
|
await this.waitForRepoReady(provider, repoInfo, options.token);
|
|
111
121
|
return {
|
|
112
122
|
repoInfo,
|
|
@@ -125,7 +135,7 @@ export class RepoLifecycleManager {
|
|
|
125
135
|
async waitForRepoReady(provider, repoInfo, token, timeoutMs = 15000, pollMs = 1000) {
|
|
126
136
|
const start = Date.now();
|
|
127
137
|
while (Date.now() - start < timeoutMs) {
|
|
128
|
-
if (await provider.exists(repoInfo, token)) {
|
|
138
|
+
if (await provider.exists({ repo: repoInfo, token })) {
|
|
129
139
|
return;
|
|
130
140
|
}
|
|
131
141
|
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import type { RepoInfo } from "../
|
|
2
|
-
import type { RepoConfig } from "../config/index.js";
|
|
3
|
-
export type LifecyclePlatform =
|
|
1
|
+
import type { RepoInfo, RepoPlatform } from "../repo/index.js";
|
|
2
|
+
import type { RepoConfig, RepoVisibility } from "../config/index.js";
|
|
3
|
+
export type LifecyclePlatform = RepoPlatform;
|
|
4
4
|
export interface LifecycleResult {
|
|
5
5
|
repoInfo: RepoInfo;
|
|
6
6
|
action: "existed" | "created" | "forked" | "migrated";
|
|
7
7
|
skipped?: boolean;
|
|
8
8
|
}
|
|
9
|
+
export type LifecycleActionKind = LifecycleResult["action"];
|
|
9
10
|
export interface LifecycleOptions {
|
|
10
11
|
dryRun: boolean;
|
|
11
12
|
workDir: string;
|
|
@@ -18,12 +19,33 @@ export interface LifecycleOptions {
|
|
|
18
19
|
* Subset of GitHubRepoSettings that makes sense for creation.
|
|
19
20
|
*/
|
|
20
21
|
export interface CreateRepoSettings {
|
|
21
|
-
visibility?:
|
|
22
|
+
visibility?: RepoVisibility;
|
|
22
23
|
description?: string;
|
|
23
24
|
hasIssues?: boolean;
|
|
24
25
|
hasWiki?: boolean;
|
|
25
26
|
defaultBranch?: string;
|
|
26
27
|
}
|
|
28
|
+
export interface LifecycleExistsParams {
|
|
29
|
+
repo: RepoInfo;
|
|
30
|
+
token?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface LifecycleCreateParams {
|
|
33
|
+
repo: RepoInfo;
|
|
34
|
+
settings?: CreateRepoSettings;
|
|
35
|
+
token?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface LifecycleForkParams {
|
|
38
|
+
upstream: RepoInfo;
|
|
39
|
+
target: RepoInfo;
|
|
40
|
+
settings?: CreateRepoSettings;
|
|
41
|
+
token?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface LifecycleReceiveMigrationParams {
|
|
44
|
+
repo: RepoInfo;
|
|
45
|
+
sourceDir: string;
|
|
46
|
+
settings?: CreateRepoSettings;
|
|
47
|
+
token?: string;
|
|
48
|
+
}
|
|
27
49
|
/**
|
|
28
50
|
* Provider for platform-specific lifecycle operations.
|
|
29
51
|
* Implementations handle create/fork/receive for a specific platform.
|
|
@@ -34,20 +56,20 @@ export interface IRepoLifecycleProvider {
|
|
|
34
56
|
* Check if a repository exists on this platform.
|
|
35
57
|
* @throws LifecycleError on network/auth failures (NOT for "repo not found")
|
|
36
58
|
*/
|
|
37
|
-
exists(
|
|
59
|
+
exists(params: LifecycleExistsParams): Promise<boolean>;
|
|
38
60
|
/**
|
|
39
61
|
* Create an empty repository.
|
|
40
62
|
*/
|
|
41
|
-
create(
|
|
63
|
+
create(params: LifecycleCreateParams): Promise<void>;
|
|
42
64
|
/**
|
|
43
65
|
* Fork from an upstream repository.
|
|
44
66
|
* Optional - not all platforms support forking.
|
|
45
67
|
*/
|
|
46
|
-
fork?(
|
|
68
|
+
fork?(params: LifecycleForkParams): Promise<void>;
|
|
47
69
|
/**
|
|
48
70
|
* Receive migrated content (repo already created, push content).
|
|
49
71
|
*/
|
|
50
|
-
receiveMigration(
|
|
72
|
+
receiveMigration(params: LifecycleReceiveMigrationParams): Promise<void>;
|
|
51
73
|
}
|
|
52
74
|
/**
|
|
53
75
|
* Source for migration operations.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { LifecycleActionKind } from "../lifecycle/types.js";
|
|
2
|
+
import type { RepoVisibility } from "../config/index.js";
|
|
1
3
|
export interface LifecycleReport {
|
|
2
4
|
actions: LifecycleAction[];
|
|
3
5
|
totals: {
|
|
@@ -9,11 +11,11 @@ export interface LifecycleReport {
|
|
|
9
11
|
}
|
|
10
12
|
export interface LifecycleAction {
|
|
11
13
|
repoName: string;
|
|
12
|
-
action:
|
|
14
|
+
action: LifecycleActionKind;
|
|
13
15
|
upstream?: string;
|
|
14
16
|
source?: string;
|
|
15
17
|
settings?: {
|
|
16
|
-
visibility?:
|
|
18
|
+
visibility?: RepoVisibility;
|
|
17
19
|
description?: string;
|
|
18
20
|
};
|
|
19
21
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PropertyDiff } from "../settings/index.js";
|
|
1
|
+
import type { PropertyDiff, ActiveAction } from "../settings/index.js";
|
|
2
2
|
import type { Ruleset, Label } from "../config/index.js";
|
|
3
3
|
export interface SettingsReport {
|
|
4
4
|
repos: RepoChanges[];
|
|
@@ -28,19 +28,19 @@ export interface RepoChanges {
|
|
|
28
28
|
}
|
|
29
29
|
export interface SettingChange {
|
|
30
30
|
name: string;
|
|
31
|
-
action:
|
|
31
|
+
action: Exclude<ActiveAction, "delete">;
|
|
32
32
|
oldValue?: unknown;
|
|
33
33
|
newValue: unknown;
|
|
34
34
|
}
|
|
35
35
|
export interface RulesetChange {
|
|
36
36
|
name: string;
|
|
37
|
-
action:
|
|
37
|
+
action: ActiveAction;
|
|
38
38
|
propertyDiffs?: PropertyDiff[];
|
|
39
39
|
config?: Ruleset;
|
|
40
40
|
}
|
|
41
41
|
export interface LabelChange {
|
|
42
42
|
name: string;
|
|
43
|
-
action:
|
|
43
|
+
action: ActiveAction;
|
|
44
44
|
newName?: string;
|
|
45
45
|
propertyChanges?: {
|
|
46
46
|
property: string;
|