@aspruyt/xfg 6.0.3 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/lifecycle-report-builder.d.ts +2 -2
- package/dist/cli/lifecycle-report-builder.js +3 -11
- package/dist/cli/program.d.ts +2 -1
- package/dist/cli/program.js +2 -3
- package/dist/cli/repo-sync-runner.d.ts +24 -0
- package/dist/cli/repo-sync-runner.js +156 -0
- package/dist/cli/results-collector.d.ts +1 -1
- package/dist/cli/results-collector.js +2 -2
- package/dist/cli/settings-factories.d.ts +7 -0
- package/dist/cli/settings-factories.js +27 -0
- package/dist/cli/settings-report-builder.d.ts +1 -1
- package/dist/cli/settings-report-builder.js +12 -23
- package/dist/cli/settings-runner.d.ts +2 -0
- package/dist/cli/settings-runner.js +87 -0
- package/dist/cli/sync-command.d.ts +1 -1
- package/dist/cli/sync-command.js +31 -372
- package/dist/cli/sync-report-builder.d.ts +1 -1
- package/dist/cli/sync-utils.d.ts +8 -0
- package/dist/cli/sync-utils.js +36 -0
- package/dist/cli/types.d.ts +5 -7
- package/dist/cli/unified-summary.d.ts +1 -3
- package/dist/cli/unified-summary.js +7 -5
- package/dist/cli.js +2 -1
- package/dist/{shared → config}/env.js +2 -2
- package/dist/config/extends-resolver.js +4 -3
- package/dist/config/file-reference-resolver.js +4 -2
- package/dist/config/formatter.js +18 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/loader.js +30 -6
- package/dist/config/merge.d.ts +11 -1
- package/dist/config/merge.js +78 -6
- package/dist/config/normalizer.js +53 -38
- package/dist/config/validator.d.ts +1 -4
- package/dist/config/validator.js +13 -599
- package/dist/config/validators/file-validator.d.ts +2 -1
- package/dist/config/validators/file-validator.js +9 -1
- package/dist/config/validators/group-validator.d.ts +3 -0
- package/dist/config/validators/group-validator.js +167 -0
- package/dist/config/validators/repo-entry-validator.d.ts +2 -0
- package/dist/config/validators/repo-entry-validator.js +165 -0
- package/dist/config/validators/repo-settings-validator.js +18 -7
- package/dist/config/validators/ruleset-validator.js +2 -5
- package/dist/config/validators/shared.d.ts +11 -0
- package/dist/config/validators/shared.js +242 -0
- package/dist/lifecycle/ado-migration-source.js +2 -4
- package/dist/lifecycle/github-lifecycle-provider.d.ts +7 -11
- package/dist/lifecycle/github-lifecycle-provider.js +125 -136
- package/dist/lifecycle/{lifecycle-helpers.d.ts → helpers.d.ts} +5 -1
- package/dist/lifecycle/{lifecycle-helpers.js → helpers.js} +9 -8
- package/dist/lifecycle/index.d.ts +2 -2
- package/dist/lifecycle/index.js +1 -1
- package/dist/lifecycle/repo-lifecycle-factory.d.ts +2 -2
- package/dist/output/github-summary.js +2 -3
- package/dist/output/index.d.ts +4 -0
- package/dist/output/index.js +4 -0
- package/dist/output/lifecycle-report.d.ts +1 -1
- package/dist/output/lifecycle-report.js +5 -0
- package/dist/output/sync-report.d.ts +25 -3
- package/dist/output/sync-report.js +11 -11
- package/dist/settings/base-processor.d.ts +18 -7
- package/dist/settings/base-processor.js +26 -5
- package/dist/settings/code-scanning/diff.js +2 -2
- package/dist/settings/code-scanning/formatter.d.ts +2 -6
- package/dist/settings/code-scanning/formatter.js +2 -25
- package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +3 -7
- package/dist/settings/code-scanning/github-code-scanning-strategy.js +2 -2
- package/dist/settings/code-scanning/processor.js +6 -4
- package/dist/settings/code-scanning/types.d.ts +10 -8
- package/dist/settings/labels/github-labels-strategy.d.ts +3 -11
- package/dist/settings/labels/types.d.ts +12 -10
- package/dist/settings/repo-settings/diff.d.ts +1 -1
- package/dist/settings/repo-settings/diff.js +1 -1
- package/dist/settings/repo-settings/formatter.d.ts +2 -6
- package/dist/settings/repo-settings/formatter.js +4 -23
- package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -2
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +8 -7
- package/dist/settings/repo-settings/processor.js +11 -11
- package/dist/settings/repo-settings/types.d.ts +2 -2
- package/dist/settings/rulesets/diff-algorithm.js +4 -2
- package/dist/settings/rulesets/diff.js +2 -51
- package/dist/settings/rulesets/formatter.js +4 -0
- package/dist/settings/rulesets/github-ruleset-strategy.d.ts +3 -3
- package/dist/settings/rulesets/github-ruleset-strategy.js +4 -6
- package/dist/settings/rulesets/index.d.ts +1 -1
- package/dist/settings/rulesets/index.js +0 -2
- package/dist/settings/rulesets/processor.js +1 -1
- package/dist/settings/rulesets/types.d.ts +6 -2
- package/dist/shared/command-executor.d.ts +4 -4
- package/dist/shared/command-executor.js +9 -7
- package/dist/shared/diff-format.d.ts +1 -0
- package/dist/shared/diff-format.js +10 -0
- package/dist/shared/errors.d.ts +7 -4
- package/dist/shared/errors.js +8 -8
- package/dist/shared/gh-api-utils.d.ts +3 -34
- package/dist/shared/gh-api-utils.js +23 -53
- package/dist/shared/gh-token-utils.d.ts +26 -0
- package/dist/shared/gh-token-utils.js +32 -0
- package/dist/shared/json-utils.js +1 -1
- package/dist/shared/regex-utils.d.ts +1 -0
- package/dist/shared/regex-utils.js +3 -0
- package/dist/shared/retry-utils.d.ts +1 -0
- package/dist/shared/retry-utils.js +13 -7
- package/dist/sync/auth-options-builder.js +1 -1
- package/dist/sync/branch-manager.js +5 -3
- package/dist/sync/commit-push-manager.js +2 -3
- package/dist/sync/diff-utils.d.ts +0 -1
- package/dist/sync/diff-utils.js +5 -10
- package/dist/sync/file-sync-orchestrator.js +0 -2
- package/dist/sync/file-writer.d.ts +3 -0
- package/dist/sync/file-writer.js +84 -81
- package/dist/sync/index.d.ts +0 -1
- package/dist/sync/index.js +0 -1
- package/dist/sync/manifest.js +1 -1
- package/dist/sync/pr-merge-handler.js +6 -6
- package/dist/sync/sync-workflow.js +1 -1
- package/dist/sync/types.d.ts +2 -2
- package/dist/vcs/ado-pr-strategy.d.ts +3 -5
- package/dist/vcs/ado-pr-strategy.js +131 -33
- package/dist/vcs/authenticated-git-ops.js +45 -23
- package/dist/vcs/git-commit-strategy.js +10 -6
- package/dist/vcs/git-ops.js +30 -24
- package/dist/vcs/github-pr-strategy.d.ts +3 -2
- package/dist/vcs/github-pr-strategy.js +80 -30
- package/dist/vcs/gitlab-pr-strategy.d.ts +2 -5
- package/dist/vcs/gitlab-pr-strategy.js +88 -87
- package/dist/vcs/graphql-commit-strategy.d.ts +1 -5
- package/dist/vcs/graphql-commit-strategy.js +21 -37
- package/dist/vcs/pr-creator.js +9 -2
- package/dist/vcs/pr-strategy.d.ts +2 -3
- package/dist/vcs/pr-strategy.js +0 -1
- package/dist/vcs/types.d.ts +9 -5
- package/package.json +5 -5
- package/dist/config/validators/index.d.ts +0 -3
- package/dist/config/validators/index.js +0 -6
- package/dist/output/types.d.ts +0 -20
- package/dist/output/types.js +0 -1
- package/dist/shared/shell-utils.d.ts +0 -6
- package/dist/shared/shell-utils.js +0 -17
- /package/dist/{shared → config}/env.d.ts +0 -0
- /package/dist/lifecycle/{lifecycle-formatter.d.ts → formatter.d.ts} +0 -0
- /package/dist/lifecycle/{lifecycle-formatter.js → formatter.js} +0 -0
- /package/dist/{vcs → shared}/sanitize-utils.d.ts +0 -0
- /package/dist/{vcs → shared}/sanitize-utils.js +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { escapeShellArg } from "../shared/shell-utils.js";
|
|
2
1
|
import { withRetry } from "../shared/retry-utils.js";
|
|
3
2
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
4
3
|
import { LifecycleError } from "../shared/errors.js";
|
|
@@ -24,9 +23,8 @@ export class AdoMigrationSource {
|
|
|
24
23
|
}
|
|
25
24
|
async cloneForMigration(repoInfo, workDir) {
|
|
26
25
|
this.assertAdo(repoInfo);
|
|
27
|
-
const command = `git clone --mirror ${escapeShellArg(repoInfo.gitUrl)} ${escapeShellArg(workDir)}`;
|
|
28
26
|
try {
|
|
29
|
-
await withRetry(() => this.executor.exec(
|
|
27
|
+
await withRetry(() => this.executor.exec("git", ["clone", "--mirror", "--", repoInfo.gitUrl, workDir], this.cwd), {
|
|
30
28
|
retries: this.retries,
|
|
31
29
|
});
|
|
32
30
|
}
|
|
@@ -34,7 +32,7 @@ export class AdoMigrationSource {
|
|
|
34
32
|
const msg = toErrorMessage(error);
|
|
35
33
|
throw new LifecycleError(`Failed to clone migration source ${repoInfo.gitUrl}: ${msg}. ` +
|
|
36
34
|
`Ensure you have authentication configured for Azure DevOps ` +
|
|
37
|
-
`(e.g., AZURE_DEVOPS_EXT_PAT or git credential helper)
|
|
35
|
+
`(e.g., AZURE_DEVOPS_EXT_PAT or git credential helper).`, { cause: error });
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
38
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ICommandExecutor } from "../shared/command-executor.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { DebugInfoWarnLog } from "../shared/logger.js";
|
|
3
3
|
import type { IRepoLifecycleProvider, LifecyclePlatform, LifecycleExistsParams, LifecycleCreateParams, LifecycleForkParams, LifecycleReceiveMigrationParams } from "./types.js";
|
|
4
4
|
/**
|
|
5
5
|
* GitHub implementation of IRepoLifecycleProvider.
|
|
@@ -13,7 +13,7 @@ interface GitHubLifecycleProviderOptions {
|
|
|
13
13
|
forkReadyTimeoutMs?: number;
|
|
14
14
|
/** Poll interval in ms for fork readiness checks (default: 2000) */
|
|
15
15
|
forkPollIntervalMs?: number;
|
|
16
|
-
log?:
|
|
16
|
+
log?: DebugInfoWarnLog;
|
|
17
17
|
}
|
|
18
18
|
export declare class GitHubLifecycleProvider implements IRepoLifecycleProvider {
|
|
19
19
|
readonly platform: LifecyclePlatform;
|
|
@@ -30,15 +30,11 @@ export declare class GitHubLifecycleProvider implements IRepoLifecycleProvider {
|
|
|
30
30
|
*/
|
|
31
31
|
private isOrganization;
|
|
32
32
|
private assertGitHub;
|
|
33
|
-
/**
|
|
34
|
-
* Builds the common gh API command prefix parts for a given repo.
|
|
35
|
-
* Returns tokenEnv for exec options, and the command prefix string
|
|
36
|
-
* (e.g., "gh api --hostname host repos/owner/repo").
|
|
37
|
-
*/
|
|
38
33
|
private buildGhApiPrefix;
|
|
39
34
|
exists(params: LifecycleExistsParams): Promise<boolean>;
|
|
40
35
|
create(params: LifecycleCreateParams): Promise<void>;
|
|
41
36
|
fork(params: LifecycleForkParams): Promise<void>;
|
|
37
|
+
private pollWithDeadline;
|
|
42
38
|
/**
|
|
43
39
|
* Wait for a forked repo to become available via the GitHub API.
|
|
44
40
|
* GitHub forks are created asynchronously; polls exists() with a timeout.
|
|
@@ -49,6 +45,10 @@ export declare class GitHubLifecycleProvider implements IRepoLifecycleProvider {
|
|
|
49
45
|
*/
|
|
50
46
|
private applyRepoSettings;
|
|
51
47
|
receiveMigration(params: LifecycleReceiveMigrationParams): Promise<void>;
|
|
48
|
+
private removeOriginRemote;
|
|
49
|
+
private cleanNonStandardRefs;
|
|
50
|
+
private renameMirrorDefaultBranch;
|
|
51
|
+
private createRepoAndPushMirror;
|
|
52
52
|
/**
|
|
53
53
|
* Rename a branch via the GitHub branch rename API.
|
|
54
54
|
* GitHub automatically updates the default branch pointer.
|
|
@@ -57,10 +57,6 @@ export declare class GitHubLifecycleProvider implements IRepoLifecycleProvider {
|
|
|
57
57
|
/**
|
|
58
58
|
* Poll until the GitHub API reports the expected default branch.
|
|
59
59
|
* After a branch rename, the API may lag for a few seconds.
|
|
60
|
-
*
|
|
61
|
-
* Note: Uses the same executor.exec pattern as the rest of this class.
|
|
62
|
-
* The command arguments are constructed from trusted RepoInfo values
|
|
63
|
-
* (validated during config parsing), not user input.
|
|
64
60
|
*/
|
|
65
61
|
private waitForDefaultBranch;
|
|
66
62
|
/**
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { escapeShellArg } from "../shared/shell-utils.js";
|
|
2
1
|
import { withRetry, isPermanentError, DEFAULT_PERMANENT_ERROR_PATTERNS, } from "../shared/retry-utils.js";
|
|
3
2
|
import { assertGitHubRepo, } from "../repo/index.js";
|
|
4
3
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
5
4
|
import { LifecycleError } from "../shared/errors.js";
|
|
6
|
-
import { buildTokenEnv,
|
|
5
|
+
import { buildTokenEnv, buildHostnameArgs } from "../shared/gh-api-utils.js";
|
|
7
6
|
/**
|
|
8
7
|
* Error messages that indicate "repo not found" vs actual errors.
|
|
9
8
|
*/
|
|
@@ -42,24 +41,24 @@ const POST_CREATE_PERMANENT_PATTERNS = [
|
|
|
42
41
|
* Interval between fork readiness checks (2 seconds).
|
|
43
42
|
*/
|
|
44
43
|
const FORK_POLL_INTERVAL_MS = 2_000;
|
|
45
|
-
function
|
|
44
|
+
function buildRepoCreateArgs(args, settings) {
|
|
46
45
|
if (settings?.visibility === "public") {
|
|
47
|
-
|
|
46
|
+
args.push("--public");
|
|
48
47
|
}
|
|
49
48
|
else if (settings?.visibility === "internal") {
|
|
50
|
-
|
|
49
|
+
args.push("--internal");
|
|
51
50
|
}
|
|
52
51
|
else {
|
|
53
|
-
|
|
52
|
+
args.push("--private");
|
|
54
53
|
}
|
|
55
54
|
if (settings?.description) {
|
|
56
|
-
|
|
55
|
+
args.push("--description", settings.description);
|
|
57
56
|
}
|
|
58
57
|
if (settings?.hasIssues === false) {
|
|
59
|
-
|
|
58
|
+
args.push("--disable-issues");
|
|
60
59
|
}
|
|
61
60
|
if (settings?.hasWiki === false) {
|
|
62
|
-
|
|
61
|
+
args.push("--disable-wiki");
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
export class GitHubLifecycleProvider {
|
|
@@ -85,10 +84,11 @@ export class GitHubLifecycleProvider {
|
|
|
85
84
|
* Uses gh api to query the user/org endpoint.
|
|
86
85
|
*/
|
|
87
86
|
async isOrganization(owner, repoInfo, token) {
|
|
88
|
-
const { tokenEnv,
|
|
89
|
-
const command = `${prefix}users/${escapeShellArg(owner)}`;
|
|
87
|
+
const { tokenEnv, baseArgs } = this.buildGhApiPrefix(repoInfo, token);
|
|
90
88
|
try {
|
|
91
|
-
const stdout = await withRetry(() => this.executor.exec(
|
|
89
|
+
const stdout = await withRetry(() => this.executor.exec("gh", [...baseArgs, `users/${owner}`], this.cwd, {
|
|
90
|
+
env: tokenEnv,
|
|
91
|
+
}), { retries: this.retries });
|
|
92
92
|
const data = JSON.parse(stdout);
|
|
93
93
|
return data.type === "Organization";
|
|
94
94
|
}
|
|
@@ -114,27 +114,22 @@ export class GitHubLifecycleProvider {
|
|
|
114
114
|
assertGitHub(repoInfo) {
|
|
115
115
|
assertGitHubRepo(repoInfo, "GitHubLifecycleProvider");
|
|
116
116
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Builds the common gh API command prefix parts for a given repo.
|
|
119
|
-
* Returns tokenEnv for exec options, and the command prefix string
|
|
120
|
-
* (e.g., "gh api --hostname host repos/owner/repo").
|
|
121
|
-
*/
|
|
122
117
|
buildGhApiPrefix(repoInfo, token) {
|
|
123
118
|
const tokenEnv = buildTokenEnv(token);
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
return { tokenEnv, prefix: `gh api ${hostnamePart}`, apiPath };
|
|
119
|
+
const hostnameArgs = buildHostnameArgs(repoInfo);
|
|
120
|
+
const apiPath = `repos/${repoInfo.owner}/${repoInfo.repo}`;
|
|
121
|
+
return { tokenEnv, baseArgs: ["api", ...hostnameArgs], apiPath };
|
|
128
122
|
}
|
|
129
123
|
async exists(params) {
|
|
130
124
|
const { repo: repoInfo, token } = params;
|
|
131
125
|
this.assertGitHub(repoInfo);
|
|
132
|
-
const { tokenEnv,
|
|
133
|
-
const command = `${prefix}${apiPath}`;
|
|
126
|
+
const { tokenEnv, baseArgs, apiPath } = this.buildGhApiPrefix(repoInfo, token);
|
|
134
127
|
try {
|
|
135
128
|
// Note: withRetry already classifies 404/not-found as permanent errors,
|
|
136
129
|
// so retries are aborted immediately for non-existent repos.
|
|
137
|
-
await withRetry(() => this.executor.exec(
|
|
130
|
+
await withRetry(() => this.executor.exec("gh", [...baseArgs, apiPath], this.cwd, {
|
|
131
|
+
env: tokenEnv,
|
|
132
|
+
}), {
|
|
138
133
|
retries: this.retries,
|
|
139
134
|
});
|
|
140
135
|
return true;
|
|
@@ -152,23 +147,23 @@ export class GitHubLifecycleProvider {
|
|
|
152
147
|
const { repo: repoInfo, settings, token } = params;
|
|
153
148
|
this.assertGitHub(repoInfo);
|
|
154
149
|
const tokenEnv = buildTokenEnv(token);
|
|
155
|
-
const
|
|
156
|
-
"
|
|
157
|
-
|
|
150
|
+
const args = [
|
|
151
|
+
"repo",
|
|
152
|
+
"create",
|
|
153
|
+
`${repoInfo.owner}/${repoInfo.repo}`,
|
|
158
154
|
];
|
|
159
|
-
|
|
155
|
+
buildRepoCreateArgs(args, settings);
|
|
160
156
|
// Add --add-readme to establish the default branch via an initial commit.
|
|
161
157
|
// This avoids empty repos where HEAD doesn't resolve.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
await withRetry(() => this.executor.exec(command, this.cwd, { env: tokenEnv }), {
|
|
158
|
+
args.push("--add-readme");
|
|
159
|
+
await withRetry(() => this.executor.exec("gh", args, this.cwd, { env: tokenEnv }), {
|
|
165
160
|
retries: this.retries,
|
|
166
161
|
});
|
|
167
162
|
// Rename default branch if requested and it differs from what GitHub created.
|
|
168
163
|
if (settings?.defaultBranch) {
|
|
169
|
-
const { tokenEnv: branchTokenEnv,
|
|
164
|
+
const { tokenEnv: branchTokenEnv, baseArgs, apiPath, } = this.buildGhApiPrefix(repoInfo, token);
|
|
170
165
|
// Detect the actual default branch name
|
|
171
|
-
const actualBranch = (await withRetry(() => this.executor.exec(
|
|
166
|
+
const actualBranch = (await withRetry(() => this.executor.exec("gh", [...baseArgs, apiPath, "--jq", ".default_branch"], this.cwd, { env: branchTokenEnv }), {
|
|
172
167
|
retries: this.retries,
|
|
173
168
|
permanentErrorPatterns: POST_CREATE_PERMANENT_PATTERNS,
|
|
174
169
|
})).trim();
|
|
@@ -199,16 +194,12 @@ export class GitHubLifecycleProvider {
|
|
|
199
194
|
// Build fork command
|
|
200
195
|
// For orgs: gh repo fork <upstream> --org <target-org> --fork-name <name> --clone=false
|
|
201
196
|
// For users: gh repo fork <upstream> --fork-name <name> --clone=false
|
|
202
|
-
const
|
|
203
|
-
"gh repo fork",
|
|
204
|
-
escapeShellArg(`${upstream.owner}/${upstream.repo}`),
|
|
205
|
-
];
|
|
197
|
+
const forkArgs = ["repo", "fork", `${upstream.owner}/${upstream.repo}`];
|
|
206
198
|
if (isOrg) {
|
|
207
|
-
|
|
199
|
+
forkArgs.push("--org", target.owner);
|
|
208
200
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
await withRetry(() => this.executor.exec(forkCommand, this.cwd, { env: tokenEnv }), {
|
|
201
|
+
forkArgs.push("--fork-name", target.repo, "--clone=false");
|
|
202
|
+
await withRetry(() => this.executor.exec("gh", forkArgs, this.cwd, { env: tokenEnv }), {
|
|
212
203
|
retries: this.retries,
|
|
213
204
|
});
|
|
214
205
|
// GitHub forks are async - wait for the fork to be ready for git operations
|
|
@@ -222,131 +213,131 @@ export class GitHubLifecycleProvider {
|
|
|
222
213
|
await this.applyRepoSettings(target, settings, token);
|
|
223
214
|
}
|
|
224
215
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
* GitHub forks are created asynchronously; polls exists() with a timeout.
|
|
228
|
-
*/
|
|
229
|
-
async waitForForkReady(repoInfo, options) {
|
|
230
|
-
const timeoutMs = options?.timeoutMs ?? FORK_READY_TIMEOUT_MS;
|
|
231
|
-
const intervalMs = options?.pollMs ?? FORK_POLL_INTERVAL_MS;
|
|
232
|
-
const token = options?.token;
|
|
233
|
-
const deadline = Date.now() + timeoutMs;
|
|
216
|
+
async pollWithDeadline(check, opts) {
|
|
217
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
234
218
|
while (Date.now() < deadline) {
|
|
235
219
|
try {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
220
|
+
if (await check())
|
|
221
|
+
return true;
|
|
240
222
|
}
|
|
241
223
|
catch (error) {
|
|
242
|
-
this.log?.debug(`Polling
|
|
224
|
+
this.log?.debug(`Polling ${opts.debugLabel}: ${toErrorMessage(error)}`);
|
|
243
225
|
}
|
|
244
226
|
const remaining = deadline - Date.now();
|
|
245
227
|
if (remaining <= 0)
|
|
246
228
|
break;
|
|
247
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(
|
|
229
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(opts.pollMs, remaining)));
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Wait for a forked repo to become available via the GitHub API.
|
|
235
|
+
* GitHub forks are created asynchronously; polls exists() with a timeout.
|
|
236
|
+
*/
|
|
237
|
+
async waitForForkReady(repoInfo, options) {
|
|
238
|
+
const timeoutMs = options?.timeoutMs ?? FORK_READY_TIMEOUT_MS;
|
|
239
|
+
const pollMs = options?.pollMs ?? FORK_POLL_INTERVAL_MS;
|
|
240
|
+
const token = options?.token;
|
|
241
|
+
const ready = await this.pollWithDeadline(() => this.exists({ repo: repoInfo, token }), { timeoutMs, pollMs, debugLabel: "fork readiness" });
|
|
242
|
+
if (!ready) {
|
|
243
|
+
throw new LifecycleError(`Timed out waiting for fork ${repoInfo.owner}/${repoInfo.repo} to become available ` +
|
|
244
|
+
`after ${timeoutMs / 1000}s. The fork may still be processing on GitHub.`);
|
|
248
245
|
}
|
|
249
|
-
throw new LifecycleError(`Timed out waiting for fork ${repoInfo.owner}/${repoInfo.repo} to become available ` +
|
|
250
|
-
`after ${timeoutMs / 1000}s. The fork may still be processing on GitHub.`);
|
|
251
246
|
}
|
|
252
247
|
/**
|
|
253
248
|
* Apply settings to an existing repo using gh repo edit.
|
|
254
249
|
*/
|
|
255
250
|
async applyRepoSettings(repoInfo, settings, token) {
|
|
256
251
|
const tokenEnv = buildTokenEnv(token);
|
|
257
|
-
const
|
|
258
|
-
"gh repo edit",
|
|
259
|
-
escapeShellArg(`${repoInfo.owner}/${repoInfo.repo}`),
|
|
260
|
-
];
|
|
252
|
+
const args = ["repo", "edit", `${repoInfo.owner}/${repoInfo.repo}`];
|
|
261
253
|
if (settings.visibility) {
|
|
262
|
-
|
|
254
|
+
args.push("--visibility", settings.visibility, "--accept-visibility-change-consequences");
|
|
263
255
|
}
|
|
264
256
|
if (settings.description) {
|
|
265
|
-
|
|
257
|
+
args.push("--description", settings.description);
|
|
266
258
|
}
|
|
267
|
-
|
|
268
|
-
await withRetry(() => this.executor.exec(command, this.cwd, { env: tokenEnv }), {
|
|
259
|
+
await withRetry(() => this.executor.exec("gh", args, this.cwd, { env: tokenEnv }), {
|
|
269
260
|
retries: this.retries,
|
|
270
261
|
});
|
|
271
262
|
}
|
|
272
263
|
async receiveMigration(params) {
|
|
273
264
|
const { repo: repoInfo, sourceDir, settings, token } = params;
|
|
274
265
|
this.assertGitHub(repoInfo);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
266
|
+
await this.removeOriginRemote(sourceDir);
|
|
267
|
+
await this.cleanNonStandardRefs(sourceDir);
|
|
268
|
+
await this.renameMirrorDefaultBranch(sourceDir, settings?.defaultBranch);
|
|
269
|
+
await this.createRepoAndPushMirror(repoInfo, sourceDir, settings, token);
|
|
270
|
+
}
|
|
271
|
+
async removeOriginRemote(sourceDir) {
|
|
278
272
|
try {
|
|
279
|
-
await this.executor.exec(
|
|
273
|
+
await this.executor.exec("git", ["-C", sourceDir, "remote", "remove", "origin"], this.cwd);
|
|
280
274
|
}
|
|
281
275
|
catch (error) {
|
|
282
276
|
this.log?.debug(`Cleanup: remote remove origin skipped - ${toErrorMessage(error)}`);
|
|
283
277
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
// refs/keep-around/* etc. must be removed.
|
|
278
|
+
}
|
|
279
|
+
// Mirror clones include ALL refs from the source, but GitHub only
|
|
280
|
+
// accepts branches (refs/heads/*) and tags (refs/tags/*).
|
|
281
|
+
async cleanNonStandardRefs(sourceDir) {
|
|
289
282
|
try {
|
|
290
|
-
const allRefs = await this.executor.exec(
|
|
283
|
+
const allRefs = await this.executor.exec("git", ["-C", sourceDir, "for-each-ref", "--format=%(refname)"], this.cwd);
|
|
291
284
|
for (const ref of allRefs.split("\n").filter((r) => r.trim())) {
|
|
292
285
|
const trimmed = ref.trim();
|
|
293
286
|
if (!trimmed.startsWith("refs/heads/") &&
|
|
294
287
|
!trimmed.startsWith("refs/tags/")) {
|
|
295
|
-
await this.executor.exec(
|
|
288
|
+
await this.executor.exec("git", ["-C", sourceDir, "update-ref", "-d", trimmed], this.cwd);
|
|
296
289
|
}
|
|
297
290
|
}
|
|
298
291
|
}
|
|
299
292
|
catch (error) {
|
|
300
293
|
this.log?.debug(`Cleanup: ref cleanup skipped - ${toErrorMessage(error)}`);
|
|
301
294
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
295
|
+
}
|
|
296
|
+
async renameMirrorDefaultBranch(sourceDir, targetBranch) {
|
|
297
|
+
if (!targetBranch)
|
|
298
|
+
return;
|
|
299
|
+
const headRef = (await this.executor.exec("git", ["-C", sourceDir, "symbolic-ref", "HEAD"], this.cwd)).trim();
|
|
300
|
+
const prefix = "refs/heads/";
|
|
301
|
+
if (!headRef.startsWith(prefix)) {
|
|
302
|
+
throw new LifecycleError(`Mirror clone HEAD symbolic-ref is '${headRef}', expected to start with '${prefix}'. ` +
|
|
303
|
+
`Cannot rename default branch.`);
|
|
304
|
+
}
|
|
305
|
+
const sourceBranch = headRef.slice(prefix.length);
|
|
306
|
+
if (sourceBranch !== targetBranch) {
|
|
307
|
+
await this.executor.exec("git", ["-C", sourceDir, "branch", "-m", sourceBranch, targetBranch], this.cwd);
|
|
308
|
+
await this.executor.exec("git", ["-C", sourceDir, "symbolic-ref", "HEAD", `refs/heads/${targetBranch}`], this.cwd);
|
|
315
309
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
//
|
|
319
|
-
//
|
|
310
|
+
}
|
|
311
|
+
async createRepoAndPushMirror(repoInfo, sourceDir, settings, token) {
|
|
312
|
+
// Split create and push: gh repo create --source --push does both
|
|
313
|
+
// atomically, but if the git backend hasn't propagated after the
|
|
314
|
+
// GraphQL create, the push fails with "Repository not found".
|
|
320
315
|
const repoSlug = `${repoInfo.owner}/${repoInfo.repo}`;
|
|
321
|
-
const
|
|
322
|
-
|
|
316
|
+
const tokenEnv = buildTokenEnv(token);
|
|
317
|
+
const createArgs = ["repo", "create", repoSlug];
|
|
318
|
+
buildRepoCreateArgs(createArgs, settings);
|
|
323
319
|
try {
|
|
324
|
-
await withRetry(() => this.executor.exec(
|
|
325
|
-
env: tokenEnv,
|
|
326
|
-
}), {
|
|
320
|
+
await withRetry(() => this.executor.exec("gh", createArgs, this.cwd, { env: tokenEnv }), {
|
|
327
321
|
retries: this.retries,
|
|
328
322
|
permanentErrorPatterns: POST_CREATE_PERMANENT_PATTERNS,
|
|
329
323
|
log: this.log
|
|
330
|
-
? { info: (m) => this.log.
|
|
324
|
+
? { info: (m) => this.log.info(m) }
|
|
331
325
|
: undefined,
|
|
332
326
|
});
|
|
333
327
|
}
|
|
334
328
|
catch (error) {
|
|
335
|
-
if (
|
|
329
|
+
if (!isPermanentError(error, [/already\s*exists/i])) {
|
|
336
330
|
throw error;
|
|
337
331
|
}
|
|
338
332
|
}
|
|
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
333
|
const remoteUrl = token
|
|
343
334
|
? `https://x-access-token:${token}@${repoInfo.host}/${repoSlug}.git`
|
|
344
335
|
: `https://${repoInfo.host}/${repoSlug}.git`;
|
|
345
|
-
await this.executor.exec(
|
|
346
|
-
await withRetry(() => this.executor.exec(
|
|
336
|
+
await this.executor.exec("git", ["-C", sourceDir, "remote", "add", "origin", remoteUrl], this.cwd);
|
|
337
|
+
await withRetry(() => this.executor.exec("git", ["-C", sourceDir, "push", "--mirror", "origin"], this.cwd, { env: tokenEnv }), {
|
|
347
338
|
retries: this.retries,
|
|
348
339
|
permanentErrorPatterns: POST_CREATE_PERMANENT_PATTERNS,
|
|
349
|
-
log: this.log ? { info: (m) => this.log.
|
|
340
|
+
log: this.log ? { info: (m) => this.log.info(m) } : undefined,
|
|
350
341
|
});
|
|
351
342
|
}
|
|
352
343
|
/**
|
|
@@ -354,41 +345,31 @@ export class GitHubLifecycleProvider {
|
|
|
354
345
|
* GitHub automatically updates the default branch pointer.
|
|
355
346
|
*/
|
|
356
347
|
async renameBranch(repoInfo, current, desired, token) {
|
|
357
|
-
const { tokenEnv,
|
|
358
|
-
await withRetry(() => this.executor.exec(
|
|
359
|
-
|
|
348
|
+
const { tokenEnv, baseArgs, apiPath } = this.buildGhApiPrefix(repoInfo, token);
|
|
349
|
+
await withRetry(() => this.executor.exec("gh", [
|
|
350
|
+
...baseArgs,
|
|
351
|
+
`${apiPath}/branches/${current}/rename`,
|
|
352
|
+
"--method",
|
|
353
|
+
"POST",
|
|
354
|
+
"-f",
|
|
355
|
+
`new_name=${desired}`,
|
|
356
|
+
], this.cwd, { env: tokenEnv }), {
|
|
360
357
|
retries: this.retries,
|
|
361
358
|
});
|
|
362
359
|
}
|
|
363
360
|
/**
|
|
364
361
|
* Poll until the GitHub API reports the expected default branch.
|
|
365
362
|
* After a branch rename, the API may lag for a few seconds.
|
|
366
|
-
*
|
|
367
|
-
* Note: Uses the same executor.exec pattern as the rest of this class.
|
|
368
|
-
* The command arguments are constructed from trusted RepoInfo values
|
|
369
|
-
* (validated during config parsing), not user input.
|
|
370
363
|
*/
|
|
371
364
|
async waitForDefaultBranch(repoInfo, expectedBranch, options) {
|
|
372
365
|
const timeoutMs = options?.timeoutMs ?? 15000;
|
|
373
366
|
const pollMs = options?.pollMs ?? 1000;
|
|
374
|
-
const { tokenEnv,
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
catch (error) {
|
|
384
|
-
this.log?.debug(`Polling default branch: ${toErrorMessage(error)}`);
|
|
385
|
-
}
|
|
386
|
-
const remaining = deadline - Date.now();
|
|
387
|
-
if (remaining <= 0)
|
|
388
|
-
break;
|
|
389
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(pollMs, remaining)));
|
|
390
|
-
}
|
|
391
|
-
// Don't throw — rename succeeded, this is just a best-effort wait
|
|
367
|
+
const { tokenEnv, baseArgs, apiPath } = this.buildGhApiPrefix(repoInfo, options?.token);
|
|
368
|
+
// Best-effort wait — don't throw on timeout since rename already succeeded
|
|
369
|
+
await this.pollWithDeadline(async () => {
|
|
370
|
+
const branch = (await this.executor.exec("gh", [...baseArgs, apiPath, "--jq", ".default_branch"], this.cwd, { env: tokenEnv })).trim();
|
|
371
|
+
return branch === expectedBranch;
|
|
372
|
+
}, { timeoutMs, pollMs, debugLabel: "default branch" });
|
|
392
373
|
}
|
|
393
374
|
/**
|
|
394
375
|
* Delete the README.md that --add-readme creates.
|
|
@@ -396,16 +377,24 @@ export class GitHubLifecycleProvider {
|
|
|
396
377
|
* commit) but no files, so xfg sync starts from a clean state.
|
|
397
378
|
*/
|
|
398
379
|
async deleteReadme(repoInfo, token) {
|
|
399
|
-
const { tokenEnv,
|
|
380
|
+
const { tokenEnv, baseArgs, apiPath } = this.buildGhApiPrefix(repoInfo, token);
|
|
400
381
|
// Get the SHA of the README.md created by --add-readme
|
|
401
|
-
const fileInfo = await withRetry(() => this.executor.exec(`${
|
|
382
|
+
const fileInfo = await withRetry(() => this.executor.exec("gh", [...baseArgs, `${apiPath}/contents/README.md`, "--jq", ".sha"], this.cwd, { env: tokenEnv }), {
|
|
402
383
|
retries: this.retries,
|
|
403
384
|
permanentErrorPatterns: POST_CREATE_PERMANENT_PATTERNS,
|
|
404
385
|
});
|
|
405
386
|
const sha = fileInfo.trim();
|
|
406
387
|
// Delete the README.md to leave the repo clean
|
|
407
|
-
await withRetry(() => this.executor.exec(
|
|
408
|
-
|
|
388
|
+
await withRetry(() => this.executor.exec("gh", [
|
|
389
|
+
...baseArgs,
|
|
390
|
+
`${apiPath}/contents/README.md`,
|
|
391
|
+
"--method",
|
|
392
|
+
"DELETE",
|
|
393
|
+
"-f",
|
|
394
|
+
"message=Remove initialization file",
|
|
395
|
+
"-f",
|
|
396
|
+
`sha=${sha}`,
|
|
397
|
+
], this.cwd, { env: tokenEnv }), {
|
|
409
398
|
retries: this.retries,
|
|
410
399
|
permanentErrorPatterns: POST_CREATE_PERMANENT_PATTERNS,
|
|
411
400
|
});
|
|
@@ -20,10 +20,14 @@ interface LifecycleCheckOptions {
|
|
|
20
20
|
* Extracts only the fields relevant for repo creation.
|
|
21
21
|
*/
|
|
22
22
|
export declare function toCreateRepoSettings(repo: GitHubRepoSettings | undefined): CreateRepoSettings | undefined;
|
|
23
|
+
export interface LifecycleReportSettings {
|
|
24
|
+
visibility?: CreateRepoSettings["visibility"];
|
|
25
|
+
description?: CreateRepoSettings["description"];
|
|
26
|
+
}
|
|
23
27
|
export interface LifecycleCheckResult {
|
|
24
28
|
lifecycleResult: LifecycleResult;
|
|
25
29
|
outputLines: string[];
|
|
26
|
-
|
|
30
|
+
reportSettings: LifecycleReportSettings | undefined;
|
|
27
31
|
}
|
|
28
32
|
/**
|
|
29
33
|
* Run lifecycle check for a single repo.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve, join } from "node:path";
|
|
2
2
|
import { generateWorkspaceName } from "../shared/workspace-utils.js";
|
|
3
|
-
import { formatLifecycleAction } from "./
|
|
3
|
+
import { formatLifecycleAction } from "./formatter.js";
|
|
4
4
|
/**
|
|
5
5
|
* Build CreateRepoSettings from GitHubRepoSettings.
|
|
6
6
|
* Extracts only the fields relevant for repo creation.
|
|
@@ -35,15 +35,16 @@ export async function runLifecycleCheck(repoConfig, repoInfo, options) {
|
|
|
35
35
|
githubHosts: options.githubHosts,
|
|
36
36
|
token: options.token,
|
|
37
37
|
}, createSettings);
|
|
38
|
+
const reportSettings = createSettings
|
|
39
|
+
? {
|
|
40
|
+
visibility: createSettings.visibility,
|
|
41
|
+
description: createSettings.description,
|
|
42
|
+
}
|
|
43
|
+
: undefined;
|
|
38
44
|
const outputLines = formatLifecycleAction(lifecycleResult, {
|
|
39
45
|
upstream: repoConfig.upstream,
|
|
40
46
|
source: repoConfig.source,
|
|
41
|
-
settings:
|
|
42
|
-
? {
|
|
43
|
-
visibility: createSettings.visibility,
|
|
44
|
-
description: createSettings.description,
|
|
45
|
-
}
|
|
46
|
-
: undefined,
|
|
47
|
+
settings: reportSettings,
|
|
47
48
|
});
|
|
48
|
-
return { lifecycleResult, outputLines,
|
|
49
|
+
return { lifecycleResult, outputLines, reportSettings };
|
|
49
50
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export type { IRepoLifecycleManager } from "./types.js";
|
|
1
|
+
export type { IRepoLifecycleManager, LifecycleActionKind } from "./types.js";
|
|
2
2
|
export { RepoLifecycleManager } from "./repo-lifecycle-manager.js";
|
|
3
|
-
export { runLifecycleCheck, toCreateRepoSettings, type LifecycleCheckResult, } from "./
|
|
3
|
+
export { runLifecycleCheck, toCreateRepoSettings, type LifecycleCheckResult, } from "./helpers.js";
|
package/dist/lifecycle/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { RepoLifecycleManager } from "./repo-lifecycle-manager.js";
|
|
2
|
-
export { runLifecycleCheck, toCreateRepoSettings, } from "./
|
|
2
|
+
export { runLifecycleCheck, toCreateRepoSettings, } from "./helpers.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ICommandExecutor } from "../shared/command-executor.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { DebugInfoWarnLog } from "../shared/logger.js";
|
|
3
3
|
import type { IRepoLifecycleFactory, IRepoLifecycleProvider, IMigrationSource, LifecyclePlatform } from "./types.js";
|
|
4
4
|
export declare class RepoLifecycleFactory implements IRepoLifecycleFactory {
|
|
5
5
|
private readonly providers;
|
|
@@ -8,7 +8,7 @@ export declare class RepoLifecycleFactory implements IRepoLifecycleFactory {
|
|
|
8
8
|
private readonly retries;
|
|
9
9
|
private readonly cwd;
|
|
10
10
|
private readonly log?;
|
|
11
|
-
constructor(executor: ICommandExecutor, retries: number | undefined, cwd: string, log?:
|
|
11
|
+
constructor(executor: ICommandExecutor, retries: number | undefined, cwd: string, log?: DebugInfoWarnLog);
|
|
12
12
|
getProvider(platform: LifecyclePlatform): IRepoLifecycleProvider;
|
|
13
13
|
getMigrationSource(platform: LifecyclePlatform): IMigrationSource;
|
|
14
14
|
}
|
|
@@ -5,11 +5,10 @@ import { toErrorMessage } from "../shared/type-guards.js";
|
|
|
5
5
|
* No-op if summaryPath is not provided.
|
|
6
6
|
*/
|
|
7
7
|
export function writeGitHubStepSummary(markdown, summaryPath, log) {
|
|
8
|
-
|
|
9
|
-
if (!path)
|
|
8
|
+
if (!summaryPath)
|
|
10
9
|
return;
|
|
11
10
|
try {
|
|
12
|
-
appendFileSync(
|
|
11
|
+
appendFileSync(summaryPath, "\n" + markdown + "\n");
|
|
13
12
|
}
|
|
14
13
|
catch (error) {
|
|
15
14
|
log?.debug(`Failed to write GitHub step summary: ${toErrorMessage(error)}`);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { writeGitHubStepSummary } from "./github-summary.js";
|
|
2
|
+
export { hasLifecycleChanges, formatLifecycleReportCLI, formatLifecycleReportMarkdown, writeLifecycleReportSummary, type LifecycleReport, type LifecycleAction, } from "./lifecycle-report.js";
|
|
3
|
+
export { formatCountEntry, formatSettingsReportCLI, renderRepoSettingsDiffLines, formatSettingsReportMarkdown, writeSettingsReportSummary, type SettingsReport, type RepoChanges, type SettingChange, type RulesetChange, type LabelChange, } from "./settings-report.js";
|
|
4
|
+
export { formatSyncReportCLI, formatSyncReportMarkdown, renderSyncLines, writeSyncReportSummary, type SyncReport, type RepoFileChanges, type ReportFileChange, } from "./sync-report.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { writeGitHubStepSummary } from "./github-summary.js";
|
|
2
|
+
export { hasLifecycleChanges, formatLifecycleReportCLI, formatLifecycleReportMarkdown, writeLifecycleReportSummary, } from "./lifecycle-report.js";
|
|
3
|
+
export { formatCountEntry, formatSettingsReportCLI, renderRepoSettingsDiffLines, formatSettingsReportMarkdown, writeSettingsReportSummary, } from "./settings-report.js";
|
|
4
|
+
export { formatSyncReportCLI, formatSyncReportMarkdown, renderSyncLines, writeSyncReportSummary, } from "./sync-report.js";
|
|
@@ -33,6 +33,11 @@ function renderActionDiffLines(actions) {
|
|
|
33
33
|
case "migrated":
|
|
34
34
|
lines.push(`+ MIGRATE ${action.source ?? "source"} -> ${action.repoName}`);
|
|
35
35
|
break;
|
|
36
|
+
/* c8 ignore next 4 */
|
|
37
|
+
default: {
|
|
38
|
+
const _exhaustive = action.action;
|
|
39
|
+
throw new Error(`Unexpected lifecycle action: ${_exhaustive}`);
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
if (action.settings) {
|
|
38
43
|
if (action.settings.visibility) {
|