@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
package/dist/cli/program.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { program, Command } from "commander";
|
|
1
|
+
import { program, Command, InvalidArgumentError } from "commander";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { ValidationError } from "../shared/errors.js";
|
|
6
6
|
import { runSync } from "./sync-command.js";
|
|
7
|
+
import { runSecretsSync } from "./secrets-command.js";
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = dirname(__filename);
|
|
9
10
|
function getVersion() {
|
|
@@ -26,7 +27,12 @@ function addSharedOptions(cmd) {
|
|
|
26
27
|
.requiredOption("-c, --config <path>", "Path to YAML config file")
|
|
27
28
|
.option("-d, --dry-run", "Show what would be done without making changes")
|
|
28
29
|
.option("-w, --work-dir <path>", "Temporary directory for cloning", "./tmp")
|
|
29
|
-
.option("-r, --retries <number>", "Number of retries for network operations (0 to disable)", (v) =>
|
|
30
|
+
.option("-r, --retries <number>", "Number of retries for network operations (0 to disable)", (v) => {
|
|
31
|
+
const n = parseInt(v, 10);
|
|
32
|
+
if (isNaN(n) || n < 0)
|
|
33
|
+
throw new InvalidArgumentError("Must be a non-negative number.");
|
|
34
|
+
return n;
|
|
35
|
+
}, 3)
|
|
30
36
|
.option("--no-delete", "Skip deletion of orphaned resources even if deleteOrphaned is configured");
|
|
31
37
|
}
|
|
32
38
|
// =============================================================================
|
|
@@ -61,7 +67,11 @@ const syncCommand = new Command("sync")
|
|
|
61
67
|
.option("--delete-branch", "Delete source branch after merge")
|
|
62
68
|
.action(async (opts) => {
|
|
63
69
|
try {
|
|
64
|
-
|
|
70
|
+
const options = {
|
|
71
|
+
...opts,
|
|
72
|
+
noDelete: opts.delete === false,
|
|
73
|
+
};
|
|
74
|
+
await runSync(options);
|
|
65
75
|
}
|
|
66
76
|
catch (error) {
|
|
67
77
|
console.error("Fatal error:", error);
|
|
@@ -70,4 +80,32 @@ const syncCommand = new Command("sync")
|
|
|
70
80
|
});
|
|
71
81
|
addSharedOptions(syncCommand);
|
|
72
82
|
program.addCommand(syncCommand);
|
|
83
|
+
const secretsCommand = new Command("secrets").description("Manage repository secrets");
|
|
84
|
+
const secretsSyncCommand = new Command("sync")
|
|
85
|
+
.description("Sync secrets to target repositories")
|
|
86
|
+
.requiredOption("-c, --config <path>", "Path to xfg config file")
|
|
87
|
+
.option("-d, --dry-run", "Show what would be done without making changes")
|
|
88
|
+
.option("--no-delete", "Skip deletion of orphaned secrets")
|
|
89
|
+
.option("-w, --work-dir <path>", "Temporary directory for cloning", "./tmp")
|
|
90
|
+
.option("-r, --retries <number>", "Number of retries for network operations (0 to disable)", (value) => {
|
|
91
|
+
const n = parseInt(value, 10);
|
|
92
|
+
if (isNaN(n) || n < 0)
|
|
93
|
+
throw new InvalidArgumentError("Must be a non-negative number.");
|
|
94
|
+
return n;
|
|
95
|
+
}, 3)
|
|
96
|
+
.action(async (opts) => {
|
|
97
|
+
try {
|
|
98
|
+
const options = {
|
|
99
|
+
...opts,
|
|
100
|
+
noDelete: opts.delete === false,
|
|
101
|
+
};
|
|
102
|
+
await runSecretsSync(options);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error("Fatal error:", error);
|
|
106
|
+
return process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
secretsCommand.addCommand(secretsSyncCommand);
|
|
110
|
+
program.addCommand(secretsCommand);
|
|
73
111
|
export { program, getVersion };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SecretsProcessorResult } from "../secrets/processor.js";
|
|
2
|
+
import type { SecretConfig, Config } from "../config/index.js";
|
|
3
|
+
import type { RepoInfo } from "../repo/index.js";
|
|
4
|
+
type SecretsConfig = Record<string, SecretConfig | boolean> & {
|
|
5
|
+
deleteOrphaned?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export interface ISecretsProcessorAdapter {
|
|
8
|
+
process(secretsConfig: SecretsConfig, repoInfo: RepoInfo, options: {
|
|
9
|
+
dryRun?: boolean;
|
|
10
|
+
token?: string;
|
|
11
|
+
noDelete?: boolean;
|
|
12
|
+
}): Promise<SecretsProcessorResult>;
|
|
13
|
+
}
|
|
14
|
+
export interface SecretsSyncDependencies {
|
|
15
|
+
processorFactory?: (config: Config, cwd: string, retries: number) => ISecretsProcessorAdapter;
|
|
16
|
+
}
|
|
17
|
+
export interface SecretsSyncOptions {
|
|
18
|
+
config: string;
|
|
19
|
+
dryRun?: boolean;
|
|
20
|
+
noDelete?: boolean;
|
|
21
|
+
workDir?: string;
|
|
22
|
+
retries?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function runSecretsSync(options: SecretsSyncOptions, deps?: SecretsSyncDependencies): Promise<void>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { loadRawConfig } from "../config/index.js";
|
|
2
|
+
import { normalizeConfig } from "../config/normalizer.js";
|
|
3
|
+
import { validateRawConfig, validateSecretsConfig, validateVariableSecretOverlaps, } from "../config/validator.js";
|
|
4
|
+
import { SecretsProcessor, GitHubSecretsStrategy, SodiumEncryptor, } from "../secrets/index.js";
|
|
5
|
+
import { EnvResolver } from "../shared/env-resolver.js";
|
|
6
|
+
import { ProcessExecutor } from "../shared/command-executor.js";
|
|
7
|
+
import { parseGitUrl } from "../repo/index.js";
|
|
8
|
+
import { Logger } from "../shared/logger.js";
|
|
9
|
+
import { toErrorMessage } from "../shared/type-guards.js";
|
|
10
|
+
function createDefaultProcessor(_config, cwd, retries) {
|
|
11
|
+
const executor = new ProcessExecutor(process.env);
|
|
12
|
+
const encryptor = new SodiumEncryptor();
|
|
13
|
+
const envResolver = new EnvResolver(process.env);
|
|
14
|
+
const strategy = new GitHubSecretsStrategy(executor, {
|
|
15
|
+
cwd,
|
|
16
|
+
retries,
|
|
17
|
+
});
|
|
18
|
+
return new SecretsProcessor(strategy, encryptor, envResolver);
|
|
19
|
+
}
|
|
20
|
+
export async function runSecretsSync(options, deps = {}) {
|
|
21
|
+
const logger = new Logger(!!(process.env.DEBUG || process.env.XFG_DEBUG));
|
|
22
|
+
const { config: configPath, dryRun, workDir, retries, noDelete } = options;
|
|
23
|
+
const cwd = workDir ?? "./tmp";
|
|
24
|
+
const rawConfig = loadRawConfig(configPath);
|
|
25
|
+
validateRawConfig(rawConfig);
|
|
26
|
+
validateSecretsConfig(rawConfig);
|
|
27
|
+
validateVariableSecretOverlaps(rawConfig);
|
|
28
|
+
const config = normalizeConfig(rawConfig, process.env);
|
|
29
|
+
if (!config.secrets) {
|
|
30
|
+
logger.info("No secrets configured. Nothing to do.");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const { deleteOrphaned, ...secretEntries } = config.secrets;
|
|
34
|
+
const secretNames = Object.keys(secretEntries).filter((k) => typeof secretEntries[k] !== "boolean");
|
|
35
|
+
if (secretNames.length === 0 && !deleteOrphaned) {
|
|
36
|
+
logger.info("No secrets configured. Nothing to do.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const processorFactory = deps.processorFactory ?? createDefaultProcessor;
|
|
40
|
+
const processor = processorFactory(config, cwd, retries ?? 3);
|
|
41
|
+
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
42
|
+
let hasErrors = false;
|
|
43
|
+
logger.setTotal(config.repos.length);
|
|
44
|
+
for (let i = 0; i < config.repos.length; i++) {
|
|
45
|
+
const repoConfig = config.repos[i];
|
|
46
|
+
const repoName = repoConfig.git;
|
|
47
|
+
try {
|
|
48
|
+
const repoInfo = parseGitUrl(repoConfig.git, {
|
|
49
|
+
githubHosts: config.githubHosts,
|
|
50
|
+
});
|
|
51
|
+
const result = await processor.process(config.secrets, repoInfo, {
|
|
52
|
+
dryRun,
|
|
53
|
+
token,
|
|
54
|
+
noDelete,
|
|
55
|
+
});
|
|
56
|
+
if (result.skipped) {
|
|
57
|
+
logger.skip(i + 1, repoName, result.message);
|
|
58
|
+
}
|
|
59
|
+
else if (result.success) {
|
|
60
|
+
logger.success(i + 1, repoName, `Secrets: ${result.message}`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
logger.error(i + 1, repoName, `Secrets: ${result.message}`);
|
|
64
|
+
hasErrors = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
logger.error(i + 1, repoName, `Secrets: ${toErrorMessage(error)}`);
|
|
69
|
+
hasErrors = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (hasErrors) {
|
|
73
|
+
throw new Error("One or more repositories failed secrets sync.");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ProcessExecutor } from "../shared/command-executor.js";
|
|
2
|
-
import type { RulesetProcessorFactory, RepoSettingsProcessorFactory, LabelsProcessorFactory, CodeScanningProcessorFactory, SettingsProcessorFactories } from "./types.js";
|
|
2
|
+
import type { RulesetProcessorFactory, RepoSettingsProcessorFactory, LabelsProcessorFactory, CodeScanningProcessorFactory, VariablesProcessorFactory, SettingsProcessorFactories } from "./types.js";
|
|
3
3
|
export declare function createDefaultRulesetProcessorFactory(executor: ProcessExecutor): RulesetProcessorFactory;
|
|
4
4
|
export declare function createDefaultRepoSettingsProcessorFactory(executor: ProcessExecutor): RepoSettingsProcessorFactory;
|
|
5
5
|
export declare function createDefaultLabelsProcessorFactory(executor: ProcessExecutor): LabelsProcessorFactory;
|
|
6
6
|
export declare function createDefaultCodeScanningProcessorFactory(executor: ProcessExecutor): CodeScanningProcessorFactory;
|
|
7
|
+
export declare function createDefaultVariablesProcessorFactory(executor: ProcessExecutor): VariablesProcessorFactory;
|
|
7
8
|
export declare function createDefaultFactories(executor: ProcessExecutor, overrides?: Partial<SettingsProcessorFactories>): SettingsProcessorFactories;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RulesetProcessor, RepoSettingsProcessor, LabelsProcessor, CodeScanningProcessor, GitHubRulesetStrategy, GitHubRepoSettingsStrategy, GitHubLabelsStrategy, GitHubCodeScanningStrategy, } from "../settings/index.js";
|
|
1
|
+
import { RulesetProcessor, RepoSettingsProcessor, LabelsProcessor, CodeScanningProcessor, VariablesProcessor, GitHubRulesetStrategy, GitHubRepoSettingsStrategy, GitHubLabelsStrategy, GitHubCodeScanningStrategy, GitHubVariablesStrategy, } from "../settings/index.js";
|
|
2
2
|
import { GitHubRepoMetadataProvider } from "../repo/index.js";
|
|
3
3
|
export function createDefaultRulesetProcessorFactory(executor) {
|
|
4
4
|
const cwd = process.cwd();
|
|
@@ -16,6 +16,10 @@ export function createDefaultCodeScanningProcessorFactory(executor) {
|
|
|
16
16
|
const cwd = process.cwd();
|
|
17
17
|
return () => new CodeScanningProcessor(new GitHubCodeScanningStrategy(executor, { cwd }), new GitHubRepoMetadataProvider(executor, { cwd }));
|
|
18
18
|
}
|
|
19
|
+
export function createDefaultVariablesProcessorFactory(executor) {
|
|
20
|
+
const cwd = process.cwd();
|
|
21
|
+
return () => new VariablesProcessor(new GitHubVariablesStrategy(executor, { cwd }));
|
|
22
|
+
}
|
|
19
23
|
export function createDefaultFactories(executor, overrides) {
|
|
20
24
|
return {
|
|
21
25
|
rulesets: overrides?.rulesets ?? createDefaultRulesetProcessorFactory(executor),
|
|
@@ -23,5 +27,6 @@ export function createDefaultFactories(executor, overrides) {
|
|
|
23
27
|
repo: overrides?.repo ?? createDefaultRepoSettingsProcessorFactory(executor),
|
|
24
28
|
codeScanning: overrides?.codeScanning ??
|
|
25
29
|
createDefaultCodeScanningProcessorFactory(executor),
|
|
30
|
+
variables: overrides?.variables ?? createDefaultVariablesProcessorFactory(executor),
|
|
26
31
|
};
|
|
27
32
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SettingsReport } from "../output/index.js";
|
|
2
|
-
import { type RepoSettingsPlanEntry, type RulesetPlanEntry, type LabelsPlanEntry, type CodeScanningPlanEntry } from "../settings/index.js";
|
|
2
|
+
import { type RepoSettingsPlanEntry, type RulesetPlanEntry, type LabelsPlanEntry, type CodeScanningPlanEntry, type VariablesPlanEntry } from "../settings/index.js";
|
|
3
3
|
/**
|
|
4
4
|
* Result from processing a repository's settings and rulesets.
|
|
5
5
|
* Used to collect results during settings command execution.
|
|
@@ -26,6 +26,11 @@ export interface ProcessorResults {
|
|
|
26
26
|
entries?: CodeScanningPlanEntry[];
|
|
27
27
|
};
|
|
28
28
|
};
|
|
29
|
+
variablesResult?: {
|
|
30
|
+
planOutput?: {
|
|
31
|
+
entries?: VariablesPlanEntry[];
|
|
32
|
+
};
|
|
33
|
+
};
|
|
29
34
|
error?: string;
|
|
30
35
|
}
|
|
31
36
|
export declare function buildSettingsReport(results: ProcessorResults[]): SettingsReport;
|
|
@@ -5,6 +5,7 @@ export function buildSettingsReport(results) {
|
|
|
5
5
|
settings: { create: 0, update: 0 },
|
|
6
6
|
rulesets: { create: 0, update: 0, delete: 0 },
|
|
7
7
|
labels: { create: 0, update: 0, delete: 0 },
|
|
8
|
+
variables: { create: 0, update: 0, delete: 0 },
|
|
8
9
|
};
|
|
9
10
|
for (const result of results) {
|
|
10
11
|
const repoChanges = {
|
|
@@ -12,12 +13,12 @@ export function buildSettingsReport(results) {
|
|
|
12
13
|
settings: [],
|
|
13
14
|
rulesets: [],
|
|
14
15
|
labels: [],
|
|
16
|
+
variables: [],
|
|
15
17
|
};
|
|
16
18
|
if (result.settingsResult?.planOutput?.entries) {
|
|
17
19
|
for (const entry of result.settingsResult.planOutput.entries) {
|
|
18
|
-
if (entry
|
|
20
|
+
if (!isActiveAction(entry))
|
|
19
21
|
continue;
|
|
20
|
-
}
|
|
21
22
|
repoChanges.settings.push({
|
|
22
23
|
name: entry.property,
|
|
23
24
|
action: entry.action,
|
|
@@ -28,6 +29,8 @@ export function buildSettingsReport(results) {
|
|
|
28
29
|
}
|
|
29
30
|
if (result.codeScanningResult?.planOutput?.entries) {
|
|
30
31
|
for (const entry of result.codeScanningResult.planOutput.entries) {
|
|
32
|
+
if (!isActiveAction(entry))
|
|
33
|
+
continue;
|
|
31
34
|
repoChanges.settings.push({
|
|
32
35
|
name: `codeScanning.${entry.property}`,
|
|
33
36
|
action: entry.action,
|
|
@@ -74,6 +77,22 @@ export function buildSettingsReport(results) {
|
|
|
74
77
|
totals.labels.update += counts.update;
|
|
75
78
|
totals.labels.delete += counts.delete;
|
|
76
79
|
}
|
|
80
|
+
if (result.variablesResult?.planOutput?.entries) {
|
|
81
|
+
for (const entry of result.variablesResult.planOutput.entries) {
|
|
82
|
+
if (!isActiveAction(entry))
|
|
83
|
+
continue;
|
|
84
|
+
repoChanges.variables.push({
|
|
85
|
+
name: entry.name,
|
|
86
|
+
action: entry.action,
|
|
87
|
+
oldValue: entry.oldValue,
|
|
88
|
+
newValue: entry.newValue,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const counts = countActions(repoChanges.variables);
|
|
92
|
+
totals.variables.create += counts.create;
|
|
93
|
+
totals.variables.update += counts.update;
|
|
94
|
+
totals.variables.delete += counts.delete;
|
|
95
|
+
}
|
|
77
96
|
if (result.error) {
|
|
78
97
|
repoChanges.error = result.error;
|
|
79
98
|
}
|
|
@@ -65,6 +65,13 @@ function buildSettingsDescriptors(ctx) {
|
|
|
65
65
|
e.codeScanningResult = r;
|
|
66
66
|
}),
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
key: "variables",
|
|
70
|
+
label: "Variables",
|
|
71
|
+
run: () => runAndStoreResult(factories.variables, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
|
|
72
|
+
e.variablesResult = r;
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
68
75
|
];
|
|
69
76
|
}
|
|
70
77
|
export async function applyRepoSettings(ctx) {
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MergeMode, MergeStrategy, RepoConfig } from "../config/index.js";
|
|
2
2
|
import type { IRepoLifecycleManager } from "../lifecycle/index.js";
|
|
3
3
|
import type { IRepositoryProcessor, FileChangeDetail } from "../sync/index.js";
|
|
4
|
-
import type { ISettingsProcessor, IRulesetProcessor, IRepoSettingsProcessor, ILabelsProcessor, ICodeScanningProcessor, BaseProcessorResult } from "../settings/index.js";
|
|
4
|
+
import type { ISettingsProcessor, IRulesetProcessor, IRepoSettingsProcessor, ILabelsProcessor, ICodeScanningProcessor, IVariablesProcessor, BaseProcessorResult } from "../settings/index.js";
|
|
5
5
|
import type { RepoInfo } from "../repo/index.js";
|
|
6
6
|
import type { ResultsCollector } from "./results-collector.js";
|
|
7
7
|
import type { Logger } from "../shared/logger.js";
|
|
@@ -11,12 +11,14 @@ export type RulesetProcessorFactory = SettingsProcessorFactory<IRulesetProcessor
|
|
|
11
11
|
export type RepoSettingsProcessorFactory = SettingsProcessorFactory<IRepoSettingsProcessor>;
|
|
12
12
|
export type LabelsProcessorFactory = SettingsProcessorFactory<ILabelsProcessor>;
|
|
13
13
|
export type CodeScanningProcessorFactory = SettingsProcessorFactory<ICodeScanningProcessor>;
|
|
14
|
-
export type
|
|
14
|
+
export type VariablesProcessorFactory = SettingsProcessorFactory<IVariablesProcessor>;
|
|
15
|
+
export type SettingsKind = "rulesets" | "labels" | "repo" | "codeScanning" | "variables";
|
|
15
16
|
export interface SettingsProcessorFactories {
|
|
16
17
|
rulesets: RulesetProcessorFactory;
|
|
17
18
|
labels: LabelsProcessorFactory;
|
|
18
19
|
repo: RepoSettingsProcessorFactory;
|
|
19
20
|
codeScanning: CodeScanningProcessorFactory;
|
|
21
|
+
variables: VariablesProcessorFactory;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Dependencies for the sync command (dependency injection).
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { PRMergeOptions, MergeMode, MergeStrategy, BypassActor, StatusCheckConfig, CodeScanningTool, PullRequestRuleParameters, RulesetRule, Ruleset, GitHubRepoSettings, RepoVisibility, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage, Label, CodeScanningSettings, CodeScanningState, CodeScanningQuerySuite, CodeScanningLanguage, RepoSettings, RawFileConfig, RawRepoFileOverride, RawGroupConfig, RawRepoSettings, RawRepoConfig, RawConfig, RawConditionalGroupWhen, RawConditionalGroupConfig, RepoConfig, Config, FileContent, ContentValue, } from "./types.js";
|
|
1
|
+
export type { PRMergeOptions, MergeMode, MergeStrategy, BypassActor, StatusCheckConfig, CodeScanningTool, PullRequestRuleParameters, RulesetRule, Ruleset, GitHubRepoSettings, RepoVisibility, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage, Label, CodeScanningSettings, CodeScanningState, CodeScanningQuerySuite, CodeScanningLanguage, RepoSettings, RawFileConfig, RawRepoFileOverride, RawGroupConfig, SecretConfig, RawRootSettings, RawRepoSettings, RawRepoConfig, RawConfig, RawConditionalGroupWhen, RawConditionalGroupConfig, RepoConfig, Config, FileContent, ContentValue, } from "./types.js";
|
|
2
2
|
export { RULESET_COMPARABLE_FIELDS } from "./types.js";
|
|
3
3
|
export { loadRawConfig, loadConfig, normalizeConfig } from "./loader.js";
|
|
4
4
|
export { convertContentToString } from "./formatter.js";
|
|
5
|
-
export { validateForSync } from "./validator.js";
|
|
5
|
+
export { validateForSync, validateSecretsConfig } from "./validator.js";
|
package/dist/config/index.js
CHANGED
|
@@ -5,4 +5,4 @@ export { loadRawConfig, loadConfig, normalizeConfig } from "./loader.js";
|
|
|
5
5
|
// Config formatting
|
|
6
6
|
export { convertContentToString } from "./formatter.js";
|
|
7
7
|
// Config validation
|
|
8
|
-
export { validateForSync } from "./validator.js";
|
|
8
|
+
export { validateForSync, validateSecretsConfig } from "./validator.js";
|
|
@@ -127,6 +127,23 @@ function mergeLabels(rootLabels, repoLabels) {
|
|
|
127
127
|
}
|
|
128
128
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
129
129
|
}
|
|
130
|
+
// GitHub treats variable names case-insensitively; overlay keys win over base keys with same name but different casing.
|
|
131
|
+
function mergeVariablesCaseInsensitive(base, overlay) {
|
|
132
|
+
const overlayUpper = new Map();
|
|
133
|
+
for (const key of Object.keys(overlay)) {
|
|
134
|
+
overlayUpper.set(key.toUpperCase(), key);
|
|
135
|
+
}
|
|
136
|
+
const result = {};
|
|
137
|
+
for (const [key, value] of Object.entries(base)) {
|
|
138
|
+
if (!overlayUpper.has(key.toUpperCase())) {
|
|
139
|
+
result[key] = value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
143
|
+
result[key] = value;
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
130
147
|
/**
|
|
131
148
|
* Merges settings: per-repo settings deep merge with root settings.
|
|
132
149
|
* Returns undefined if no settings are defined.
|
|
@@ -165,7 +182,9 @@ export function mergeSettings(root, perRepo) {
|
|
|
165
182
|
}
|
|
166
183
|
}
|
|
167
184
|
// deleteOrphaned: per-repo overrides root
|
|
168
|
-
const deleteOrphaned = perRepo?.deleteOrphaned
|
|
185
|
+
const deleteOrphaned = perRepo?.deleteOrphaned !== undefined
|
|
186
|
+
? perRepo.deleteOrphaned
|
|
187
|
+
: root?.deleteOrphaned;
|
|
169
188
|
if (deleteOrphaned !== undefined) {
|
|
170
189
|
result.deleteOrphaned = deleteOrphaned;
|
|
171
190
|
}
|
|
@@ -209,6 +228,37 @@ export function mergeSettings(root, perRepo) {
|
|
|
209
228
|
}
|
|
210
229
|
}
|
|
211
230
|
}
|
|
231
|
+
// Variables merging — deleteOrphaned is a peer key (like secrets' deleteOrphaned)
|
|
232
|
+
if (root?.variables || perRepo?.variables) {
|
|
233
|
+
const rootVars = root?.variables ?? {};
|
|
234
|
+
const repoVars = perRepo?.variables ?? {};
|
|
235
|
+
const rootDeleteOrphaned = rootVars
|
|
236
|
+
.deleteOrphaned;
|
|
237
|
+
const repoDeleteOrphaned = repoVars
|
|
238
|
+
.deleteOrphaned;
|
|
239
|
+
const effectiveDeleteOrphaned = repoDeleteOrphaned !== undefined
|
|
240
|
+
? repoDeleteOrphaned
|
|
241
|
+
: rootDeleteOrphaned;
|
|
242
|
+
const inherit = repoVars.inherit;
|
|
243
|
+
if (inherit === false) {
|
|
244
|
+
const { inherit: _, deleteOrphaned: _d, ...rest } = repoVars;
|
|
245
|
+
result.variables = Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== false));
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
const combined = mergeVariablesCaseInsensitive(rootVars, repoVars);
|
|
249
|
+
const { inherit: _, deleteOrphaned: _d, ...rest } = combined;
|
|
250
|
+
result.variables = Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== false));
|
|
251
|
+
}
|
|
252
|
+
if (effectiveDeleteOrphaned !== undefined) {
|
|
253
|
+
result.variables.deleteOrphaned =
|
|
254
|
+
effectiveDeleteOrphaned;
|
|
255
|
+
}
|
|
256
|
+
// Only delete if no variable entries remain (deleteOrphaned alone is not actionable)
|
|
257
|
+
const { deleteOrphaned: _check, ...varEntries } = result.variables;
|
|
258
|
+
if (Object.keys(varEntries).length === 0 && !effectiveDeleteOrphaned) {
|
|
259
|
+
delete result.variables;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
212
262
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
213
263
|
}
|
|
214
264
|
/**
|
|
@@ -348,6 +398,40 @@ function mergeRawSettings(base, overlay) {
|
|
|
348
398
|
result.codeScanning = structuredClone(overlay.codeScanning);
|
|
349
399
|
}
|
|
350
400
|
}
|
|
401
|
+
// Variables: simple string values with false opt-outs (mergeNamedEntries won't work for strings)
|
|
402
|
+
if (overlay.variables) {
|
|
403
|
+
const overlayVars = overlay.variables;
|
|
404
|
+
const inherit = overlayVars.inherit !== false;
|
|
405
|
+
const baseVars = inherit
|
|
406
|
+
? { ...(result.variables ?? {}) }
|
|
407
|
+
: {};
|
|
408
|
+
// Build overlay entries (excluding meta keys)
|
|
409
|
+
const overlayEntries = {};
|
|
410
|
+
for (const [name, entry] of Object.entries(overlay.variables)) {
|
|
411
|
+
if (name === "inherit" || name === "deleteOrphaned")
|
|
412
|
+
continue;
|
|
413
|
+
overlayEntries[name] = entry;
|
|
414
|
+
}
|
|
415
|
+
// Case-insensitive merge: overlay keys replace base keys with same name
|
|
416
|
+
const merged = mergeVariablesCaseInsensitive(baseVars, overlayEntries);
|
|
417
|
+
// Apply false opt-outs
|
|
418
|
+
const cleaned = {};
|
|
419
|
+
for (const [name, value] of Object.entries(merged)) {
|
|
420
|
+
if (name === "inherit" || name === "deleteOrphaned")
|
|
421
|
+
continue;
|
|
422
|
+
if (value !== false) {
|
|
423
|
+
cleaned[name] = value;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const baseDelete = baseVars.deleteOrphaned;
|
|
427
|
+
const effectiveDelete = overlayVars.deleteOrphaned !== undefined
|
|
428
|
+
? overlayVars.deleteOrphaned
|
|
429
|
+
: baseDelete;
|
|
430
|
+
if (effectiveDelete !== undefined) {
|
|
431
|
+
cleaned.deleteOrphaned = effectiveDelete;
|
|
432
|
+
}
|
|
433
|
+
result.variables = cleaned;
|
|
434
|
+
}
|
|
351
435
|
// deleteOrphaned: overlay wins
|
|
352
436
|
if (overlay.deleteOrphaned !== undefined) {
|
|
353
437
|
result.deleteOrphaned = overlay.deleteOrphaned;
|
|
@@ -537,5 +621,6 @@ export function normalizeConfig(raw, env) {
|
|
|
537
621
|
githubHosts: raw.githubHosts,
|
|
538
622
|
deleteOrphaned: raw.deleteOrphaned,
|
|
539
623
|
settings: normalizedRootSettings,
|
|
624
|
+
secrets: raw.secrets,
|
|
540
625
|
};
|
|
541
626
|
}
|
package/dist/config/types.d.ts
CHANGED
|
@@ -267,6 +267,9 @@ export interface CodeScanningSettings {
|
|
|
267
267
|
querySuite?: CodeScanningQuerySuite;
|
|
268
268
|
languages?: CodeScanningLanguage[];
|
|
269
269
|
}
|
|
270
|
+
export interface SecretConfig {
|
|
271
|
+
env: string;
|
|
272
|
+
}
|
|
270
273
|
export interface RepoSettings {
|
|
271
274
|
/** GitHub rulesets keyed by name */
|
|
272
275
|
rulesets?: Record<string, Ruleset>;
|
|
@@ -276,6 +279,10 @@ export interface RepoSettings {
|
|
|
276
279
|
labels?: Record<string, Label>;
|
|
277
280
|
/** GitHub code scanning default setup */
|
|
278
281
|
codeScanning?: CodeScanningSettings;
|
|
282
|
+
/** GitHub Actions variables keyed by name */
|
|
283
|
+
variables?: Record<string, string> & {
|
|
284
|
+
deleteOrphaned?: boolean;
|
|
285
|
+
};
|
|
279
286
|
deleteOrphaned?: boolean;
|
|
280
287
|
}
|
|
281
288
|
export type ContentValue = Record<string, unknown> | string | string[];
|
|
@@ -336,6 +343,9 @@ export interface RawRootSettings {
|
|
|
336
343
|
repo?: GitHubRepoSettings | false;
|
|
337
344
|
labels?: Record<string, Label | false>;
|
|
338
345
|
codeScanning?: CodeScanningSettings | false;
|
|
346
|
+
variables?: Record<string, string | false> & {
|
|
347
|
+
deleteOrphaned?: boolean;
|
|
348
|
+
};
|
|
339
349
|
deleteOrphaned?: boolean;
|
|
340
350
|
}
|
|
341
351
|
export interface RawRepoSettings {
|
|
@@ -347,6 +357,10 @@ export interface RawRepoSettings {
|
|
|
347
357
|
inherit?: boolean;
|
|
348
358
|
};
|
|
349
359
|
codeScanning?: CodeScanningSettings | false;
|
|
360
|
+
variables?: Record<string, string | false> & {
|
|
361
|
+
inherit?: boolean;
|
|
362
|
+
deleteOrphaned?: boolean;
|
|
363
|
+
};
|
|
350
364
|
deleteOrphaned?: boolean;
|
|
351
365
|
}
|
|
352
366
|
export interface RawRepoConfig {
|
|
@@ -373,6 +387,9 @@ export interface RawConfig {
|
|
|
373
387
|
githubHosts?: string[];
|
|
374
388
|
deleteOrphaned?: boolean;
|
|
375
389
|
settings?: RawRootSettings;
|
|
390
|
+
secrets?: Record<string, SecretConfig | boolean> & {
|
|
391
|
+
deleteOrphaned?: boolean;
|
|
392
|
+
};
|
|
376
393
|
}
|
|
377
394
|
export interface FileContent {
|
|
378
395
|
fileName: string;
|
|
@@ -402,5 +419,8 @@ export interface Config {
|
|
|
402
419
|
githubHosts?: string[];
|
|
403
420
|
deleteOrphaned?: boolean;
|
|
404
421
|
settings?: RepoSettings;
|
|
422
|
+
secrets?: Record<string, SecretConfig | boolean> & {
|
|
423
|
+
deleteOrphaned?: boolean;
|
|
424
|
+
};
|
|
405
425
|
}
|
|
406
426
|
export {};
|
|
@@ -9,4 +9,8 @@ export declare function validateRawConfig(config: RawConfig): void;
|
|
|
9
9
|
* @throws ValidationError if neither files nor settings are present
|
|
10
10
|
*/
|
|
11
11
|
export declare function validateForSync(config: RawConfig): void;
|
|
12
|
+
export declare function validateVariableSecretOverlaps(config: RawConfig): void;
|
|
12
13
|
export declare function hasActionableSettings(settings: RawRootSettings | RawRepoSettings | undefined): boolean;
|
|
14
|
+
export declare function validateVariableName(name: string): void;
|
|
15
|
+
export declare function validateSecretName(name: string): void;
|
|
16
|
+
export declare function validateSecretsConfig(config: RawConfig): void;
|