@aspruyt/xfg 4.0.2 → 4.0.4
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/index.d.ts +1 -1
- package/dist/cli/index.js +0 -6
- package/dist/cli/program.js +3 -2
- package/dist/cli/settings-report-builder.js +4 -4
- package/dist/cli/sync-command.js +72 -36
- package/dist/cli/sync-report-builder.d.ts +2 -6
- package/dist/cli/types.d.ts +2 -14
- package/dist/cli/types.js +1 -9
- package/dist/config/file-reference-resolver.js +13 -23
- package/dist/config/formatter.d.ts +0 -6
- package/dist/config/formatter.js +0 -9
- package/dist/config/index.d.ts +1 -2
- package/dist/config/index.js +0 -2
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +3 -3
- package/dist/config/normalizer.d.ts +1 -1
- package/dist/config/normalizer.js +44 -57
- package/dist/config/validator.d.ts +1 -1
- package/dist/config/validator.js +120 -121
- package/dist/config/validators/file-validator.d.ts +2 -4
- package/dist/config/validators/file-validator.js +3 -7
- package/dist/config/validators/repo-settings-validator.js +1 -1
- package/dist/config/validators/ruleset-validator.js +28 -12
- package/dist/index.d.ts +3 -1
- package/dist/index.js +0 -1
- package/dist/lifecycle/ado-migration-source.d.ts +2 -1
- package/dist/lifecycle/ado-migration-source.js +7 -5
- package/dist/lifecycle/github-lifecycle-provider.d.ts +6 -3
- package/dist/lifecycle/github-lifecycle-provider.js +29 -19
- package/dist/lifecycle/lifecycle-formatter.js +2 -1
- package/dist/lifecycle/lifecycle-helpers.d.ts +5 -1
- package/dist/lifecycle/lifecycle-helpers.js +4 -4
- package/dist/lifecycle/repo-lifecycle-factory.d.ts +4 -4
- package/dist/lifecycle/repo-lifecycle-factory.js +12 -9
- package/dist/lifecycle/repo-lifecycle-manager.d.ts +4 -1
- package/dist/lifecycle/repo-lifecycle-manager.js +11 -7
- package/dist/lifecycle/types.d.ts +0 -15
- package/dist/output/github-summary.d.ts +6 -5
- package/dist/output/github-summary.js +36 -52
- package/dist/output/index.d.ts +2 -2
- package/dist/output/index.js +1 -1
- package/dist/output/lifecycle-report.d.ts +2 -12
- package/dist/output/lifecycle-report.js +18 -35
- package/dist/output/settings-report.d.ts +4 -4
- package/dist/output/settings-report.js +6 -6
- package/dist/output/sync-report.d.ts +4 -6
- package/dist/output/sync-report.js +2 -2
- package/dist/output/unified-summary.d.ts +1 -0
- package/dist/output/unified-summary.js +8 -8
- package/dist/settings/base-processor.d.ts +1 -1
- package/dist/settings/base-processor.js +1 -1
- package/dist/settings/index.d.ts +3 -3
- package/dist/settings/index.js +3 -3
- package/dist/settings/labels/diff.js +3 -2
- package/dist/settings/labels/formatter.js +3 -3
- package/dist/settings/labels/github-labels-strategy.d.ts +2 -23
- package/dist/settings/labels/github-labels-strategy.js +8 -28
- package/dist/settings/labels/index.d.ts +1 -0
- package/dist/settings/labels/index.js +2 -0
- package/dist/settings/labels/processor.d.ts +2 -2
- package/dist/settings/labels/processor.js +3 -4
- package/dist/settings/labels/types.d.ts +0 -3
- package/dist/settings/repo-settings/diff.d.ts +1 -1
- package/dist/settings/repo-settings/diff.js +2 -2
- package/dist/settings/repo-settings/formatter.d.ts +1 -1
- package/dist/settings/repo-settings/formatter.js +4 -4
- package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -7
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +9 -17
- package/dist/settings/repo-settings/index.d.ts +1 -0
- package/dist/settings/repo-settings/index.js +2 -0
- package/dist/settings/repo-settings/processor.d.ts +2 -2
- package/dist/settings/repo-settings/processor.js +5 -6
- package/dist/settings/repo-settings/types.d.ts +9 -13
- package/dist/settings/repo-settings/types.js +1 -14
- package/dist/settings/rulesets/diff-algorithm.d.ts +0 -1
- package/dist/settings/rulesets/diff-algorithm.js +6 -8
- package/dist/settings/rulesets/formatter.js +15 -51
- package/dist/settings/rulesets/github-ruleset-strategy.d.ts +2 -20
- package/dist/settings/rulesets/github-ruleset-strategy.js +6 -30
- package/dist/settings/rulesets/index.d.ts +2 -1
- package/dist/settings/rulesets/index.js +3 -1
- package/dist/settings/rulesets/processor.d.ts +2 -2
- package/dist/settings/rulesets/processor.js +3 -4
- package/dist/{vcs → shared}/branch-utils.js +5 -4
- package/dist/shared/command-executor.d.ts +2 -1
- package/dist/shared/command-executor.js +9 -5
- package/dist/shared/env.d.ts +6 -6
- package/dist/shared/env.js +10 -17
- package/dist/shared/errors.d.ts +26 -0
- package/dist/shared/errors.js +34 -0
- package/dist/shared/gh-api-utils.d.ts +21 -14
- package/dist/shared/gh-api-utils.js +33 -22
- package/dist/shared/index.d.ts +9 -2
- package/dist/shared/index.js +16 -2
- package/dist/shared/logger.d.ts +24 -1
- package/dist/shared/logger.js +8 -3
- package/dist/shared/repo-detector.js +9 -11
- package/dist/shared/retry-utils.d.ts +5 -7
- package/dist/shared/retry-utils.js +3 -10
- package/dist/shared/shell-utils.d.ts +0 -3
- package/dist/shared/shell-utils.js +2 -4
- package/dist/shared/type-guards.d.ts +2 -9
- package/dist/shared/type-guards.js +0 -6
- package/dist/shared/xfg-template.d.ts +2 -2
- package/dist/shared/xfg-template.js +2 -1
- package/dist/sync/auth-options-builder.d.ts +3 -2
- package/dist/sync/auth-options-builder.js +14 -10
- package/dist/sync/branch-manager.d.ts +12 -7
- package/dist/sync/branch-manager.js +4 -7
- package/dist/sync/commit-message.d.ts +1 -1
- package/dist/sync/commit-push-manager.d.ts +8 -2
- package/dist/sync/commit-push-manager.js +6 -5
- package/dist/sync/file-sync-orchestrator.js +17 -21
- package/dist/sync/file-writer.js +3 -5
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/manifest-manager.d.ts +1 -0
- package/dist/sync/manifest.d.ts +4 -7
- package/dist/sync/manifest.js +42 -45
- package/dist/sync/repository-processor.d.ts +5 -2
- package/dist/sync/repository-processor.js +11 -17
- package/dist/sync/repository-session.js +2 -1
- package/dist/sync/sync-workflow.d.ts +2 -2
- package/dist/sync/sync-workflow.js +16 -23
- package/dist/sync/types.d.ts +20 -25
- package/dist/vcs/authenticated-git-ops.d.ts +3 -4
- package/dist/vcs/authenticated-git-ops.js +5 -1
- package/dist/vcs/azure-pr-strategy.d.ts +6 -1
- package/dist/vcs/azure-pr-strategy.js +38 -31
- package/dist/vcs/commit-strategy-selector.d.ts +10 -19
- package/dist/vcs/commit-strategy-selector.js +8 -24
- package/dist/vcs/git-commit-strategy.d.ts +1 -1
- package/dist/vcs/git-commit-strategy.js +1 -3
- package/dist/vcs/git-ops.d.ts +4 -8
- package/dist/vcs/git-ops.js +18 -22
- package/dist/vcs/github-app-token-manager.js +9 -8
- package/dist/vcs/github-pr-strategy.js +18 -11
- package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
- package/dist/vcs/gitlab-pr-strategy.js +14 -7
- package/dist/vcs/graphql-commit-strategy.d.ts +1 -7
- package/dist/vcs/graphql-commit-strategy.js +24 -32
- package/dist/vcs/index.d.ts +2 -1
- package/dist/vcs/pr-creator.d.ts +6 -9
- package/dist/vcs/pr-strategy-factory.d.ts +1 -1
- package/dist/vcs/pr-strategy-factory.js +2 -1
- package/dist/vcs/pr-strategy.d.ts +1 -1
- package/dist/vcs/pr-strategy.js +2 -3
- package/dist/vcs/types.d.ts +6 -10
- package/package.json +2 -2
- package/dist/config/errors.d.ts +0 -9
- package/dist/config/errors.js +0 -11
- /package/dist/{vcs → shared}/branch-utils.d.ts +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { defaultExecutor, } from "../../shared/command-executor.js";
|
|
2
1
|
import { assertGitHubRepo, } from "../../shared/repo-detector.js";
|
|
3
2
|
import { GhApiClient, parseApiJson, isHttp404Error, } from "../../shared/gh-api-utils.js";
|
|
4
3
|
import { camelToSnake } from "../../shared/string-utils.js";
|
|
@@ -56,26 +55,19 @@ function configToGitHubPayload(settings) {
|
|
|
56
55
|
}
|
|
57
56
|
return payload;
|
|
58
57
|
}
|
|
59
|
-
/**
|
|
60
|
-
* GitHub Repository Settings Strategy.
|
|
61
|
-
* Manages repository settings via GitHub REST API using `gh api` CLI.
|
|
62
|
-
* Note: Uses exec via ICommandExecutor for gh CLI integration, consistent
|
|
63
|
-
* with other strategies in this codebase. Inputs are escaped via escapeShellArg.
|
|
64
|
-
*/
|
|
65
58
|
export class GitHubRepoSettingsStrategy {
|
|
66
59
|
api;
|
|
67
60
|
constructor(executor, options) {
|
|
68
|
-
this.api = new GhApiClient(executor
|
|
61
|
+
this.api = new GhApiClient(executor, options.retries ?? 3, options.cwd);
|
|
69
62
|
}
|
|
70
63
|
async getSettings(repoInfo, options) {
|
|
71
64
|
assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
|
|
72
65
|
const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}`;
|
|
73
|
-
const result = await this.api.call("GET", endpoint,
|
|
66
|
+
const result = await this.api.call("GET", endpoint, { options });
|
|
74
67
|
const parsed = parseApiJson(result, "repo settings response");
|
|
75
68
|
const settings = parsed;
|
|
76
69
|
// Extract owner type from nested API response
|
|
77
70
|
settings.owner_type = parsed.owner?.type;
|
|
78
|
-
// Fetch security settings from separate endpoints
|
|
79
71
|
settings.vulnerability_alerts = await this.getVulnerabilityAlerts(repoInfo, options);
|
|
80
72
|
// Pass vulnerability_alerts state - automated security fixes requires it enabled
|
|
81
73
|
settings.automated_security_fixes = await this.getAutomatedSecurityFixes(repoInfo, options, settings.vulnerability_alerts);
|
|
@@ -91,30 +83,30 @@ export class GitHubRepoSettingsStrategy {
|
|
|
91
83
|
return;
|
|
92
84
|
}
|
|
93
85
|
const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}`;
|
|
94
|
-
await this.api.call("PATCH", endpoint, payload, options);
|
|
86
|
+
await this.api.call("PATCH", endpoint, { payload, options });
|
|
95
87
|
}
|
|
96
88
|
async setVulnerabilityAlerts(repoInfo, enable, options) {
|
|
97
89
|
assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
|
|
98
90
|
const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/vulnerability-alerts`;
|
|
99
91
|
const method = enable ? "PUT" : "DELETE";
|
|
100
|
-
await this.api.call(method, endpoint,
|
|
92
|
+
await this.api.call(method, endpoint, { options });
|
|
101
93
|
}
|
|
102
94
|
async setAutomatedSecurityFixes(repoInfo, enable, options) {
|
|
103
95
|
assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
|
|
104
96
|
const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/automated-security-fixes`;
|
|
105
97
|
const method = enable ? "PUT" : "DELETE";
|
|
106
|
-
await this.api.call(method, endpoint,
|
|
98
|
+
await this.api.call(method, endpoint, { options });
|
|
107
99
|
}
|
|
108
100
|
async setPrivateVulnerabilityReporting(repoInfo, enable, options) {
|
|
109
101
|
assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
|
|
110
102
|
const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/private-vulnerability-reporting`;
|
|
111
103
|
const method = enable ? "PUT" : "DELETE";
|
|
112
|
-
await this.api.call(method, endpoint,
|
|
104
|
+
await this.api.call(method, endpoint, { options });
|
|
113
105
|
}
|
|
114
106
|
async getVulnerabilityAlerts(github, options) {
|
|
115
107
|
const endpoint = `/repos/${github.owner}/${github.repo}/vulnerability-alerts`;
|
|
116
108
|
try {
|
|
117
|
-
await this.api.call("GET", endpoint,
|
|
109
|
+
await this.api.call("GET", endpoint, { options });
|
|
118
110
|
return true; // 204 = enabled
|
|
119
111
|
}
|
|
120
112
|
catch (error) {
|
|
@@ -128,7 +120,7 @@ export class GitHubRepoSettingsStrategy {
|
|
|
128
120
|
// Note: GitHub returns JSON with {enabled: boolean} for this endpoint
|
|
129
121
|
const endpoint = `/repos/${github.owner}/${github.repo}/automated-security-fixes`;
|
|
130
122
|
try {
|
|
131
|
-
const result = await this.api.call("GET", endpoint,
|
|
123
|
+
const result = await this.api.call("GET", endpoint, { options });
|
|
132
124
|
// Parse JSON response - GitHub returns {"enabled": true/false}
|
|
133
125
|
if (result) {
|
|
134
126
|
const data = parseApiJson(result, "automated security fixes response");
|
|
@@ -147,7 +139,7 @@ export class GitHubRepoSettingsStrategy {
|
|
|
147
139
|
async getPrivateVulnerabilityReporting(github, options) {
|
|
148
140
|
const endpoint = `/repos/${github.owner}/${github.repo}/private-vulnerability-reporting`;
|
|
149
141
|
try {
|
|
150
|
-
const result = await this.api.call("GET", endpoint,
|
|
142
|
+
const result = await this.api.call("GET", endpoint, { options });
|
|
151
143
|
const data = parseApiJson(result, "private vulnerability reporting response");
|
|
152
144
|
return data.enabled === true;
|
|
153
145
|
}
|
|
@@ -12,9 +12,9 @@ export interface RepoSettingsProcessorResult extends BaseProcessorResult {
|
|
|
12
12
|
}
|
|
13
13
|
export declare class RepoSettingsProcessor implements IRepoSettingsProcessor {
|
|
14
14
|
private readonly strategy;
|
|
15
|
-
constructor(strategy
|
|
15
|
+
constructor(strategy: IRepoSettingsStrategy);
|
|
16
16
|
process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: RepoSettingsProcessorOptions): Promise<RepoSettingsProcessorResult>;
|
|
17
|
-
private
|
|
17
|
+
private applySettings;
|
|
18
18
|
private applyChanges;
|
|
19
19
|
private validateSecuritySettings;
|
|
20
20
|
}
|
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import { GitHubRepoSettingsStrategy } from "./github-repo-settings-strategy.js";
|
|
2
1
|
import { diffRepoSettings, hasChanges } from "./diff.js";
|
|
3
2
|
import { formatRepoSettingsPlan } from "./formatter.js";
|
|
4
3
|
import { withGitHubGuards, buildDryRunResult, buildApplyResult, } from "../base-processor.js";
|
|
5
4
|
export class RepoSettingsProcessor {
|
|
6
5
|
strategy;
|
|
7
6
|
constructor(strategy) {
|
|
8
|
-
this.strategy = strategy
|
|
7
|
+
this.strategy = strategy;
|
|
9
8
|
}
|
|
10
9
|
async process(repoConfig, repoInfo, options) {
|
|
11
10
|
return withGitHubGuards(repoConfig, repoInfo, options, {
|
|
12
11
|
hasDesiredSettings: (rc) => {
|
|
13
|
-
const
|
|
14
|
-
return !!
|
|
12
|
+
const repoSettings = rc.settings?.repo;
|
|
13
|
+
return !!repoSettings && Object.keys(repoSettings).length > 0;
|
|
15
14
|
},
|
|
16
15
|
emptySettingsMessage: "No repo settings configured",
|
|
17
|
-
|
|
16
|
+
applySettings: (githubRepo, rc, opts, token, repoName) => this.applySettings(githubRepo, rc, opts, token, repoName),
|
|
18
17
|
});
|
|
19
18
|
}
|
|
20
|
-
async
|
|
19
|
+
async applySettings(githubRepo, repoConfig, options, effectiveToken, repoName) {
|
|
21
20
|
const { dryRun } = options;
|
|
22
21
|
const desiredSettings = repoConfig.settings.repo;
|
|
23
22
|
const strategyOptions = { token: effectiveToken, host: githubRepo.host };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RepoInfo } from "../../shared/repo-detector.js";
|
|
2
|
-
import type { GitHubRepoSettings } from "../../config/index.js";
|
|
2
|
+
import type { GitHubRepoSettings, RepoVisibility, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage } from "../../config/index.js";
|
|
3
3
|
import type { GhApiOptions } from "../../shared/gh-api-utils.js";
|
|
4
4
|
/**
|
|
5
5
|
* Current repository settings from GitHub API (snake_case).
|
|
@@ -12,7 +12,7 @@ export interface CurrentRepoSettings {
|
|
|
12
12
|
has_discussions?: boolean;
|
|
13
13
|
is_template?: boolean;
|
|
14
14
|
allow_forking?: boolean;
|
|
15
|
-
visibility?:
|
|
15
|
+
visibility?: RepoVisibility;
|
|
16
16
|
archived?: boolean;
|
|
17
17
|
allow_squash_merge?: boolean;
|
|
18
18
|
allow_merge_commit?: boolean;
|
|
@@ -20,21 +20,21 @@ export interface CurrentRepoSettings {
|
|
|
20
20
|
allow_auto_merge?: boolean;
|
|
21
21
|
delete_branch_on_merge?: boolean;
|
|
22
22
|
allow_update_branch?: boolean;
|
|
23
|
-
squash_merge_commit_title?:
|
|
24
|
-
squash_merge_commit_message?:
|
|
25
|
-
merge_commit_title?:
|
|
26
|
-
merge_commit_message?:
|
|
23
|
+
squash_merge_commit_title?: SquashMergeCommitTitle;
|
|
24
|
+
squash_merge_commit_message?: SquashMergeCommitMessage;
|
|
25
|
+
merge_commit_title?: MergeCommitTitle;
|
|
26
|
+
merge_commit_message?: MergeCommitMessage;
|
|
27
27
|
web_commit_signoff_required?: boolean;
|
|
28
28
|
default_branch?: string;
|
|
29
29
|
security_and_analysis?: {
|
|
30
30
|
secret_scanning?: {
|
|
31
|
-
status:
|
|
31
|
+
status: "enabled" | "disabled";
|
|
32
32
|
};
|
|
33
33
|
secret_scanning_push_protection?: {
|
|
34
|
-
status:
|
|
34
|
+
status: "enabled" | "disabled";
|
|
35
35
|
};
|
|
36
36
|
secret_scanning_validity_checks?: {
|
|
37
|
-
status:
|
|
37
|
+
status: "enabled" | "disabled";
|
|
38
38
|
};
|
|
39
39
|
};
|
|
40
40
|
owner_type?: "User" | "Organization";
|
|
@@ -64,7 +64,3 @@ export interface IRepoSettingsStrategy {
|
|
|
64
64
|
*/
|
|
65
65
|
setPrivateVulnerabilityReporting(repoInfo: RepoInfo, enable: boolean, options?: GhApiOptions): Promise<void>;
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Type guard to check if an object implements IRepoSettingsStrategy.
|
|
69
|
-
*/
|
|
70
|
-
export declare function isRepoSettingsStrategy(obj: unknown): obj is IRepoSettingsStrategy;
|
|
@@ -1,14 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Type guard to check if an object implements IRepoSettingsStrategy.
|
|
3
|
-
*/
|
|
4
|
-
export function isRepoSettingsStrategy(obj) {
|
|
5
|
-
if (typeof obj !== "object" || obj === null) {
|
|
6
|
-
return false;
|
|
7
|
-
}
|
|
8
|
-
const strategy = obj;
|
|
9
|
-
return (typeof strategy.getSettings === "function" &&
|
|
10
|
-
typeof strategy.updateSettings === "function" &&
|
|
11
|
-
typeof strategy.setVulnerabilityAlerts === "function" &&
|
|
12
|
-
typeof strategy.setAutomatedSecurityFixes === "function" &&
|
|
13
|
-
typeof strategy.setPrivateVulnerabilityReporting === "function");
|
|
14
|
-
}
|
|
1
|
+
export {};
|
|
@@ -5,7 +5,6 @@ export interface PropertyDiff {
|
|
|
5
5
|
oldValue?: unknown;
|
|
6
6
|
newValue?: unknown;
|
|
7
7
|
}
|
|
8
|
-
export declare function isObject(val: unknown): val is Record<string, unknown>;
|
|
9
8
|
export declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
10
9
|
export declare function isArrayOfObjects(arr: unknown[]): boolean;
|
|
11
10
|
/**
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// src/settings/rulesets/diff-algorithm.ts
|
|
2
|
-
|
|
3
|
-
return val !== null && typeof val === "object" && !Array.isArray(val);
|
|
4
|
-
}
|
|
2
|
+
import { isPlainObject } from "../../shared/type-guards.js";
|
|
5
3
|
export function deepEqual(a, b) {
|
|
6
4
|
if (a === b)
|
|
7
5
|
return true;
|
|
@@ -14,7 +12,7 @@ export function deepEqual(a, b) {
|
|
|
14
12
|
return false;
|
|
15
13
|
return a.every((val, i) => deepEqual(val, b[i]));
|
|
16
14
|
}
|
|
17
|
-
if (
|
|
15
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
18
16
|
const keysA = Object.keys(a);
|
|
19
17
|
const keysB = Object.keys(b);
|
|
20
18
|
if (keysA.length !== keysB.length)
|
|
@@ -24,7 +22,7 @@ export function deepEqual(a, b) {
|
|
|
24
22
|
return false;
|
|
25
23
|
}
|
|
26
24
|
export function isArrayOfObjects(arr) {
|
|
27
|
-
return arr.length > 0 && arr.every((item) =>
|
|
25
|
+
return arr.length > 0 && arr.every((item) => isPlainObject(item));
|
|
28
26
|
}
|
|
29
27
|
/**
|
|
30
28
|
* Recursively compute property-level diffs between two objects.
|
|
@@ -46,7 +44,7 @@ export function computePropertyDiffs(current, desired, parentPath = []) {
|
|
|
46
44
|
}
|
|
47
45
|
else if (!deepEqual(currentVal, desiredVal)) {
|
|
48
46
|
// Changed property
|
|
49
|
-
if (
|
|
47
|
+
if (isPlainObject(currentVal) && isPlainObject(desiredVal)) {
|
|
50
48
|
// Recurse into nested objects
|
|
51
49
|
diffs.push(...computePropertyDiffs(currentVal, desiredVal, path));
|
|
52
50
|
}
|
|
@@ -75,7 +73,7 @@ export function computePropertyDiffs(current, desired, parentPath = []) {
|
|
|
75
73
|
*/
|
|
76
74
|
function diffObjectArrays(currentArr, desiredArr, parentPath) {
|
|
77
75
|
const diffs = [];
|
|
78
|
-
const hasType = desiredArr.every((item) =>
|
|
76
|
+
const hasType = desiredArr.every((item) => isPlainObject(item) && "type" in item);
|
|
79
77
|
if (hasType) {
|
|
80
78
|
// Match by type field
|
|
81
79
|
const currentByType = new Map();
|
|
@@ -139,7 +137,7 @@ function diffObjectArrays(currentArr, desiredArr, parentPath) {
|
|
|
139
137
|
oldValue: currentArr[i],
|
|
140
138
|
});
|
|
141
139
|
}
|
|
142
|
-
else if (
|
|
140
|
+
else if (isPlainObject(currentArr[i]) && isPlainObject(desiredArr[i])) {
|
|
143
141
|
const itemDiffs = computePropertyDiffs(currentArr[i], desiredArr[i], [...parentPath, label]);
|
|
144
142
|
diffs.push(...itemDiffs);
|
|
145
143
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { projectToDesiredShape, normalizeRuleset, } from "./diff.js";
|
|
3
|
-
import { computePropertyDiffs,
|
|
3
|
+
import { computePropertyDiffs, } from "./diff-algorithm.js";
|
|
4
|
+
import { isPlainObject } from "../../shared/type-guards.js";
|
|
4
5
|
/**
|
|
5
6
|
* Build a tree structure from flat property diffs.
|
|
6
7
|
*/
|
|
@@ -67,7 +68,7 @@ function renderNestedValue(val, action, indent) {
|
|
|
67
68
|
if (Array.isArray(val)) {
|
|
68
69
|
for (let i = 0; i < val.length; i++) {
|
|
69
70
|
const item = val[i];
|
|
70
|
-
if (
|
|
71
|
+
if (isPlainObject(item)) {
|
|
71
72
|
const obj = item;
|
|
72
73
|
const typeLabel = "type" in obj ? ` (${obj.type})` : "";
|
|
73
74
|
lines.push(style.color(`${indentStr}${style.symbol} [${i}]${typeLabel}:`));
|
|
@@ -78,7 +79,7 @@ function renderNestedValue(val, action, indent) {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
|
-
else if (
|
|
82
|
+
else if (isPlainObject(val)) {
|
|
82
83
|
lines.push(...renderNestedObject(val, action, indent));
|
|
83
84
|
}
|
|
84
85
|
return lines;
|
|
@@ -90,11 +91,11 @@ function renderNestedObject(obj, action, indent) {
|
|
|
90
91
|
for (const [key, value] of Object.entries(obj)) {
|
|
91
92
|
if (value === null || value === undefined)
|
|
92
93
|
continue;
|
|
93
|
-
if (Array.isArray(value) && value.some((v) =>
|
|
94
|
+
if (Array.isArray(value) && value.some((v) => isPlainObject(v))) {
|
|
94
95
|
lines.push(style.color(`${indentStr}${style.symbol} ${key}:`));
|
|
95
96
|
lines.push(...renderNestedValue(value, action, indent + 1));
|
|
96
97
|
}
|
|
97
|
-
else if (
|
|
98
|
+
else if (isPlainObject(value)) {
|
|
98
99
|
lines.push(style.color(`${indentStr}${style.symbol} ${key}:`));
|
|
99
100
|
lines.push(...renderNestedObject(value, action, indent + 1));
|
|
100
101
|
}
|
|
@@ -135,12 +136,12 @@ function renderTree(node, indent = 0) {
|
|
|
135
136
|
}
|
|
136
137
|
else {
|
|
137
138
|
// Leaf node with value
|
|
138
|
-
const hasComplexNew =
|
|
139
|
+
const hasComplexNew = isPlainObject(child.newValue) ||
|
|
139
140
|
(Array.isArray(child.newValue) &&
|
|
140
|
-
child.newValue.some((v) =>
|
|
141
|
-
const hasComplexOld =
|
|
141
|
+
child.newValue.some((v) => isPlainObject(v)));
|
|
142
|
+
const hasComplexOld = isPlainObject(child.oldValue) ||
|
|
142
143
|
(Array.isArray(child.oldValue) &&
|
|
143
|
-
child.oldValue.some((v) =>
|
|
144
|
+
child.oldValue.some((v) => isPlainObject(v)));
|
|
144
145
|
if (child.action === "add" && hasComplexNew) {
|
|
145
146
|
lines.push(style.color(`${indentStr}${style.symbol} ${child.name}:`));
|
|
146
147
|
lines.push(...renderNestedValue(child.newValue, child.action, indent + 1));
|
|
@@ -188,47 +189,10 @@ export function formatPropertyTree(diffs) {
|
|
|
188
189
|
}
|
|
189
190
|
/**
|
|
190
191
|
* Format a full ruleset config as tree lines (for create action).
|
|
192
|
+
* Delegates to renderNestedObject which handles recursive rendering.
|
|
191
193
|
*/
|
|
192
194
|
function formatFullConfig(ruleset, indent = 2) {
|
|
193
|
-
|
|
194
|
-
const style = getActionStyle("add");
|
|
195
|
-
function renderValue(key, value, currentIndent) {
|
|
196
|
-
const pad = " ".repeat(currentIndent);
|
|
197
|
-
if (value === null || value === undefined)
|
|
198
|
-
return;
|
|
199
|
-
if (Array.isArray(value)) {
|
|
200
|
-
if (value.length === 0) {
|
|
201
|
-
lines.push(style.color(`${pad}+ ${key}: []`));
|
|
202
|
-
}
|
|
203
|
-
else if (value.every((v) => typeof v !== "object")) {
|
|
204
|
-
lines.push(style.color(`${pad}+ ${key}: ${formatValue(value)}`));
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
lines.push(style.color(`${pad}+ ${key}:`));
|
|
208
|
-
for (const item of value) {
|
|
209
|
-
if (typeof item === "object" && item !== null) {
|
|
210
|
-
lines.push(style.color(`${pad} + ${JSON.stringify(item)}`));
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
lines.push(style.color(`${pad} + ${formatValue(item)}`));
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
else if (typeof value === "object") {
|
|
219
|
-
lines.push(style.color(`${pad}+ ${key}:`));
|
|
220
|
-
for (const [k, v] of Object.entries(value)) {
|
|
221
|
-
renderValue(k, v, currentIndent + 1);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
lines.push(style.color(`${pad}+ ${key}: ${formatValue(value)}`));
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
for (const [key, value] of Object.entries(ruleset)) {
|
|
229
|
-
renderValue(key, value, indent);
|
|
230
|
-
}
|
|
231
|
-
return lines;
|
|
195
|
+
return renderNestedObject(ruleset, "add", indent);
|
|
232
196
|
}
|
|
233
197
|
/**
|
|
234
198
|
* Format ruleset changes as a Terraform-style plan.
|
|
@@ -244,11 +208,11 @@ export function formatRulesetPlan(changes) {
|
|
|
244
208
|
const createChanges = changes.filter((c) => c.action === "create");
|
|
245
209
|
const updateChanges = changes.filter((c) => c.action === "update");
|
|
246
210
|
const deleteChanges = changes.filter((c) => c.action === "delete");
|
|
247
|
-
const
|
|
211
|
+
const unchangedItems = changes.filter((c) => c.action === "unchanged");
|
|
248
212
|
creates = createChanges.length;
|
|
249
213
|
updates = updateChanges.length;
|
|
250
214
|
deletes = deleteChanges.length;
|
|
251
|
-
unchanged =
|
|
215
|
+
unchanged = unchangedItems.length;
|
|
252
216
|
if (createChanges.length > 0) {
|
|
253
217
|
lines.push(chalk.bold(" Create:"));
|
|
254
218
|
for (const change of createChanges) {
|
|
@@ -305,7 +269,7 @@ export function formatRulesetPlan(changes) {
|
|
|
305
269
|
}
|
|
306
270
|
lines.push(""); // Blank line after deletes
|
|
307
271
|
}
|
|
308
|
-
for (const change of
|
|
272
|
+
for (const change of unchangedItems) {
|
|
309
273
|
entries.push({ name: change.name, action: "unchanged" });
|
|
310
274
|
}
|
|
311
275
|
return { lines, creates, updates, deletes, unchanged, entries };
|
|
@@ -17,33 +17,15 @@ interface GitHubRulesetPayload {
|
|
|
17
17
|
}
|
|
18
18
|
interface GitHubRulesetStrategyOptions {
|
|
19
19
|
retries?: number;
|
|
20
|
+
cwd: string;
|
|
20
21
|
}
|
|
21
|
-
/**
|
|
22
|
-
* GitHub Ruleset Strategy for managing repository rulesets via GitHub REST API.
|
|
23
|
-
* Uses `gh api` CLI for authentication and API calls.
|
|
24
|
-
*/
|
|
25
22
|
export declare class GitHubRulesetStrategy implements IRulesetStrategy {
|
|
26
23
|
private api;
|
|
27
|
-
constructor(executor
|
|
28
|
-
/**
|
|
29
|
-
* Lists all rulesets for a repository.
|
|
30
|
-
*/
|
|
24
|
+
constructor(executor: ICommandExecutor, options: GitHubRulesetStrategyOptions);
|
|
31
25
|
list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubRuleset[]>;
|
|
32
|
-
/**
|
|
33
|
-
* Gets a single ruleset by ID.
|
|
34
|
-
*/
|
|
35
26
|
get(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<GitHubRuleset>;
|
|
36
|
-
/**
|
|
37
|
-
* Creates a new ruleset.
|
|
38
|
-
*/
|
|
39
27
|
create(repoInfo: RepoInfo, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
|
|
40
|
-
/**
|
|
41
|
-
* Updates an existing ruleset.
|
|
42
|
-
*/
|
|
43
28
|
update(repoInfo: RepoInfo, rulesetId: number, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
|
|
44
|
-
/**
|
|
45
|
-
* Deletes a ruleset.
|
|
46
|
-
*/
|
|
47
29
|
delete(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<void>;
|
|
48
30
|
}
|
|
49
31
|
export {};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { defaultExecutor, } from "../../shared/command-executor.js";
|
|
2
1
|
import { assertGitHubRepo } from "../../shared/repo-detector.js";
|
|
3
2
|
import { camelToSnake } from "../../shared/string-utils.js";
|
|
4
3
|
import { GhApiClient, parseApiJson, } from "../../shared/gh-api-utils.js";
|
|
@@ -33,10 +32,6 @@ export function configToGitHub(name, ruleset) {
|
|
|
33
32
|
}
|
|
34
33
|
return payload;
|
|
35
34
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Default parameters for pull_request rules.
|
|
38
|
-
* GitHub API requires all parameters to be present.
|
|
39
|
-
*/
|
|
40
35
|
const PULL_REQUEST_DEFAULTS = {
|
|
41
36
|
required_approving_review_count: 0,
|
|
42
37
|
dismiss_stale_reviews_on_push: false,
|
|
@@ -98,59 +93,40 @@ function convertValue(value) {
|
|
|
98
93
|
}
|
|
99
94
|
return value;
|
|
100
95
|
}
|
|
101
|
-
/**
|
|
102
|
-
* GitHub Ruleset Strategy for managing repository rulesets via GitHub REST API.
|
|
103
|
-
* Uses `gh api` CLI for authentication and API calls.
|
|
104
|
-
*/
|
|
105
96
|
export class GitHubRulesetStrategy {
|
|
106
97
|
api;
|
|
107
98
|
constructor(executor, options) {
|
|
108
|
-
this.api = new GhApiClient(executor
|
|
99
|
+
this.api = new GhApiClient(executor, options.retries ?? 3, options.cwd);
|
|
109
100
|
}
|
|
110
|
-
/**
|
|
111
|
-
* Lists all rulesets for a repository.
|
|
112
|
-
*/
|
|
113
101
|
async list(repoInfo, options) {
|
|
114
102
|
assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
|
|
115
103
|
const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets`;
|
|
116
|
-
const result = await this.api.call("GET", endpoint,
|
|
104
|
+
const result = await this.api.call("GET", endpoint, { options });
|
|
117
105
|
return parseApiJson(result, "rulesets response");
|
|
118
106
|
}
|
|
119
|
-
/**
|
|
120
|
-
* Gets a single ruleset by ID.
|
|
121
|
-
*/
|
|
122
107
|
async get(repoInfo, rulesetId, options) {
|
|
123
108
|
assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
|
|
124
109
|
const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets/${rulesetId}`;
|
|
125
|
-
const result = await this.api.call("GET", endpoint,
|
|
110
|
+
const result = await this.api.call("GET", endpoint, { options });
|
|
126
111
|
return parseApiJson(result, "ruleset response");
|
|
127
112
|
}
|
|
128
|
-
/**
|
|
129
|
-
* Creates a new ruleset.
|
|
130
|
-
*/
|
|
131
113
|
async create(repoInfo, name, ruleset, options) {
|
|
132
114
|
assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
|
|
133
115
|
const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets`;
|
|
134
116
|
const payload = configToGitHub(name, ruleset);
|
|
135
|
-
const result = await this.api.call("POST", endpoint, payload, options);
|
|
117
|
+
const result = await this.api.call("POST", endpoint, { payload, options });
|
|
136
118
|
return parseApiJson(result, "ruleset response");
|
|
137
119
|
}
|
|
138
|
-
/**
|
|
139
|
-
* Updates an existing ruleset.
|
|
140
|
-
*/
|
|
141
120
|
async update(repoInfo, rulesetId, name, ruleset, options) {
|
|
142
121
|
assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
|
|
143
122
|
const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets/${rulesetId}`;
|
|
144
123
|
const payload = configToGitHub(name, ruleset);
|
|
145
|
-
const result = await this.api.call("PUT", endpoint, payload, options);
|
|
124
|
+
const result = await this.api.call("PUT", endpoint, { payload, options });
|
|
146
125
|
return parseApiJson(result, "ruleset response");
|
|
147
126
|
}
|
|
148
|
-
/**
|
|
149
|
-
* Deletes a ruleset.
|
|
150
|
-
*/
|
|
151
127
|
async delete(repoInfo, rulesetId, options) {
|
|
152
128
|
assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
|
|
153
129
|
const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets/${rulesetId}`;
|
|
154
|
-
await this.api.call("DELETE", endpoint,
|
|
130
|
+
await this.api.call("DELETE", endpoint, { options });
|
|
155
131
|
}
|
|
156
132
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { computePropertyDiffs, deepEqual,
|
|
1
|
+
export { computePropertyDiffs, deepEqual, isArrayOfObjects, type PropertyDiff, } from "./diff-algorithm.js";
|
|
2
2
|
export { formatPropertyTree, type RulesetPlanEntry } from "./formatter.js";
|
|
3
3
|
export { RulesetProcessor, type IRulesetProcessor } from "./processor.js";
|
|
4
|
+
export { GitHubRulesetStrategy } from "./github-ruleset-strategy.js";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Diff algorithm - property-level diffing for ruleset comparisons
|
|
2
|
-
export { computePropertyDiffs, deepEqual,
|
|
2
|
+
export { computePropertyDiffs, deepEqual, isArrayOfObjects, } from "./diff-algorithm.js";
|
|
3
3
|
// Formatter
|
|
4
4
|
export { formatPropertyTree } from "./formatter.js";
|
|
5
5
|
// Processor
|
|
6
6
|
export { RulesetProcessor } from "./processor.js";
|
|
7
|
+
// Strategy
|
|
8
|
+
export { GitHubRulesetStrategy } from "./github-ruleset-strategy.js";
|
|
@@ -17,7 +17,7 @@ export interface RulesetProcessorResult extends BaseProcessorResult {
|
|
|
17
17
|
*/
|
|
18
18
|
export declare class RulesetProcessor implements IRulesetProcessor {
|
|
19
19
|
private readonly strategy;
|
|
20
|
-
constructor(strategy
|
|
20
|
+
constructor(strategy: IRulesetStrategy);
|
|
21
21
|
process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: RulesetProcessorOptions): Promise<RulesetProcessorResult>;
|
|
22
|
-
private
|
|
22
|
+
private applySettings;
|
|
23
23
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { GitHubRulesetStrategy } from "./github-ruleset-strategy.js";
|
|
2
1
|
import { diffRulesets } from "./diff.js";
|
|
3
2
|
import { formatRulesetPlan } from "./formatter.js";
|
|
4
3
|
import { withGitHubGuards, countActions, buildDryRunResult, buildApplyResult, } from "../base-processor.js";
|
|
@@ -9,16 +8,16 @@ import { withGitHubGuards, countActions, buildDryRunResult, buildApplyResult, }
|
|
|
9
8
|
export class RulesetProcessor {
|
|
10
9
|
strategy;
|
|
11
10
|
constructor(strategy) {
|
|
12
|
-
this.strategy = strategy
|
|
11
|
+
this.strategy = strategy;
|
|
13
12
|
}
|
|
14
13
|
async process(repoConfig, repoInfo, options) {
|
|
15
14
|
return withGitHubGuards(repoConfig, repoInfo, options, {
|
|
16
15
|
hasDesiredSettings: (rc) => Object.keys(rc.settings?.rulesets ?? {}).length > 0,
|
|
17
16
|
emptySettingsMessage: "No rulesets configured",
|
|
18
|
-
|
|
17
|
+
applySettings: (githubRepo, rc, opts, token, repoName) => this.applySettings(githubRepo, rc, opts, token, repoName),
|
|
19
18
|
});
|
|
20
19
|
}
|
|
21
|
-
async
|
|
20
|
+
async applySettings(githubRepo, repoConfig, options, effectiveToken, repoName) {
|
|
22
21
|
const { dryRun, noDelete } = options;
|
|
23
22
|
const settings = repoConfig.settings;
|
|
24
23
|
const desiredRulesets = settings?.rulesets ?? {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ValidationError } from "./errors.js";
|
|
1
2
|
export function sanitizeBranchName(fileName) {
|
|
2
3
|
return fileName
|
|
3
4
|
.toLowerCase()
|
|
@@ -12,18 +13,18 @@ export function sanitizeBranchName(fileName) {
|
|
|
12
13
|
*/
|
|
13
14
|
export function validateBranchName(branchName) {
|
|
14
15
|
if (!branchName || branchName.trim() === "") {
|
|
15
|
-
throw new
|
|
16
|
+
throw new ValidationError("Branch name cannot be empty");
|
|
16
17
|
}
|
|
17
18
|
if (branchName.startsWith(".") || branchName.startsWith("-")) {
|
|
18
|
-
throw new
|
|
19
|
+
throw new ValidationError('Branch name cannot start with "." or "-"');
|
|
19
20
|
}
|
|
20
21
|
// Git disallows: space, ~, ^, :, ?, *, [, \, and consecutive dots (..)
|
|
21
22
|
if (/[\s~^:?*[\\]/.test(branchName) || branchName.includes("..")) {
|
|
22
|
-
throw new
|
|
23
|
+
throw new ValidationError("Branch name contains invalid characters");
|
|
23
24
|
}
|
|
24
25
|
if (branchName.endsWith("/") ||
|
|
25
26
|
branchName.endsWith(".lock") ||
|
|
26
27
|
branchName.endsWith(".")) {
|
|
27
|
-
throw new
|
|
28
|
+
throw new ValidationError("Branch name has invalid ending");
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -6,8 +6,9 @@ export interface ICommandExecutor {
|
|
|
6
6
|
exec(command: string, cwd: string, options?: ExecOptions): Promise<string>;
|
|
7
7
|
}
|
|
8
8
|
export declare class ShellCommandExecutor implements ICommandExecutor {
|
|
9
|
+
private readonly baseEnv;
|
|
10
|
+
constructor(baseEnv: Record<string, string | undefined>);
|
|
9
11
|
exec(command: string, cwd: string, options?: ExecOptions): Promise<string>;
|
|
10
12
|
}
|
|
11
|
-
export declare const defaultExecutor: ICommandExecutor;
|
|
12
13
|
/** Extract stderr string from an exec error (child_process errors attach stderr). */
|
|
13
14
|
export declare function getStderr(error: unknown): string;
|