@aspruyt/xfg 3.8.2 → 3.9.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/dist/cli/settings-command.d.ts +2 -1
- package/dist/cli/settings-command.js +82 -7
- package/dist/cli/sync-command.d.ts +2 -1
- package/dist/cli/sync-command.js +80 -5
- package/dist/config/normalizer.js +2 -0
- package/dist/config/types.d.ts +9 -0
- package/dist/config/validator.js +62 -0
- package/dist/lifecycle/ado-migration-source.d.ts +15 -0
- package/dist/lifecycle/ado-migration-source.js +37 -0
- package/dist/lifecycle/github-lifecycle-provider.d.ts +56 -0
- package/dist/lifecycle/github-lifecycle-provider.js +314 -0
- package/dist/lifecycle/index.d.ts +7 -0
- package/dist/lifecycle/index.js +6 -0
- package/dist/lifecycle/lifecycle-formatter.d.ts +14 -0
- package/dist/lifecycle/lifecycle-formatter.js +34 -0
- package/dist/lifecycle/lifecycle-helpers.d.ts +27 -0
- package/dist/lifecycle/lifecycle-helpers.js +47 -0
- package/dist/lifecycle/repo-lifecycle-factory.d.ts +14 -0
- package/dist/lifecycle/repo-lifecycle-factory.js +57 -0
- package/dist/lifecycle/repo-lifecycle-manager.d.ts +20 -0
- package/dist/lifecycle/repo-lifecycle-manager.js +139 -0
- package/dist/lifecycle/types.d.ts +104 -0
- package/dist/lifecycle/types.js +1 -0
- package/dist/output/lifecycle-report.d.ts +37 -0
- package/dist/output/lifecycle-report.js +146 -0
- package/dist/output/settings-report.js +26 -26
- package/dist/output/sync-report.js +5 -5
- package/dist/output/unified-summary.d.ts +4 -0
- package/dist/output/unified-summary.js +147 -0
- package/dist/settings/repo-settings/diff.js +1 -0
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +1 -0
- package/dist/shared/logger.d.ts +2 -0
- package/dist/shared/logger.js +5 -0
- package/dist/sync/manifest-strategy.js +3 -1
- package/dist/vcs/authenticated-git-ops.js +1 -1
- package/dist/vcs/git-ops.js +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { parseGitUrl } from "../shared/repo-detector.js";
|
|
4
|
+
import { logger } from "../shared/logger.js";
|
|
5
|
+
import { RepoLifecycleFactory } from "./repo-lifecycle-factory.js";
|
|
6
|
+
/**
|
|
7
|
+
* Orchestrates repo lifecycle operations before sync.
|
|
8
|
+
*/
|
|
9
|
+
export class RepoLifecycleManager {
|
|
10
|
+
factory;
|
|
11
|
+
constructor(factory, retries) {
|
|
12
|
+
this.factory = factory ?? new RepoLifecycleFactory(undefined, retries);
|
|
13
|
+
}
|
|
14
|
+
async ensureRepo(repoConfig, repoInfo, options, settings) {
|
|
15
|
+
let provider;
|
|
16
|
+
try {
|
|
17
|
+
provider = this.factory.getProvider(repoInfo.type);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
// If user explicitly configured lifecycle (upstream/source), propagate the error
|
|
21
|
+
if (repoConfig.upstream || repoConfig.source) {
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
// Platform doesn't support lifecycle operations yet - skip silently
|
|
25
|
+
return { repoInfo, action: "existed" };
|
|
26
|
+
}
|
|
27
|
+
const { token } = options;
|
|
28
|
+
// Check if repo exists
|
|
29
|
+
const exists = await provider.exists(repoInfo, token);
|
|
30
|
+
if (exists) {
|
|
31
|
+
// Repo exists - nothing to do (ignore upstream/source)
|
|
32
|
+
return {
|
|
33
|
+
repoInfo,
|
|
34
|
+
action: "existed",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// Repo doesn't exist - determine what action to take
|
|
38
|
+
if (repoConfig.source) {
|
|
39
|
+
// Migration mode
|
|
40
|
+
return this.migrate(repoConfig, repoInfo, options, settings);
|
|
41
|
+
}
|
|
42
|
+
if (repoConfig.upstream) {
|
|
43
|
+
// Fork mode
|
|
44
|
+
return this.fork(repoConfig, repoInfo, provider, options, settings);
|
|
45
|
+
}
|
|
46
|
+
// Create mode (no upstream or source)
|
|
47
|
+
return this.create(repoInfo, provider, options, settings);
|
|
48
|
+
}
|
|
49
|
+
async create(repoInfo, provider, options, settings) {
|
|
50
|
+
if (options.dryRun) {
|
|
51
|
+
return {
|
|
52
|
+
repoInfo,
|
|
53
|
+
action: "created",
|
|
54
|
+
skipped: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
await provider.create(repoInfo, settings, options.token);
|
|
58
|
+
await this.waitForRepoReady(provider, repoInfo, options.token);
|
|
59
|
+
return {
|
|
60
|
+
repoInfo,
|
|
61
|
+
action: "created",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async fork(repoConfig, repoInfo, provider, options, settings) {
|
|
65
|
+
if (options.dryRun) {
|
|
66
|
+
return {
|
|
67
|
+
repoInfo,
|
|
68
|
+
action: "forked",
|
|
69
|
+
skipped: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (!provider.fork) {
|
|
73
|
+
throw new Error(`Platform '${repoInfo.type}' does not support forking`);
|
|
74
|
+
}
|
|
75
|
+
// Parse upstream URL to get repo info
|
|
76
|
+
const upstreamInfo = parseGitUrl(repoConfig.upstream, {
|
|
77
|
+
githubHosts: options.githubHosts,
|
|
78
|
+
});
|
|
79
|
+
await provider.fork(upstreamInfo, repoInfo, settings, options.token);
|
|
80
|
+
await this.waitForRepoReady(provider, repoInfo, options.token);
|
|
81
|
+
return {
|
|
82
|
+
repoInfo,
|
|
83
|
+
action: "forked",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async migrate(repoConfig, repoInfo, options, settings) {
|
|
87
|
+
if (options.dryRun) {
|
|
88
|
+
return {
|
|
89
|
+
repoInfo,
|
|
90
|
+
action: "migrated",
|
|
91
|
+
skipped: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Parse source URL to get platform and repo info
|
|
95
|
+
const sourceInfo = parseGitUrl(repoConfig.source, {
|
|
96
|
+
githubHosts: options.githubHosts,
|
|
97
|
+
});
|
|
98
|
+
const source = this.factory.getMigrationSource(sourceInfo.type);
|
|
99
|
+
// Clone source repo to temp directory
|
|
100
|
+
const sourceDir = join(options.workDir, "migration-source");
|
|
101
|
+
try {
|
|
102
|
+
await source.cloneForMigration(sourceInfo, sourceDir);
|
|
103
|
+
// Create target and push content
|
|
104
|
+
const provider = this.factory.getProvider(repoInfo.type);
|
|
105
|
+
await provider.receiveMigration(repoInfo, sourceDir, settings, options.token);
|
|
106
|
+
await this.waitForRepoReady(provider, repoInfo, options.token);
|
|
107
|
+
return {
|
|
108
|
+
repoInfo,
|
|
109
|
+
action: "migrated",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
// Clean up migration source directory
|
|
114
|
+
try {
|
|
115
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
catch (cleanupError) {
|
|
118
|
+
// Log cleanup errors at debug level for troubleshooting
|
|
119
|
+
logger.debug(`Failed to clean up migration source directory ${sourceDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Polls provider.exists() until the repo is visible, with timeout.
|
|
125
|
+
* GitHub's API may return success from create/fork before the git
|
|
126
|
+
* backend has fully propagated, causing subsequent clone to 403.
|
|
127
|
+
*/
|
|
128
|
+
async waitForRepoReady(provider, repoInfo, token, timeoutMs = 15000, pollMs = 1000) {
|
|
129
|
+
const start = Date.now();
|
|
130
|
+
while (Date.now() - start < timeoutMs) {
|
|
131
|
+
if (await provider.exists(repoInfo, token)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
135
|
+
}
|
|
136
|
+
// Timed out — proceed anyway and let downstream operations handle it
|
|
137
|
+
logger.info(`Repo ${repoInfo.owner}/${repoInfo.repo} not yet visible after ${timeoutMs}ms, proceeding`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { RepoInfo } from "../shared/repo-detector.js";
|
|
2
|
+
import type { RepoConfig } from "../config/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Supported platforms for lifecycle operations.
|
|
5
|
+
*/
|
|
6
|
+
export type LifecyclePlatform = "github" | "azure-devops" | "gitlab";
|
|
7
|
+
/**
|
|
8
|
+
* Result of a lifecycle operation.
|
|
9
|
+
*/
|
|
10
|
+
export interface LifecycleResult {
|
|
11
|
+
/** The repo info (may be updated) */
|
|
12
|
+
repoInfo: RepoInfo;
|
|
13
|
+
/** What action was taken */
|
|
14
|
+
action: "existed" | "created" | "forked" | "migrated";
|
|
15
|
+
/** True if skipped due to dry-run */
|
|
16
|
+
skipped?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Options for lifecycle operations.
|
|
20
|
+
*/
|
|
21
|
+
export interface LifecycleOptions {
|
|
22
|
+
/** Dry-run mode - don't make changes */
|
|
23
|
+
dryRun: boolean;
|
|
24
|
+
/** Working directory for git operations */
|
|
25
|
+
workDir: string;
|
|
26
|
+
/** GitHub Enterprise hostnames for URL detection */
|
|
27
|
+
githubHosts?: string[];
|
|
28
|
+
/** Auth token (GitHub App installation token or PAT) for gh CLI commands */
|
|
29
|
+
token?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Repo settings to apply when creating a new repo.
|
|
33
|
+
* Subset of GitHubRepoSettings that makes sense for creation.
|
|
34
|
+
*/
|
|
35
|
+
export interface CreateRepoSettings {
|
|
36
|
+
visibility?: "public" | "private" | "internal";
|
|
37
|
+
description?: string;
|
|
38
|
+
hasIssues?: boolean;
|
|
39
|
+
hasWiki?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Provider for platform-specific lifecycle operations.
|
|
43
|
+
* Implementations handle create/fork/receive for a specific platform.
|
|
44
|
+
*/
|
|
45
|
+
export interface IRepoLifecycleProvider {
|
|
46
|
+
/** Platform this provider handles */
|
|
47
|
+
readonly platform: LifecyclePlatform;
|
|
48
|
+
/**
|
|
49
|
+
* Check if a repository exists on this platform.
|
|
50
|
+
* @throws Error on network/auth failures (NOT for "repo not found")
|
|
51
|
+
*/
|
|
52
|
+
exists(repoInfo: RepoInfo, token?: string): Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* Create an empty repository.
|
|
55
|
+
*/
|
|
56
|
+
create(repoInfo: RepoInfo, settings?: CreateRepoSettings, token?: string): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Fork from an upstream repository.
|
|
59
|
+
* Optional - not all platforms support forking.
|
|
60
|
+
*/
|
|
61
|
+
fork?(upstream: RepoInfo, target: RepoInfo, settings?: CreateRepoSettings, token?: string): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Receive migrated content (repo already created, push content).
|
|
64
|
+
*/
|
|
65
|
+
receiveMigration(repoInfo: RepoInfo, sourceDir: string, settings?: CreateRepoSettings, token?: string): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Source for migration operations.
|
|
69
|
+
* Implementations handle cloning from a source platform.
|
|
70
|
+
*/
|
|
71
|
+
export interface IMigrationSource {
|
|
72
|
+
/** Platform this source handles */
|
|
73
|
+
readonly platform: LifecyclePlatform;
|
|
74
|
+
/**
|
|
75
|
+
* Clone repository with all refs for migration.
|
|
76
|
+
* Uses --mirror to get all branches/tags.
|
|
77
|
+
*/
|
|
78
|
+
cloneForMigration(repoInfo: RepoInfo, workDir: string): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Factory for getting providers by platform.
|
|
82
|
+
*/
|
|
83
|
+
export interface IRepoLifecycleFactory {
|
|
84
|
+
/**
|
|
85
|
+
* Get lifecycle provider for a platform.
|
|
86
|
+
* @throws Error if platform not supported as target
|
|
87
|
+
*/
|
|
88
|
+
getProvider(platform: LifecyclePlatform): IRepoLifecycleProvider;
|
|
89
|
+
/**
|
|
90
|
+
* Get migration source for a platform.
|
|
91
|
+
* @throws Error if platform not supported as source
|
|
92
|
+
*/
|
|
93
|
+
getMigrationSource(platform: LifecyclePlatform): IMigrationSource;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Manager that orchestrates lifecycle operations before sync.
|
|
97
|
+
*/
|
|
98
|
+
export interface IRepoLifecycleManager {
|
|
99
|
+
/**
|
|
100
|
+
* Ensure repository exists, creating/forking/migrating if needed.
|
|
101
|
+
* Call this before sync/settings operations.
|
|
102
|
+
*/
|
|
103
|
+
ensureRepo(repoConfig: RepoConfig, repoInfo: RepoInfo, options: LifecycleOptions, settings?: CreateRepoSettings): Promise<LifecycleResult>;
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface LifecycleReport {
|
|
2
|
+
actions: LifecycleAction[];
|
|
3
|
+
totals: {
|
|
4
|
+
created: number;
|
|
5
|
+
forked: number;
|
|
6
|
+
migrated: number;
|
|
7
|
+
existed: number;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface LifecycleAction {
|
|
11
|
+
repoName: string;
|
|
12
|
+
action: "existed" | "created" | "forked" | "migrated";
|
|
13
|
+
upstream?: string;
|
|
14
|
+
source?: string;
|
|
15
|
+
settings?: {
|
|
16
|
+
visibility?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface LifecycleReportInput {
|
|
21
|
+
repoName: string;
|
|
22
|
+
action: "existed" | "created" | "forked" | "migrated";
|
|
23
|
+
upstream?: string;
|
|
24
|
+
source?: string;
|
|
25
|
+
settings?: {
|
|
26
|
+
visibility?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare function buildLifecycleReport(results: LifecycleReportInput[]): LifecycleReport;
|
|
31
|
+
/**
|
|
32
|
+
* Returns true if the report has any non-"existed" actions worth displaying.
|
|
33
|
+
*/
|
|
34
|
+
export declare function hasLifecycleChanges(report: LifecycleReport): boolean;
|
|
35
|
+
export declare function formatLifecycleReportCLI(report: LifecycleReport): string[];
|
|
36
|
+
export declare function formatLifecycleReportMarkdown(report: LifecycleReport, dryRun: boolean): string;
|
|
37
|
+
export declare function writeLifecycleReportSummary(report: LifecycleReport, dryRun: boolean): void;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// src/output/lifecycle-report.ts
|
|
2
|
+
import { appendFileSync } from "node:fs";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Builder
|
|
6
|
+
// =============================================================================
|
|
7
|
+
export function buildLifecycleReport(results) {
|
|
8
|
+
const actions = [];
|
|
9
|
+
const totals = { created: 0, forked: 0, migrated: 0, existed: 0 };
|
|
10
|
+
for (const result of results) {
|
|
11
|
+
actions.push({
|
|
12
|
+
repoName: result.repoName,
|
|
13
|
+
action: result.action,
|
|
14
|
+
upstream: result.upstream,
|
|
15
|
+
source: result.source,
|
|
16
|
+
settings: result.settings,
|
|
17
|
+
});
|
|
18
|
+
totals[result.action]++;
|
|
19
|
+
}
|
|
20
|
+
return { actions, totals };
|
|
21
|
+
}
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Helpers
|
|
24
|
+
// =============================================================================
|
|
25
|
+
function formatSummary(totals) {
|
|
26
|
+
const total = totals.created + totals.forked + totals.migrated;
|
|
27
|
+
if (total === 0) {
|
|
28
|
+
return "No changes";
|
|
29
|
+
}
|
|
30
|
+
const parts = [];
|
|
31
|
+
if (totals.created > 0)
|
|
32
|
+
parts.push(`${totals.created} to create`);
|
|
33
|
+
if (totals.forked > 0)
|
|
34
|
+
parts.push(`${totals.forked} to fork`);
|
|
35
|
+
if (totals.migrated > 0)
|
|
36
|
+
parts.push(`${totals.migrated} to migrate`);
|
|
37
|
+
const repoWord = total === 1 ? "repo" : "repos";
|
|
38
|
+
return `Plan: ${total} ${repoWord} (${parts.join(", ")})`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns true if the report has any non-"existed" actions worth displaying.
|
|
42
|
+
*/
|
|
43
|
+
export function hasLifecycleChanges(report) {
|
|
44
|
+
return report.actions.some((a) => a.action !== "existed");
|
|
45
|
+
}
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// CLI Formatter
|
|
48
|
+
// =============================================================================
|
|
49
|
+
export function formatLifecycleReportCLI(report) {
|
|
50
|
+
if (!hasLifecycleChanges(report)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const lines = [];
|
|
54
|
+
for (const action of report.actions) {
|
|
55
|
+
if (action.action === "existed")
|
|
56
|
+
continue;
|
|
57
|
+
switch (action.action) {
|
|
58
|
+
case "created":
|
|
59
|
+
lines.push(chalk.green(`+ CREATE ${action.repoName}`));
|
|
60
|
+
break;
|
|
61
|
+
case "forked":
|
|
62
|
+
lines.push(chalk.green(`+ FORK ${action.upstream ?? "upstream"} -> ${action.repoName}`));
|
|
63
|
+
break;
|
|
64
|
+
case "migrated":
|
|
65
|
+
lines.push(chalk.green(`+ MIGRATE ${action.source ?? "source"} -> ${action.repoName}`));
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
if (action.settings) {
|
|
69
|
+
if (action.settings.visibility) {
|
|
70
|
+
lines.push(` visibility: ${action.settings.visibility}`);
|
|
71
|
+
}
|
|
72
|
+
if (action.settings.description) {
|
|
73
|
+
lines.push(` description: "${action.settings.description}"`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
lines.push("");
|
|
78
|
+
// Summary
|
|
79
|
+
lines.push(formatSummary(report.totals));
|
|
80
|
+
return lines;
|
|
81
|
+
}
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// Markdown Formatter
|
|
84
|
+
// =============================================================================
|
|
85
|
+
export function formatLifecycleReportMarkdown(report, dryRun) {
|
|
86
|
+
if (!hasLifecycleChanges(report)) {
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
const lines = [];
|
|
90
|
+
// Title
|
|
91
|
+
const titleSuffix = dryRun ? " (Dry Run)" : "";
|
|
92
|
+
lines.push(`## Lifecycle Summary${titleSuffix}`);
|
|
93
|
+
lines.push("");
|
|
94
|
+
// Dry-run warning
|
|
95
|
+
if (dryRun) {
|
|
96
|
+
lines.push("> [!WARNING]");
|
|
97
|
+
lines.push("> This was a dry run — no changes were applied");
|
|
98
|
+
lines.push("");
|
|
99
|
+
}
|
|
100
|
+
// Diff block
|
|
101
|
+
const diffLines = [];
|
|
102
|
+
for (const action of report.actions) {
|
|
103
|
+
if (action.action === "existed")
|
|
104
|
+
continue;
|
|
105
|
+
switch (action.action) {
|
|
106
|
+
case "created":
|
|
107
|
+
diffLines.push(`+ CREATE ${action.repoName}`);
|
|
108
|
+
break;
|
|
109
|
+
case "forked":
|
|
110
|
+
diffLines.push(`+ FORK ${action.upstream ?? "upstream"} -> ${action.repoName}`);
|
|
111
|
+
break;
|
|
112
|
+
case "migrated":
|
|
113
|
+
diffLines.push(`+ MIGRATE ${action.source ?? "source"} -> ${action.repoName}`);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (action.settings) {
|
|
117
|
+
if (action.settings.visibility) {
|
|
118
|
+
diffLines.push(` visibility: ${action.settings.visibility}`);
|
|
119
|
+
}
|
|
120
|
+
if (action.settings.description) {
|
|
121
|
+
diffLines.push(` description: "${action.settings.description}"`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (diffLines.length > 0) {
|
|
126
|
+
lines.push("```diff");
|
|
127
|
+
lines.push(...diffLines);
|
|
128
|
+
lines.push("```");
|
|
129
|
+
lines.push("");
|
|
130
|
+
}
|
|
131
|
+
// Summary
|
|
132
|
+
lines.push(`**${formatSummary(report.totals)}**`);
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
// =============================================================================
|
|
136
|
+
// File Writer
|
|
137
|
+
// =============================================================================
|
|
138
|
+
export function writeLifecycleReportSummary(report, dryRun) {
|
|
139
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
140
|
+
if (!summaryPath)
|
|
141
|
+
return;
|
|
142
|
+
const markdown = formatLifecycleReportMarkdown(report, dryRun);
|
|
143
|
+
if (!markdown)
|
|
144
|
+
return;
|
|
145
|
+
appendFileSync(summaryPath, "\n" + markdown + "\n");
|
|
146
|
+
}
|
|
@@ -164,52 +164,52 @@ function formatValuePlain(val) {
|
|
|
164
164
|
return val ? "true" : "false";
|
|
165
165
|
return String(val);
|
|
166
166
|
}
|
|
167
|
-
function formatRulesetConfigPlain(config
|
|
167
|
+
function formatRulesetConfigPlain(config) {
|
|
168
168
|
const lines = [];
|
|
169
|
-
function renderObject(obj,
|
|
169
|
+
function renderObject(obj, depth) {
|
|
170
170
|
for (const [k, v] of Object.entries(obj)) {
|
|
171
|
-
renderValue(k, v,
|
|
171
|
+
renderValue(k, v, depth);
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
-
function renderValue(key, value,
|
|
175
|
-
const
|
|
174
|
+
function renderValue(key, value, depth) {
|
|
175
|
+
const indent = " ".repeat(depth);
|
|
176
176
|
if (value === null || value === undefined)
|
|
177
177
|
return;
|
|
178
178
|
if (Array.isArray(value)) {
|
|
179
179
|
if (value.length === 0) {
|
|
180
|
-
lines.push(
|
|
180
|
+
lines.push(`+${indent} ${key}: []`);
|
|
181
181
|
}
|
|
182
182
|
else if (value.every((v) => typeof v !== "object")) {
|
|
183
|
-
lines.push(
|
|
183
|
+
lines.push(`+${indent} ${key}: [${value.map((v) => (typeof v === "string" ? `"${v}"` : String(v))).join(", ")}]`);
|
|
184
184
|
}
|
|
185
185
|
else {
|
|
186
|
-
lines.push(
|
|
186
|
+
lines.push(`+${indent} ${key}:`);
|
|
187
187
|
for (let i = 0; i < value.length; i++) {
|
|
188
188
|
const item = value[i];
|
|
189
189
|
if (typeof item === "object" && item !== null) {
|
|
190
190
|
const obj = item;
|
|
191
191
|
const typeLabel = "type" in obj ? ` (${obj.type})` : "";
|
|
192
|
-
lines.push(
|
|
193
|
-
renderObject(obj,
|
|
192
|
+
lines.push(`+${indent} [${i}]${typeLabel}:`);
|
|
193
|
+
renderObject(obj, depth + 2);
|
|
194
194
|
}
|
|
195
195
|
else {
|
|
196
|
-
lines.push(
|
|
196
|
+
lines.push(`+${indent} ${formatValuePlain(item)}`);
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
else if (typeof value === "object") {
|
|
202
|
-
lines.push(
|
|
203
|
-
renderObject(value,
|
|
202
|
+
lines.push(`+${indent} ${key}:`);
|
|
203
|
+
renderObject(value, depth + 1);
|
|
204
204
|
}
|
|
205
205
|
else {
|
|
206
|
-
lines.push(
|
|
206
|
+
lines.push(`+${indent} ${key}: ${formatValuePlain(value)}`);
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
for (const [key, value] of Object.entries(config)) {
|
|
210
210
|
if (key === "name")
|
|
211
211
|
continue;
|
|
212
|
-
renderValue(key, value,
|
|
212
|
+
renderValue(key, value, 1);
|
|
213
213
|
}
|
|
214
214
|
return lines;
|
|
215
215
|
}
|
|
@@ -233,49 +233,49 @@ export function formatSettingsReportMarkdown(report, dryRun) {
|
|
|
233
233
|
!repo.error) {
|
|
234
234
|
continue;
|
|
235
235
|
}
|
|
236
|
-
diffLines.push(
|
|
236
|
+
diffLines.push(`@@ ${repo.repoName} @@`);
|
|
237
237
|
for (const setting of repo.settings) {
|
|
238
238
|
// Skip settings where both values are undefined
|
|
239
239
|
if (setting.oldValue === undefined && setting.newValue === undefined) {
|
|
240
240
|
continue;
|
|
241
241
|
}
|
|
242
242
|
if (setting.action === "add") {
|
|
243
|
-
diffLines.push(
|
|
243
|
+
diffLines.push(`+ ${setting.name}: ${formatValuePlain(setting.newValue)}`);
|
|
244
244
|
}
|
|
245
245
|
else {
|
|
246
|
-
diffLines.push(
|
|
246
|
+
diffLines.push(`! ${setting.name}: ${formatValuePlain(setting.oldValue)} → ${formatValuePlain(setting.newValue)}`);
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
for (const ruleset of repo.rulesets) {
|
|
250
250
|
if (ruleset.action === "create") {
|
|
251
|
-
diffLines.push(
|
|
251
|
+
diffLines.push(`+ ruleset "${ruleset.name}"`);
|
|
252
252
|
if (ruleset.config) {
|
|
253
|
-
diffLines.push(...formatRulesetConfigPlain(ruleset.config
|
|
253
|
+
diffLines.push(...formatRulesetConfigPlain(ruleset.config));
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
else if (ruleset.action === "update") {
|
|
257
|
-
diffLines.push(
|
|
257
|
+
diffLines.push(`! ruleset "${ruleset.name}"`);
|
|
258
258
|
if (ruleset.propertyDiffs && ruleset.propertyDiffs.length > 0) {
|
|
259
259
|
for (const diff of ruleset.propertyDiffs) {
|
|
260
260
|
const path = diff.path.join(".");
|
|
261
261
|
if (diff.action === "add") {
|
|
262
|
-
diffLines.push(
|
|
262
|
+
diffLines.push(`+ ${path}: ${formatValuePlain(diff.newValue)}`);
|
|
263
263
|
}
|
|
264
264
|
else if (diff.action === "change") {
|
|
265
|
-
diffLines.push(
|
|
265
|
+
diffLines.push(`! ${path}: ${formatValuePlain(diff.oldValue)} → ${formatValuePlain(diff.newValue)}`);
|
|
266
266
|
}
|
|
267
267
|
else if (diff.action === "remove") {
|
|
268
|
-
diffLines.push(
|
|
268
|
+
diffLines.push(`- ${path}`);
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
else if (ruleset.action === "delete") {
|
|
274
|
-
diffLines.push(
|
|
274
|
+
diffLines.push(`- ruleset "${ruleset.name}"`);
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
if (repo.error) {
|
|
278
|
-
diffLines.push(
|
|
278
|
+
diffLines.push(`- Error: ${repo.error}`);
|
|
279
279
|
}
|
|
280
280
|
}
|
|
281
281
|
if (diffLines.length > 0) {
|
|
@@ -64,20 +64,20 @@ export function formatSyncReportMarkdown(report, dryRun) {
|
|
|
64
64
|
if (repo.files.length === 0 && !repo.error) {
|
|
65
65
|
continue;
|
|
66
66
|
}
|
|
67
|
-
diffLines.push(
|
|
67
|
+
diffLines.push(`@@ ${repo.repoName} @@`);
|
|
68
68
|
for (const file of repo.files) {
|
|
69
69
|
if (file.action === "create") {
|
|
70
|
-
diffLines.push(
|
|
70
|
+
diffLines.push(`+ ${file.path}`);
|
|
71
71
|
}
|
|
72
72
|
else if (file.action === "update") {
|
|
73
|
-
diffLines.push(
|
|
73
|
+
diffLines.push(`! ${file.path}`);
|
|
74
74
|
}
|
|
75
75
|
else if (file.action === "delete") {
|
|
76
|
-
diffLines.push(
|
|
76
|
+
diffLines.push(`- ${file.path}`);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
if (repo.error) {
|
|
80
|
-
diffLines.push(
|
|
80
|
+
diffLines.push(`- Error: ${repo.error}`);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
if (diffLines.length > 0) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { LifecycleReport } from "./lifecycle-report.js";
|
|
2
|
+
import type { SyncReport } from "./sync-report.js";
|
|
3
|
+
export declare function formatUnifiedSummaryMarkdown(lifecycle: LifecycleReport, sync: SyncReport, dryRun: boolean): string;
|
|
4
|
+
export declare function writeUnifiedSummary(lifecycle: LifecycleReport, sync: SyncReport, dryRun: boolean): void;
|