@aspruyt/xfg 6.2.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.
- package/dist/cli/program.js +41 -3
- package/dist/cli/secrets-command.d.ts +25 -0
- package/dist/cli/secrets-command.js +75 -0
- package/dist/cli/settings-factories.d.ts +2 -1
- package/dist/cli/settings-factories.js +6 -1
- package/dist/cli/settings-report-builder.d.ts +6 -1
- package/dist/cli/settings-report-builder.js +21 -2
- package/dist/cli/settings-runner.js +7 -0
- package/dist/cli/types.d.ts +4 -2
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.js +1 -1
- package/dist/config/normalizer.js +86 -1
- package/dist/config/types.d.ts +20 -0
- package/dist/config/validator.d.ts +4 -0
- package/dist/config/validator.js +174 -5
- package/dist/output/settings-report.d.ts +11 -0
- package/dist/output/settings-report.js +24 -0
- package/dist/secrets/encryption.d.ts +9 -0
- package/dist/secrets/encryption.js +29 -0
- package/dist/secrets/github-secrets-strategy.d.ts +17 -0
- package/dist/secrets/github-secrets-strategy.js +38 -0
- package/dist/secrets/index.d.ts +5 -0
- package/dist/secrets/index.js +3 -0
- package/dist/secrets/processor.d.ts +31 -0
- package/dist/secrets/processor.js +115 -0
- package/dist/secrets/types.d.ts +21 -0
- package/dist/secrets/types.js +1 -0
- package/dist/settings/index.d.ts +1 -0
- package/dist/settings/index.js +2 -0
- package/dist/settings/variables/diff.d.ts +10 -0
- package/dist/settings/variables/diff.js +39 -0
- package/dist/settings/variables/formatter.d.ts +16 -0
- package/dist/settings/variables/formatter.js +70 -0
- package/dist/settings/variables/github-variables-strategy.d.ts +17 -0
- package/dist/settings/variables/github-variables-strategy.js +40 -0
- package/dist/settings/variables/index.d.ts +4 -0
- package/dist/settings/variables/index.js +2 -0
- package/dist/settings/variables/processor.d.ts +19 -0
- package/dist/settings/variables/processor.js +60 -0
- package/dist/settings/variables/types.d.ts +18 -0
- package/dist/settings/variables/types.js +1 -0
- package/dist/shared/env-resolver.d.ts +16 -0
- package/dist/shared/env-resolver.js +33 -0
- package/package.json +3 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { countActions } from "../base-processor.js";
|
|
3
|
+
export function formatVariablesPlan(changes) {
|
|
4
|
+
const lines = [];
|
|
5
|
+
const entries = [];
|
|
6
|
+
const { create: creates, update: updates, delete: deletes, unchanged, } = countActions(changes);
|
|
7
|
+
const grouped = {
|
|
8
|
+
create: [],
|
|
9
|
+
update: [],
|
|
10
|
+
delete: [],
|
|
11
|
+
unchanged: [],
|
|
12
|
+
};
|
|
13
|
+
for (const c of changes) {
|
|
14
|
+
grouped[c.action].push(c);
|
|
15
|
+
}
|
|
16
|
+
if (grouped.create.length > 0) {
|
|
17
|
+
lines.push(chalk.bold(" Create:"));
|
|
18
|
+
for (const change of grouped.create) {
|
|
19
|
+
lines.push(chalk.green(` + variable "${change.name}"`));
|
|
20
|
+
if (change.newValue !== undefined) {
|
|
21
|
+
lines.push(chalk.green(` value: "${change.newValue}"`));
|
|
22
|
+
}
|
|
23
|
+
entries.push({
|
|
24
|
+
name: change.name,
|
|
25
|
+
action: "create",
|
|
26
|
+
newValue: change.newValue,
|
|
27
|
+
});
|
|
28
|
+
lines.push("");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (grouped.update.length > 0) {
|
|
32
|
+
lines.push(chalk.bold(" Update:"));
|
|
33
|
+
for (const change of grouped.update) {
|
|
34
|
+
lines.push(chalk.yellow(` ~ variable "${change.name}"`));
|
|
35
|
+
if (change.oldValue !== undefined && change.newValue !== undefined) {
|
|
36
|
+
lines.push(chalk.yellow(` value: "${change.oldValue}" → "${change.newValue}"`));
|
|
37
|
+
}
|
|
38
|
+
entries.push({
|
|
39
|
+
name: change.name,
|
|
40
|
+
action: "update",
|
|
41
|
+
oldValue: change.oldValue,
|
|
42
|
+
newValue: change.newValue,
|
|
43
|
+
});
|
|
44
|
+
lines.push("");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (grouped.delete.length > 0) {
|
|
48
|
+
lines.push(chalk.bold(" Delete:"));
|
|
49
|
+
for (const change of grouped.delete) {
|
|
50
|
+
lines.push(chalk.red(` - variable "${change.name}"`));
|
|
51
|
+
entries.push({ name: change.name, action: "delete" });
|
|
52
|
+
}
|
|
53
|
+
lines.push("");
|
|
54
|
+
}
|
|
55
|
+
for (const change of grouped.unchanged) {
|
|
56
|
+
entries.push({ name: change.name, action: "unchanged" });
|
|
57
|
+
}
|
|
58
|
+
const total = creates + updates + deletes;
|
|
59
|
+
if (total > 0) {
|
|
60
|
+
const parts = [];
|
|
61
|
+
if (creates > 0)
|
|
62
|
+
parts.push(`${creates} to create`);
|
|
63
|
+
if (updates > 0)
|
|
64
|
+
parts.push(`${updates} to update`);
|
|
65
|
+
if (deletes > 0)
|
|
66
|
+
parts.push(`${deletes} to delete`);
|
|
67
|
+
lines.push(` Plan: ${total} variables (${parts.join(", ")})`);
|
|
68
|
+
}
|
|
69
|
+
return { lines, creates, updates, deletes, unchanged, entries };
|
|
70
|
+
}
|
|
@@ -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 { IVariablesStrategy, GitHubVariable } from "./types.js";
|
|
5
|
+
interface GitHubVariablesStrategyOptions {
|
|
6
|
+
retries?: number;
|
|
7
|
+
cwd: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class GitHubVariablesStrategy implements IVariablesStrategy {
|
|
10
|
+
private api;
|
|
11
|
+
constructor(executor: ICommandExecutor, options: GitHubVariablesStrategyOptions);
|
|
12
|
+
list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubVariable[]>;
|
|
13
|
+
create(repoInfo: RepoInfo, name: string, value: string, options?: GhApiOptions): Promise<void>;
|
|
14
|
+
update(repoInfo: RepoInfo, name: string, value: string, options?: GhApiOptions): Promise<void>;
|
|
15
|
+
delete(repoInfo: RepoInfo, name: string, options?: GhApiOptions): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
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 GitHubVariablesStrategy {
|
|
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 Variables strategy");
|
|
11
|
+
const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/variables`;
|
|
12
|
+
const result = await this.api.call("GET", endpoint, {
|
|
13
|
+
options,
|
|
14
|
+
paginate: true,
|
|
15
|
+
});
|
|
16
|
+
const response = parseApiJson(result, "variables response");
|
|
17
|
+
return response.variables ?? [];
|
|
18
|
+
}
|
|
19
|
+
async create(repoInfo, name, value, options) {
|
|
20
|
+
assertGitHubRepo(repoInfo, "GitHub Variables strategy");
|
|
21
|
+
const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/variables`;
|
|
22
|
+
await this.api.call("POST", endpoint, {
|
|
23
|
+
payload: { name, value },
|
|
24
|
+
options,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async update(repoInfo, name, value, options) {
|
|
28
|
+
assertGitHubRepo(repoInfo, "GitHub Variables strategy");
|
|
29
|
+
const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/variables/${encodeURIComponent(name)}`;
|
|
30
|
+
await this.api.call("PATCH", endpoint, {
|
|
31
|
+
payload: { value },
|
|
32
|
+
options,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async delete(repoInfo, name, options) {
|
|
36
|
+
assertGitHubRepo(repoInfo, "GitHub Variables strategy");
|
|
37
|
+
const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/variables/${encodeURIComponent(name)}`;
|
|
38
|
+
await this.api.call("DELETE", endpoint, { options });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { type VariableChange, type VariableAction } from "./diff.js";
|
|
2
|
+
export { type VariablesPlanEntry } from "./formatter.js";
|
|
3
|
+
export { VariablesProcessor, type IVariablesProcessor } from "./processor.js";
|
|
4
|
+
export { GitHubVariablesStrategy } from "./github-variables-strategy.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RepoConfig } from "../../config/index.js";
|
|
2
|
+
import type { RepoInfo } from "../../repo/index.js";
|
|
3
|
+
import { type VariablesPlanResult } from "./formatter.js";
|
|
4
|
+
import type { IVariablesStrategy } from "./types.js";
|
|
5
|
+
import { type BaseProcessorOptions, type BaseProcessorResult, type ISettingsProcessor, type ChangeCounts } from "../base-processor.js";
|
|
6
|
+
export type IVariablesProcessor = ISettingsProcessor<VariablesProcessorOptions, VariablesProcessorResult>;
|
|
7
|
+
export interface VariablesProcessorOptions extends BaseProcessorOptions {
|
|
8
|
+
noDelete?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface VariablesProcessorResult extends BaseProcessorResult {
|
|
11
|
+
changes?: ChangeCounts;
|
|
12
|
+
planOutput?: VariablesPlanResult;
|
|
13
|
+
}
|
|
14
|
+
export declare class VariablesProcessor implements IVariablesProcessor {
|
|
15
|
+
private readonly strategy;
|
|
16
|
+
constructor(strategy: IVariablesStrategy);
|
|
17
|
+
process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: VariablesProcessorOptions): Promise<VariablesProcessorResult>;
|
|
18
|
+
private applySettings;
|
|
19
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { diffVariables } from "./diff.js";
|
|
2
|
+
import { formatVariablesPlan } from "./formatter.js";
|
|
3
|
+
import { withGitHubGuards, countActions, buildDryRunResult, buildApplyResult, } from "../base-processor.js";
|
|
4
|
+
export class VariablesProcessor {
|
|
5
|
+
strategy;
|
|
6
|
+
constructor(strategy) {
|
|
7
|
+
this.strategy = strategy;
|
|
8
|
+
}
|
|
9
|
+
async process(repoConfig, repoInfo, options) {
|
|
10
|
+
return withGitHubGuards(repoConfig, repoInfo, options, {
|
|
11
|
+
hasDesiredSettings: (rc) => {
|
|
12
|
+
const vars = rc.settings?.variables ?? {};
|
|
13
|
+
const { deleteOrphaned, ...entries } = vars;
|
|
14
|
+
return Object.keys(entries).length > 0 || deleteOrphaned === true;
|
|
15
|
+
},
|
|
16
|
+
emptySettingsMessage: "No variables configured",
|
|
17
|
+
applySettings: (githubRepo, rc, opts, token, repoName) => this.applySettings(githubRepo, rc, opts, token, repoName),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async applySettings(githubRepo, repoConfig, options, effectiveToken, repoName) {
|
|
21
|
+
const { dryRun, noDelete } = options;
|
|
22
|
+
const settings = repoConfig.settings;
|
|
23
|
+
const { deleteOrphaned: varDeleteOrphaned = false, ...desiredVariables } = (settings?.variables ?? {});
|
|
24
|
+
const deleteOrphaned = varDeleteOrphaned && !(noDelete ?? false);
|
|
25
|
+
const strategyOptions = { token: effectiveToken, host: githubRepo.host };
|
|
26
|
+
const currentVariables = await this.strategy.list(githubRepo, strategyOptions);
|
|
27
|
+
const changes = diffVariables(currentVariables, desiredVariables, deleteOrphaned);
|
|
28
|
+
const changeCounts = countActions(changes);
|
|
29
|
+
const planOutput = formatVariablesPlan(changes);
|
|
30
|
+
if (dryRun) {
|
|
31
|
+
return buildDryRunResult(repoName, changeCounts, { planOutput });
|
|
32
|
+
}
|
|
33
|
+
let appliedCount = 0;
|
|
34
|
+
for (const change of changes) {
|
|
35
|
+
switch (change.action) {
|
|
36
|
+
case "create":
|
|
37
|
+
if (change.newValue !== undefined) {
|
|
38
|
+
await this.strategy.create(githubRepo, change.name, change.newValue, strategyOptions);
|
|
39
|
+
appliedCount++;
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
case "update":
|
|
43
|
+
if (change.newValue !== undefined) {
|
|
44
|
+
await this.strategy.update(githubRepo, change.name, change.newValue, strategyOptions);
|
|
45
|
+
appliedCount++;
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case "delete":
|
|
49
|
+
await this.strategy.delete(githubRepo, change.name, strategyOptions);
|
|
50
|
+
appliedCount++;
|
|
51
|
+
break;
|
|
52
|
+
case "unchanged":
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return buildApplyResult(repoName, changeCounts, appliedCount, {
|
|
57
|
+
planOutput,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RepoInfo } from "../../repo/index.js";
|
|
2
|
+
import type { GhApiOptions } from "../../shared/gh-api-utils.js";
|
|
3
|
+
export interface GitHubVariable {
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
created_at: string;
|
|
7
|
+
updated_at: string;
|
|
8
|
+
}
|
|
9
|
+
export interface GitHubVariablesListResponse {
|
|
10
|
+
total_count: number;
|
|
11
|
+
variables: GitHubVariable[];
|
|
12
|
+
}
|
|
13
|
+
export interface IVariablesStrategy {
|
|
14
|
+
list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubVariable[]>;
|
|
15
|
+
create(repoInfo: RepoInfo, name: string, value: string, options?: GhApiOptions): Promise<void>;
|
|
16
|
+
update(repoInfo: RepoInfo, name: string, value: string, options?: GhApiOptions): Promise<void>;
|
|
17
|
+
delete(repoInfo: RepoInfo, name: string, options?: GhApiOptions): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface IEnvResolver {
|
|
2
|
+
resolve(envName: string): string;
|
|
3
|
+
resolveAll(entries: {
|
|
4
|
+
name: string;
|
|
5
|
+
envVar: string;
|
|
6
|
+
}[]): Map<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export declare class EnvResolver implements IEnvResolver {
|
|
9
|
+
private readonly env;
|
|
10
|
+
constructor(env: Record<string, string | undefined>);
|
|
11
|
+
resolve(envName: string): string;
|
|
12
|
+
resolveAll(entries: {
|
|
13
|
+
name: string;
|
|
14
|
+
envVar: string;
|
|
15
|
+
}[]): Map<string, string>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export class EnvResolver {
|
|
2
|
+
env;
|
|
3
|
+
constructor(env) {
|
|
4
|
+
this.env = env;
|
|
5
|
+
}
|
|
6
|
+
resolve(envName) {
|
|
7
|
+
const value = this.env[envName];
|
|
8
|
+
if (value === undefined) {
|
|
9
|
+
throw new Error(`Environment variable '${envName}' is not set.`);
|
|
10
|
+
}
|
|
11
|
+
if (value === "") {
|
|
12
|
+
throw new Error(`Environment variable '${envName}' is empty.`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
resolveAll(entries) {
|
|
17
|
+
const missing = new Set();
|
|
18
|
+
const result = new Map();
|
|
19
|
+
for (const { name, envVar } of entries) {
|
|
20
|
+
const value = this.env[envVar];
|
|
21
|
+
if (value === undefined || value === "") {
|
|
22
|
+
missing.add(envVar);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
result.set(name, value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (missing.size > 0) {
|
|
29
|
+
throw new Error(`Missing environment variables: ${[...missing].join(", ")}`);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aspruyt/xfg",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"description": "Manage files, settings, and repositories across GitHub, Azure DevOps, and GitLab — declaratively, from a single YAML config",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -74,10 +74,12 @@
|
|
|
74
74
|
"chalk": "^5.3.0",
|
|
75
75
|
"commander": "^14.0.2",
|
|
76
76
|
"json5": "^2.2.3",
|
|
77
|
+
"libsodium-wrappers": "^0.8.4",
|
|
77
78
|
"p-retry": "^8.0.0",
|
|
78
79
|
"yaml": "^2.4.5"
|
|
79
80
|
},
|
|
80
81
|
"devDependencies": {
|
|
82
|
+
"@types/libsodium-wrappers": "^0.8.0",
|
|
81
83
|
"@types/node": "^24.12.2",
|
|
82
84
|
"bottleneck": "^2.19.5",
|
|
83
85
|
"c8": "^11.0.0",
|