@aspruyt/xfg 6.1.0 → 6.3.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.
Files changed (174) hide show
  1. package/dist/cli/lifecycle-report-builder.d.ts +2 -2
  2. package/dist/cli/lifecycle-report-builder.js +3 -11
  3. package/dist/cli/program.d.ts +2 -1
  4. package/dist/cli/program.js +43 -6
  5. package/dist/cli/repo-sync-runner.d.ts +24 -0
  6. package/dist/cli/repo-sync-runner.js +156 -0
  7. package/dist/cli/results-collector.d.ts +1 -1
  8. package/dist/cli/results-collector.js +2 -2
  9. package/dist/cli/secrets-command.d.ts +25 -0
  10. package/dist/cli/secrets-command.js +75 -0
  11. package/dist/cli/settings-factories.d.ts +8 -0
  12. package/dist/cli/settings-factories.js +32 -0
  13. package/dist/cli/settings-report-builder.d.ts +7 -2
  14. package/dist/cli/settings-report-builder.js +28 -20
  15. package/dist/cli/settings-runner.d.ts +2 -0
  16. package/dist/cli/settings-runner.js +94 -0
  17. package/dist/cli/sync-command.d.ts +1 -1
  18. package/dist/cli/sync-command.js +31 -372
  19. package/dist/cli/sync-report-builder.d.ts +1 -1
  20. package/dist/cli/sync-utils.d.ts +8 -0
  21. package/dist/cli/sync-utils.js +36 -0
  22. package/dist/cli/types.d.ts +8 -8
  23. package/dist/cli/unified-summary.d.ts +1 -3
  24. package/dist/cli/unified-summary.js +7 -5
  25. package/dist/cli.js +2 -1
  26. package/dist/{shared → config}/env.js +2 -2
  27. package/dist/config/extends-resolver.js +4 -3
  28. package/dist/config/file-reference-resolver.js +4 -2
  29. package/dist/config/formatter.js +1 -0
  30. package/dist/config/index.d.ts +2 -2
  31. package/dist/config/index.js +1 -1
  32. package/dist/config/loader.js +30 -6
  33. package/dist/config/merge.d.ts +11 -1
  34. package/dist/config/merge.js +78 -6
  35. package/dist/config/normalizer.js +129 -49
  36. package/dist/config/types.d.ts +20 -0
  37. package/dist/config/validator.d.ts +5 -4
  38. package/dist/config/validator.js +187 -614
  39. package/dist/config/validators/file-validator.d.ts +2 -1
  40. package/dist/config/validators/file-validator.js +9 -1
  41. package/dist/config/validators/group-validator.d.ts +3 -0
  42. package/dist/config/validators/group-validator.js +167 -0
  43. package/dist/config/validators/repo-entry-validator.d.ts +2 -0
  44. package/dist/config/validators/repo-entry-validator.js +165 -0
  45. package/dist/config/validators/repo-settings-validator.js +18 -7
  46. package/dist/config/validators/ruleset-validator.js +2 -5
  47. package/dist/config/validators/shared.d.ts +11 -0
  48. package/dist/config/validators/shared.js +242 -0
  49. package/dist/lifecycle/ado-migration-source.js +2 -4
  50. package/dist/lifecycle/github-lifecycle-provider.d.ts +7 -11
  51. package/dist/lifecycle/github-lifecycle-provider.js +125 -136
  52. package/dist/lifecycle/{lifecycle-helpers.d.ts → helpers.d.ts} +5 -1
  53. package/dist/lifecycle/{lifecycle-helpers.js → helpers.js} +9 -8
  54. package/dist/lifecycle/index.d.ts +2 -2
  55. package/dist/lifecycle/index.js +1 -1
  56. package/dist/lifecycle/repo-lifecycle-factory.d.ts +2 -2
  57. package/dist/output/github-summary.js +2 -3
  58. package/dist/output/index.d.ts +4 -0
  59. package/dist/output/index.js +4 -0
  60. package/dist/output/lifecycle-report.d.ts +1 -1
  61. package/dist/output/lifecycle-report.js +5 -0
  62. package/dist/output/settings-report.d.ts +11 -0
  63. package/dist/output/settings-report.js +24 -0
  64. package/dist/output/sync-report.d.ts +25 -3
  65. package/dist/output/sync-report.js +11 -11
  66. package/dist/secrets/encryption.d.ts +9 -0
  67. package/dist/secrets/encryption.js +29 -0
  68. package/dist/secrets/github-secrets-strategy.d.ts +17 -0
  69. package/dist/secrets/github-secrets-strategy.js +38 -0
  70. package/dist/secrets/index.d.ts +5 -0
  71. package/dist/secrets/index.js +3 -0
  72. package/dist/secrets/processor.d.ts +31 -0
  73. package/dist/secrets/processor.js +115 -0
  74. package/dist/secrets/types.d.ts +21 -0
  75. package/dist/settings/base-processor.d.ts +18 -7
  76. package/dist/settings/base-processor.js +26 -5
  77. package/dist/settings/code-scanning/diff.js +2 -2
  78. package/dist/settings/code-scanning/formatter.d.ts +2 -6
  79. package/dist/settings/code-scanning/formatter.js +2 -25
  80. package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +3 -7
  81. package/dist/settings/code-scanning/github-code-scanning-strategy.js +2 -2
  82. package/dist/settings/code-scanning/processor.js +6 -4
  83. package/dist/settings/code-scanning/types.d.ts +10 -8
  84. package/dist/settings/index.d.ts +1 -0
  85. package/dist/settings/index.js +2 -0
  86. package/dist/settings/labels/github-labels-strategy.d.ts +3 -11
  87. package/dist/settings/labels/types.d.ts +12 -10
  88. package/dist/settings/repo-settings/diff.d.ts +1 -1
  89. package/dist/settings/repo-settings/diff.js +1 -1
  90. package/dist/settings/repo-settings/formatter.d.ts +2 -6
  91. package/dist/settings/repo-settings/formatter.js +4 -23
  92. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -2
  93. package/dist/settings/repo-settings/github-repo-settings-strategy.js +8 -7
  94. package/dist/settings/repo-settings/processor.js +11 -11
  95. package/dist/settings/repo-settings/types.d.ts +2 -2
  96. package/dist/settings/rulesets/diff-algorithm.js +4 -2
  97. package/dist/settings/rulesets/diff.js +2 -51
  98. package/dist/settings/rulesets/formatter.js +4 -0
  99. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +3 -3
  100. package/dist/settings/rulesets/github-ruleset-strategy.js +4 -6
  101. package/dist/settings/rulesets/index.d.ts +1 -1
  102. package/dist/settings/rulesets/index.js +0 -2
  103. package/dist/settings/rulesets/processor.js +1 -1
  104. package/dist/settings/rulesets/types.d.ts +6 -2
  105. package/dist/settings/variables/diff.d.ts +10 -0
  106. package/dist/settings/variables/diff.js +39 -0
  107. package/dist/settings/variables/formatter.d.ts +16 -0
  108. package/dist/settings/variables/formatter.js +70 -0
  109. package/dist/settings/variables/github-variables-strategy.d.ts +17 -0
  110. package/dist/settings/variables/github-variables-strategy.js +40 -0
  111. package/dist/settings/variables/index.d.ts +4 -0
  112. package/dist/settings/variables/index.js +2 -0
  113. package/dist/settings/variables/processor.d.ts +19 -0
  114. package/dist/settings/variables/processor.js +60 -0
  115. package/dist/settings/variables/types.d.ts +18 -0
  116. package/dist/settings/variables/types.js +1 -0
  117. package/dist/shared/command-executor.d.ts +4 -4
  118. package/dist/shared/command-executor.js +9 -7
  119. package/dist/shared/diff-format.d.ts +1 -0
  120. package/dist/shared/diff-format.js +10 -0
  121. package/dist/shared/env-resolver.d.ts +16 -0
  122. package/dist/shared/env-resolver.js +33 -0
  123. package/dist/shared/errors.d.ts +7 -4
  124. package/dist/shared/errors.js +8 -8
  125. package/dist/shared/gh-api-utils.d.ts +3 -34
  126. package/dist/shared/gh-api-utils.js +23 -53
  127. package/dist/shared/gh-token-utils.d.ts +26 -0
  128. package/dist/shared/gh-token-utils.js +32 -0
  129. package/dist/shared/json-utils.js +1 -1
  130. package/dist/shared/regex-utils.d.ts +1 -0
  131. package/dist/shared/regex-utils.js +3 -0
  132. package/dist/shared/retry-utils.d.ts +1 -0
  133. package/dist/shared/retry-utils.js +13 -7
  134. package/dist/sync/auth-options-builder.js +1 -1
  135. package/dist/sync/branch-manager.js +5 -3
  136. package/dist/sync/commit-push-manager.js +2 -3
  137. package/dist/sync/diff-utils.d.ts +0 -1
  138. package/dist/sync/diff-utils.js +5 -10
  139. package/dist/sync/file-sync-orchestrator.js +0 -2
  140. package/dist/sync/file-writer.d.ts +3 -0
  141. package/dist/sync/file-writer.js +84 -81
  142. package/dist/sync/index.d.ts +0 -1
  143. package/dist/sync/index.js +0 -1
  144. package/dist/sync/manifest.js +1 -1
  145. package/dist/sync/pr-merge-handler.js +6 -6
  146. package/dist/sync/sync-workflow.js +1 -1
  147. package/dist/sync/types.d.ts +2 -2
  148. package/dist/vcs/ado-pr-strategy.d.ts +3 -5
  149. package/dist/vcs/ado-pr-strategy.js +131 -33
  150. package/dist/vcs/authenticated-git-ops.js +45 -23
  151. package/dist/vcs/git-commit-strategy.js +10 -6
  152. package/dist/vcs/git-ops.js +30 -24
  153. package/dist/vcs/github-pr-strategy.d.ts +3 -2
  154. package/dist/vcs/github-pr-strategy.js +80 -30
  155. package/dist/vcs/gitlab-pr-strategy.d.ts +2 -5
  156. package/dist/vcs/gitlab-pr-strategy.js +88 -87
  157. package/dist/vcs/graphql-commit-strategy.d.ts +1 -5
  158. package/dist/vcs/graphql-commit-strategy.js +21 -37
  159. package/dist/vcs/pr-creator.js +9 -2
  160. package/dist/vcs/pr-strategy.d.ts +2 -3
  161. package/dist/vcs/pr-strategy.js +0 -1
  162. package/dist/vcs/types.d.ts +9 -5
  163. package/package.json +7 -5
  164. package/dist/config/validators/index.d.ts +0 -3
  165. package/dist/config/validators/index.js +0 -6
  166. package/dist/output/types.d.ts +0 -20
  167. package/dist/shared/shell-utils.d.ts +0 -6
  168. package/dist/shared/shell-utils.js +0 -17
  169. /package/dist/{shared → config}/env.d.ts +0 -0
  170. /package/dist/lifecycle/{lifecycle-formatter.d.ts → formatter.d.ts} +0 -0
  171. /package/dist/lifecycle/{lifecycle-formatter.js → formatter.js} +0 -0
  172. /package/dist/{output → secrets}/types.js +0 -0
  173. /package/dist/{vcs → shared}/sanitize-utils.d.ts +0 -0
  174. /package/dist/{vcs → shared}/sanitize-utils.js +0 -0
@@ -17,6 +17,11 @@ export interface SettingsReport {
17
17
  update: number;
18
18
  delete: number;
19
19
  };
20
+ variables?: {
21
+ create: number;
22
+ update: number;
23
+ delete: number;
24
+ };
20
25
  };
21
26
  }
22
27
  export interface RepoChanges {
@@ -24,6 +29,12 @@ export interface RepoChanges {
24
29
  settings: SettingChange[];
25
30
  rulesets: RulesetChange[];
26
31
  labels: LabelChange[];
32
+ variables?: {
33
+ name: string;
34
+ action: ActiveAction;
35
+ oldValue?: string;
36
+ newValue?: string;
37
+ }[];
27
38
  error?: string;
28
39
  }
29
40
  export interface SettingChange {
@@ -90,6 +90,13 @@ function formatSettingsSummary(totals) {
90
90
  ]);
91
91
  if (labelsEntry)
92
92
  parts.push(labelsEntry);
93
+ const variablesEntry = formatCountEntry("variable", "variables", [
94
+ { label: "to create", value: totals.variables?.create ?? 0 },
95
+ { label: "to update", value: totals.variables?.update ?? 0 },
96
+ { label: "to delete", value: totals.variables?.delete ?? 0 },
97
+ ]);
98
+ if (variablesEntry)
99
+ parts.push(variablesEntry);
93
100
  if (parts.length === 0) {
94
101
  return "No changes";
95
102
  }
@@ -112,6 +119,7 @@ export function formatSettingsReportCLI(report) {
112
119
  if (repo.settings.length === 0 &&
113
120
  repo.rulesets.length === 0 &&
114
121
  repo.labels.length === 0 &&
122
+ (repo.variables ?? []).length === 0 &&
115
123
  !repo.error) {
116
124
  continue;
117
125
  }
@@ -231,6 +239,21 @@ export function renderRepoSettingsDiffLines(repo, diffLines) {
231
239
  diffLines.push(`- label "${label.name}"`);
232
240
  }
233
241
  }
242
+ // Blank line before variables if there was content above
243
+ if ((repo.variables ?? []).length > 0 && diffLines.length > startLength) {
244
+ diffLines.push("");
245
+ }
246
+ for (const variable of repo.variables ?? []) {
247
+ if (variable.action === "create") {
248
+ diffLines.push(`+ variable "${variable.name}": ${formatValuePlain(variable.newValue)}`);
249
+ }
250
+ else if (variable.action === "update") {
251
+ diffLines.push(`! variable "${variable.name}": ${formatValuePlain(variable.oldValue)} → ${formatValuePlain(variable.newValue)}`);
252
+ }
253
+ else if (variable.action === "delete") {
254
+ diffLines.push(`- variable "${variable.name}"`);
255
+ }
256
+ }
234
257
  if (repo.error) {
235
258
  diffLines.push(`- Error: ${repo.error}`);
236
259
  }
@@ -252,6 +275,7 @@ export function formatSettingsReportMarkdown(report, dryRun) {
252
275
  if (repo.settings.length === 0 &&
253
276
  repo.rulesets.length === 0 &&
254
277
  repo.labels.length === 0 &&
278
+ (repo.variables ?? []).length === 0 &&
255
279
  !repo.error) {
256
280
  continue;
257
281
  }
@@ -1,6 +1,28 @@
1
- import type { SyncReport, RepoFileChanges, ReportFileChange } from "./types.js";
2
- export type { SyncReport, RepoFileChanges, ReportFileChange };
1
+ import type { MergeMode } from "../config/index.js";
2
+ import type { ActiveAction } from "../settings/index.js";
3
+ export interface ReportFileChange {
4
+ path: string;
5
+ action: ActiveAction;
6
+ diffLines?: string[];
7
+ }
8
+ export interface SyncReport {
9
+ repos: RepoFileChanges[];
10
+ totals: {
11
+ files: {
12
+ create: number;
13
+ update: number;
14
+ delete: number;
15
+ };
16
+ };
17
+ }
18
+ export interface RepoFileChanges {
19
+ repoName: string;
20
+ files: ReportFileChange[];
21
+ prUrl?: string;
22
+ mergeOutcome?: MergeMode;
23
+ error?: string;
24
+ }
3
25
  export declare function formatSyncReportCLI(report: SyncReport): string[];
4
26
  export declare function formatSyncReportMarkdown(report: SyncReport, dryRun: boolean): string;
5
- export declare function renderSyncLines(syncRepo: RepoFileChanges, diffLines: string[]): void;
27
+ export declare function renderSyncLines(syncRepo: RepoFileChanges): string[];
6
28
  export declare function writeSyncReportSummary(report: SyncReport, dryRun: boolean, summaryPath: string | undefined): void;
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { writeGitHubStepSummary } from "./github-summary.js";
3
3
  import { formatCountEntry } from "./settings-report.js";
4
- import { formatDiffLine } from "../sync/index.js";
4
+ import { formatDiffLine } from "../shared/diff-format.js";
5
5
  function formatSyncSummary(totals) {
6
6
  const entry = formatCountEntry("file", "files", [
7
7
  { label: "to create", value: totals.files.create },
@@ -65,8 +65,7 @@ export function formatSyncReportMarkdown(report, dryRun) {
65
65
  }
66
66
  lines.push(`### ${repo.repoName}`);
67
67
  lines.push("");
68
- const diffLines = [];
69
- renderSyncLines(repo, diffLines);
68
+ const diffLines = renderSyncLines(repo);
70
69
  if (diffLines.length > 0) {
71
70
  lines.push("```diff");
72
71
  lines.push(...diffLines);
@@ -78,28 +77,29 @@ export function formatSyncReportMarkdown(report, dryRun) {
78
77
  lines.push(`**${formatSyncSummary(report.totals)}**`);
79
78
  return lines.join("\n");
80
79
  }
81
- export function renderSyncLines(syncRepo, diffLines) {
80
+ export function renderSyncLines(syncRepo) {
81
+ const lines = [];
82
82
  for (let i = 0; i < syncRepo.files.length; i++) {
83
83
  const file = syncRepo.files[i];
84
- // Blank line between files for readability
85
84
  if (i > 0)
86
- diffLines.push("");
85
+ lines.push("");
87
86
  if (file.action === "create") {
88
- diffLines.push(`+ ${file.path}`);
87
+ lines.push(`+ ${file.path}`);
89
88
  }
90
89
  else if (file.action === "update") {
91
- diffLines.push(`! ${file.path}`);
90
+ lines.push(`! ${file.path}`);
92
91
  }
93
92
  else if (file.action === "delete") {
94
- diffLines.push(`- ${file.path}`);
93
+ lines.push(`- ${file.path}`);
95
94
  }
96
95
  if (file.diffLines) {
97
- diffLines.push(...file.diffLines);
96
+ lines.push(...file.diffLines);
98
97
  }
99
98
  }
100
99
  if (syncRepo.error) {
101
- diffLines.push(`- Error: ${syncRepo.error}`);
100
+ lines.push(`- Error: ${syncRepo.error}`);
102
101
  }
102
+ return lines;
103
103
  }
104
104
  export function writeSyncReportSummary(report, dryRun, summaryPath) {
105
105
  const markdown = formatSyncReportMarkdown(report, dryRun);
@@ -0,0 +1,9 @@
1
+ export interface ISecretEncryptor {
2
+ encrypt(value: string, publicKeyBase64: string): Promise<string>;
3
+ }
4
+ export declare class SodiumEncryptor implements ISecretEncryptor {
5
+ private sodium;
6
+ private initPromise;
7
+ private ensureInitialized;
8
+ encrypt(value: string, publicKeyBase64: string): Promise<string>;
9
+ }
@@ -0,0 +1,29 @@
1
+ export class SodiumEncryptor {
2
+ sodium;
3
+ initPromise = null;
4
+ ensureInitialized() {
5
+ if (!this.initPromise) {
6
+ this.initPromise = (async () => {
7
+ try {
8
+ const sodium = await import("libsodium-wrappers");
9
+ await sodium.default.ready;
10
+ this.sodium = sodium.default;
11
+ }
12
+ catch {
13
+ throw new Error("Failed to load libsodium-wrappers. Install it: npm install libsodium-wrappers");
14
+ }
15
+ })().catch((err) => {
16
+ this.initPromise = null;
17
+ throw err;
18
+ });
19
+ }
20
+ return this.initPromise.then(() => this.sodium);
21
+ }
22
+ async encrypt(value, publicKeyBase64) {
23
+ const sodium = await this.ensureInitialized();
24
+ const messageBytes = sodium.from_string(value);
25
+ const publicKey = sodium.from_base64(publicKeyBase64, sodium.base64_variants.ORIGINAL);
26
+ const encrypted = sodium.crypto_box_seal(messageBytes, publicKey);
27
+ return sodium.to_base64(encrypted, sodium.base64_variants.ORIGINAL);
28
+ }
29
+ }
@@ -0,0 +1,17 @@
1
+ import type { ICommandExecutor } from "../shared/command-executor.js";
2
+ import { type RepoInfo } from "../repo/index.js";
3
+ import { type GhApiOptions } from "../shared/gh-api-utils.js";
4
+ import type { ISecretsStrategy, GitHubSecret, GitHubPublicKey } from "./types.js";
5
+ interface GitHubSecretsStrategyOptions {
6
+ retries?: number;
7
+ cwd: string;
8
+ }
9
+ export declare class GitHubSecretsStrategy implements ISecretsStrategy {
10
+ private api;
11
+ constructor(executor: ICommandExecutor, options: GitHubSecretsStrategyOptions);
12
+ list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubSecret[]>;
13
+ getPublicKey(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubPublicKey>;
14
+ upsert(repoInfo: RepoInfo, name: string, encryptedValue: string, keyId: string, options?: GhApiOptions): Promise<void>;
15
+ delete(repoInfo: RepoInfo, name: string, options?: GhApiOptions): Promise<void>;
16
+ }
17
+ export {};
@@ -0,0 +1,38 @@
1
+ import { assertGitHubRepo } from "../repo/index.js";
2
+ import { GhApiClient } from "../shared/gh-api-utils.js";
3
+ import { parseApiJson } from "../shared/json-utils.js";
4
+ export class GitHubSecretsStrategy {
5
+ api;
6
+ constructor(executor, options) {
7
+ this.api = new GhApiClient(executor, options.retries ?? 3, options.cwd);
8
+ }
9
+ async list(repoInfo, options) {
10
+ assertGitHubRepo(repoInfo, "GitHub Secrets strategy");
11
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/secrets`;
12
+ const result = await this.api.call("GET", endpoint, {
13
+ options,
14
+ paginate: true,
15
+ });
16
+ const response = parseApiJson(result, "secrets response");
17
+ return response.secrets ?? [];
18
+ }
19
+ async getPublicKey(repoInfo, options) {
20
+ assertGitHubRepo(repoInfo, "GitHub Secrets strategy");
21
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/secrets/public-key`;
22
+ const result = await this.api.call("GET", endpoint, { options });
23
+ return parseApiJson(result, "public key response");
24
+ }
25
+ async upsert(repoInfo, name, encryptedValue, keyId, options) {
26
+ assertGitHubRepo(repoInfo, "GitHub Secrets strategy");
27
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/secrets/${encodeURIComponent(name)}`;
28
+ await this.api.call("PUT", endpoint, {
29
+ payload: { encrypted_value: encryptedValue, key_id: keyId },
30
+ options,
31
+ });
32
+ }
33
+ async delete(repoInfo, name, options) {
34
+ assertGitHubRepo(repoInfo, "GitHub Secrets strategy");
35
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/secrets/${encodeURIComponent(name)}`;
36
+ await this.api.call("DELETE", endpoint, { options });
37
+ }
38
+ }
@@ -0,0 +1,5 @@
1
+ export { SecretsProcessor } from "./processor.js";
2
+ export type { SecretsProcessorOptions, SecretsProcessorResult, } from "./processor.js";
3
+ export { GitHubSecretsStrategy } from "./github-secrets-strategy.js";
4
+ export { SodiumEncryptor, type ISecretEncryptor } from "./encryption.js";
5
+ export type { ISecretsStrategy, GitHubSecret, GitHubPublicKey, } from "./types.js";
@@ -0,0 +1,3 @@
1
+ export { SecretsProcessor } from "./processor.js";
2
+ export { GitHubSecretsStrategy } from "./github-secrets-strategy.js";
3
+ export { SodiumEncryptor } from "./encryption.js";
@@ -0,0 +1,31 @@
1
+ import { type RepoInfo } from "../repo/index.js";
2
+ import type { ISecretsStrategy } from "./types.js";
3
+ import type { ISecretEncryptor } from "./encryption.js";
4
+ import type { IEnvResolver } from "../shared/env-resolver.js";
5
+ import type { SecretConfig } from "../config/index.js";
6
+ type SecretsConfig = Record<string, SecretConfig | boolean> & {
7
+ deleteOrphaned?: boolean;
8
+ };
9
+ export interface SecretsProcessorOptions {
10
+ dryRun?: boolean;
11
+ token?: string;
12
+ noDelete?: boolean;
13
+ }
14
+ export interface SecretsProcessorResult {
15
+ success: boolean;
16
+ repoName: string;
17
+ message: string;
18
+ skipped?: boolean;
19
+ dryRun?: boolean;
20
+ created: number;
21
+ updated: number;
22
+ deleted: number;
23
+ }
24
+ export declare class SecretsProcessor {
25
+ private readonly strategy;
26
+ private readonly encryptor;
27
+ private readonly envResolver;
28
+ constructor(strategy: ISecretsStrategy, encryptor: ISecretEncryptor, envResolver: IEnvResolver);
29
+ process(secretsConfig: SecretsConfig, repoInfo: RepoInfo, options: SecretsProcessorOptions): Promise<SecretsProcessorResult>;
30
+ }
31
+ export {};
@@ -0,0 +1,115 @@
1
+ import { isGitHubRepo, getRepoDisplayName, } from "../repo/index.js";
2
+ export class SecretsProcessor {
3
+ strategy;
4
+ encryptor;
5
+ envResolver;
6
+ constructor(strategy, encryptor, envResolver) {
7
+ this.strategy = strategy;
8
+ this.encryptor = encryptor;
9
+ this.envResolver = envResolver;
10
+ }
11
+ async process(secretsConfig, repoInfo, options) {
12
+ const repoName = getRepoDisplayName(repoInfo);
13
+ if (!isGitHubRepo(repoInfo)) {
14
+ return {
15
+ success: true,
16
+ repoName,
17
+ message: "Skipped: not a GitHub repository",
18
+ skipped: true,
19
+ created: 0,
20
+ updated: 0,
21
+ deleted: 0,
22
+ };
23
+ }
24
+ const githubRepo = repoInfo;
25
+ const { deleteOrphaned: configDeleteOrphaned = false, ...rawEntries } = secretsConfig;
26
+ const { dryRun, token, noDelete } = options;
27
+ const deleteOrphaned = configDeleteOrphaned && !(noDelete ?? false);
28
+ const strategyOptions = { token, host: githubRepo.host };
29
+ const secretEntries = Object.entries(rawEntries).filter((entry) => typeof entry[1] !== "boolean");
30
+ let resolvedValues;
31
+ if (!dryRun && secretEntries.length > 0) {
32
+ resolvedValues = this.envResolver.resolveAll(secretEntries.map(([name, config]) => ({
33
+ name,
34
+ envVar: config.env,
35
+ })));
36
+ }
37
+ else {
38
+ resolvedValues = new Map();
39
+ }
40
+ const currentSecrets = await this.strategy.list(githubRepo, strategyOptions);
41
+ const currentByName = new Set(currentSecrets.map((s) => s.name.toUpperCase()));
42
+ const desiredNames = new Set(secretEntries.map(([name]) => name.toUpperCase()));
43
+ let created = 0;
44
+ let updated = 0;
45
+ let deleted = 0;
46
+ if (!dryRun) {
47
+ if (secretEntries.length > 0) {
48
+ const publicKey = await this.strategy.getPublicKey(githubRepo, strategyOptions);
49
+ for (const [name] of secretEntries) {
50
+ const value = resolvedValues.get(name);
51
+ const encrypted = await this.encryptor.encrypt(value, publicKey.key);
52
+ await this.strategy.upsert(githubRepo, name, encrypted, publicKey.key_id, strategyOptions);
53
+ if (currentByName.has(name.toUpperCase())) {
54
+ updated++;
55
+ }
56
+ else {
57
+ created++;
58
+ }
59
+ }
60
+ }
61
+ if (deleteOrphaned) {
62
+ for (const current of currentSecrets) {
63
+ if (!desiredNames.has(current.name.toUpperCase())) {
64
+ await this.strategy.delete(githubRepo, current.name, strategyOptions);
65
+ deleted++;
66
+ }
67
+ }
68
+ }
69
+ }
70
+ else {
71
+ for (const [name] of secretEntries) {
72
+ if (currentByName.has(name.toUpperCase())) {
73
+ updated++;
74
+ }
75
+ else {
76
+ created++;
77
+ }
78
+ }
79
+ if (deleteOrphaned) {
80
+ for (const current of currentSecrets) {
81
+ if (!desiredNames.has(current.name.toUpperCase())) {
82
+ deleted++;
83
+ }
84
+ }
85
+ }
86
+ }
87
+ const parts = [];
88
+ if (created > 0)
89
+ parts.push(`${created} created`);
90
+ if (updated > 0)
91
+ parts.push(`${updated} updated`);
92
+ if (deleted > 0)
93
+ parts.push(`${deleted} deleted`);
94
+ const summary = parts.length > 0 ? parts.join(", ") : "no changes";
95
+ if (dryRun) {
96
+ return {
97
+ success: true,
98
+ repoName,
99
+ message: `[DRY RUN] ${summary}`,
100
+ dryRun: true,
101
+ created,
102
+ updated,
103
+ deleted,
104
+ };
105
+ }
106
+ return {
107
+ success: true,
108
+ repoName,
109
+ message: summary,
110
+ created,
111
+ updated,
112
+ deleted,
113
+ };
114
+ }
115
+ }
@@ -0,0 +1,21 @@
1
+ import type { RepoInfo } from "../repo/index.js";
2
+ import type { GhApiOptions } from "../shared/gh-api-utils.js";
3
+ export interface GitHubSecret {
4
+ name: string;
5
+ created_at: string;
6
+ updated_at: string;
7
+ }
8
+ export interface GitHubSecretsListResponse {
9
+ total_count: number;
10
+ secrets: GitHubSecret[];
11
+ }
12
+ export interface GitHubPublicKey {
13
+ key_id: string;
14
+ key: string;
15
+ }
16
+ export interface ISecretsStrategy {
17
+ list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubSecret[]>;
18
+ getPublicKey(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubPublicKey>;
19
+ upsert(repoInfo: RepoInfo, name: string, encryptedValue: string, keyId: string, options?: GhApiOptions): Promise<void>;
20
+ delete(repoInfo: RepoInfo, name: string, options?: GhApiOptions): Promise<void>;
21
+ }
@@ -33,11 +33,8 @@ interface SettingsGuards<TOptions extends BaseProcessorOptions, TResult extends
33
33
  * empty settings check, token resolution, and error wrapping.
34
34
  */
35
35
  export declare function withGitHubGuards<TOptions extends BaseProcessorOptions, TResult extends BaseProcessorResult>(repoConfig: RepoConfig, repoInfo: RepoInfo, options: TOptions, guards: SettingsGuards<TOptions, TResult>): Promise<TResult>;
36
- /** Common action literals shared by all settings processors. */
37
36
  export type SettingsAction = "create" | "update" | "delete" | "unchanged";
38
- /** Actions that represent an actual change (excludes "unchanged"). */
39
37
  export type ActiveAction = Exclude<SettingsAction, "unchanged">;
40
- /** Type predicate that narrows entries with an active (non-"unchanged") action. */
41
38
  export declare function isActiveAction<T extends {
42
39
  action: SettingsAction;
43
40
  }>(entry: T): entry is T & {
@@ -49,14 +46,28 @@ export interface ChangeCounts {
49
46
  delete: number;
50
47
  unchanged: number;
51
48
  }
52
- /**
53
- * Count actions from a diff result array.
54
- * Works with any change type that has an `action` field.
55
- */
56
49
  export declare function countActions(changes: ReadonlyArray<{
57
50
  action: SettingsAction;
58
51
  }>): ChangeCounts;
59
52
  export declare function formatChangeSummary(counts: ChangeCounts): string;
53
+ export interface PlanEntry {
54
+ property: string;
55
+ action: "create" | "update";
56
+ oldValue?: unknown;
57
+ newValue?: unknown;
58
+ }
59
+ export interface FormatChangeResult {
60
+ lines: string[];
61
+ entries: PlanEntry[];
62
+ creates: number;
63
+ updates: number;
64
+ }
65
+ export declare function formatChangeLines(changes: ReadonlyArray<{
66
+ property: string;
67
+ action: SettingsAction;
68
+ oldValue?: unknown;
69
+ newValue?: unknown;
70
+ }>, formatValue: (val: unknown) => string): FormatChangeResult;
60
71
  /**
61
72
  * Build a standardized dry-run result for settings processors.
62
73
  * Returns an intersection of BaseProcessorResult with the extra fields,
@@ -1,3 +1,4 @@
1
+ import chalk from "chalk";
1
2
  import { isGitHubRepo, getRepoDisplayName } from "../repo/index.js";
2
3
  import { toErrorMessage } from "../shared/type-guards.js";
3
4
  /**
@@ -43,14 +44,9 @@ export async function withGitHubGuards(repoConfig, repoInfo, options, guards) {
43
44
  });
44
45
  }
45
46
  }
46
- /** Type predicate that narrows entries with an active (non-"unchanged") action. */
47
47
  export function isActiveAction(entry) {
48
48
  return entry.action !== "unchanged";
49
49
  }
50
- /**
51
- * Count actions from a diff result array.
52
- * Works with any change type that has an `action` field.
53
- */
54
50
  export function countActions(changes) {
55
51
  const counts = {
56
52
  create: 0,
@@ -75,6 +71,31 @@ export function formatChangeSummary(counts) {
75
71
  parts.push(`${counts.unchanged} unchanged`);
76
72
  return parts.length > 0 ? parts.join(", ") : "no changes";
77
73
  }
74
+ export function formatChangeLines(changes, formatValue) {
75
+ const lines = [];
76
+ const entries = [];
77
+ const { create: creates, update: updates } = countActions(changes);
78
+ for (const change of changes) {
79
+ if (change.action === "create") {
80
+ lines.push(chalk.green(` + ${change.property}: ${formatValue(change.newValue)}`));
81
+ entries.push({
82
+ property: change.property,
83
+ action: "create",
84
+ newValue: change.newValue,
85
+ });
86
+ }
87
+ else if (change.action === "update") {
88
+ lines.push(chalk.yellow(` ~ ${change.property}: ${formatValue(change.oldValue)} → ${formatValue(change.newValue)}`));
89
+ entries.push({
90
+ property: change.property,
91
+ action: "update",
92
+ oldValue: change.oldValue,
93
+ newValue: change.newValue,
94
+ });
95
+ }
96
+ }
97
+ return { lines, entries, creates, updates };
98
+ }
78
99
  /**
79
100
  * Build a standardized dry-run result for settings processors.
80
101
  * Returns an intersection of BaseProcessorResult with the extra fields,
@@ -44,8 +44,8 @@ export function diffCodeScanning(current, desired) {
44
44
  }
45
45
  // languages: only diff if specified in desired (sorted comparison)
46
46
  if (desired.languages !== undefined) {
47
- const currentLangs = [...(current.languages ?? [])].sort();
48
- const desiredLangs = [...desired.languages].sort();
47
+ const currentLangs = [...(current.languages ?? [])].sort((a, b) => a.localeCompare(b));
48
+ const desiredLangs = [...desired.languages].sort((a, b) => a.localeCompare(b));
49
49
  const langsMatch = currentLangs.length === desiredLangs.length &&
50
50
  currentLangs.every((lang, i) => lang === desiredLangs[i]);
51
51
  if (!langsMatch) {
@@ -1,10 +1,6 @@
1
1
  import type { CodeScanningChange } from "./diff.js";
2
- export interface CodeScanningPlanEntry {
3
- property: string;
4
- action: "create" | "update";
5
- oldValue?: unknown;
6
- newValue?: unknown;
7
- }
2
+ import { type PlanEntry } from "../base-processor.js";
3
+ export type CodeScanningPlanEntry = PlanEntry;
8
4
  export interface CodeScanningPlanResult {
9
5
  lines: string[];
10
6
  creates: number;
@@ -1,6 +1,5 @@
1
- import chalk from "chalk";
2
1
  import { formatScalarValue } from "../../shared/string-utils.js";
3
- import { countActions } from "../base-processor.js";
2
+ import { formatChangeLines } from "../base-processor.js";
4
3
  function formatValue(val) {
5
4
  if (Array.isArray(val)) {
6
5
  return `[${val.join(", ")}]`;
@@ -11,27 +10,5 @@ function formatValue(val) {
11
10
  * Formats code scanning changes as Terraform-style plan output.
12
11
  */
13
12
  export function formatCodeScanningPlan(changes) {
14
- const lines = [];
15
- const entries = [];
16
- const { create: creates, update: updates } = countActions(changes);
17
- for (const change of changes) {
18
- if (change.action === "create") {
19
- lines.push(chalk.green(` + ${change.property}: ${formatValue(change.newValue)}`));
20
- entries.push({
21
- property: change.property,
22
- action: "create",
23
- newValue: change.newValue,
24
- });
25
- }
26
- else if (change.action === "update") {
27
- lines.push(chalk.yellow(` ~ ${change.property}: ${formatValue(change.oldValue)} → ${formatValue(change.newValue)}`));
28
- entries.push({
29
- property: change.property,
30
- action: "update",
31
- oldValue: change.oldValue,
32
- newValue: change.newValue,
33
- });
34
- }
35
- }
36
- return { lines, creates, updates, entries };
13
+ return formatChangeLines(changes, formatValue);
37
14
  }
@@ -1,7 +1,7 @@
1
1
  import type { ICommandExecutor } from "../../shared/command-executor.js";
2
2
  import { type RepoInfo } from "../../repo/index.js";
3
3
  import { type GhApiOptions } from "../../shared/gh-api-utils.js";
4
- import type { ICodeScanningStrategy, CurrentCodeScanningSettings } from "./types.js";
4
+ import type { ICodeScanningStrategy, CurrentCodeScanningSettings, CodeScanningUpdateParams } from "./types.js";
5
5
  interface GitHubCodeScanningStrategyOptions {
6
6
  retries?: number;
7
7
  cwd: string;
@@ -9,11 +9,7 @@ interface GitHubCodeScanningStrategyOptions {
9
9
  export declare class GitHubCodeScanningStrategy implements ICodeScanningStrategy {
10
10
  private api;
11
11
  constructor(executor: ICommandExecutor, options: GitHubCodeScanningStrategyOptions);
12
- getDefaultSetup(repoInfo: RepoInfo, options?: GhApiOptions): Promise<CurrentCodeScanningSettings>;
13
- updateDefaultSetup(repoInfo: RepoInfo, settings: {
14
- state: string;
15
- query_suite?: string;
16
- languages?: string[];
17
- }, options?: GhApiOptions): Promise<void>;
12
+ get(repoInfo: RepoInfo, options?: GhApiOptions): Promise<CurrentCodeScanningSettings>;
13
+ update(repoInfo: RepoInfo, settings: CodeScanningUpdateParams, options?: GhApiOptions): Promise<void>;
18
14
  }
19
15
  export {};
@@ -6,13 +6,13 @@ export class GitHubCodeScanningStrategy {
6
6
  constructor(executor, options) {
7
7
  this.api = new GhApiClient(executor, options.retries ?? 3, options.cwd);
8
8
  }
9
- async getDefaultSetup(repoInfo, options) {
9
+ async get(repoInfo, options) {
10
10
  assertGitHubRepo(repoInfo, "GitHub Code Scanning strategy");
11
11
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/code-scanning/default-setup`;
12
12
  const result = await this.api.call("GET", endpoint, { options });
13
13
  return parseApiJson(result, "code scanning default setup response");
14
14
  }
15
- async updateDefaultSetup(repoInfo, settings, options) {
15
+ async update(repoInfo, settings, options) {
16
16
  assertGitHubRepo(repoInfo, "GitHub Code Scanning strategy");
17
17
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/code-scanning/default-setup`;
18
18
  await this.api.call("PATCH", endpoint, { payload: settings, options });