reposets 0.4.0 → 0.4.2
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/README.md +15 -14
- package/bin/reposets.d.ts +1 -0
- package/bin/reposets.js +25 -507
- package/cli/commands/credentials.js +74 -0
- package/cli/commands/doctor.js +154 -0
- package/cli/commands/init.js +147 -0
- package/cli/commands/list.js +49 -0
- package/cli/commands/sync.js +70 -0
- package/cli/commands/validate.js +53 -0
- package/errors.js +12 -0
- package/index.d.ts +1750 -1840
- package/index.js +19 -2
- package/lib/crypto.js +27 -0
- package/package.json +61 -73
- package/schemas/common.js +130 -0
- package/schemas/config.js +469 -0
- package/schemas/credentials.js +78 -0
- package/schemas/environment.js +70 -0
- package/schemas/ruleset.js +545 -0
- package/services/ConfigFiles.js +119 -0
- package/services/CredentialResolver.js +40 -0
- package/services/GitHubClient.js +875 -0
- package/services/OnePasswordClient.js +30 -0
- package/services/SyncEngine.js +580 -0
- package/services/SyncLogger.js +106 -0
- package/tsdoc-metadata.json +11 -11
- package/500.js +0 -3354
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { OnePasswordError } from "../errors.js";
|
|
2
|
+
import { Context, Effect, Layer } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/services/OnePasswordClient.ts
|
|
5
|
+
var OnePasswordClient = class extends Context.Tag("OnePasswordClient")() {};
|
|
6
|
+
/* v8 ignore start -- live 1Password SDK calls, tested via OnePasswordClientTest */
|
|
7
|
+
const OnePasswordClientLive = Layer.succeed(OnePasswordClient, { resolve(reference, serviceAccountToken) {
|
|
8
|
+
return Effect.tryPromise({
|
|
9
|
+
try: async () => {
|
|
10
|
+
const { createClient } = await import("@1password/sdk");
|
|
11
|
+
return await (await createClient({
|
|
12
|
+
auth: serviceAccountToken,
|
|
13
|
+
integrationName: "reposets",
|
|
14
|
+
integrationVersion: "1.0.0"
|
|
15
|
+
})).secrets.resolve(reference);
|
|
16
|
+
},
|
|
17
|
+
catch: (error) => new OnePasswordError({ message: `Failed to resolve ${reference}: ${error instanceof Error ? error.message : String(error)}` })
|
|
18
|
+
});
|
|
19
|
+
} });
|
|
20
|
+
/* v8 ignore stop */
|
|
21
|
+
function OnePasswordClientTest(stubs) {
|
|
22
|
+
return Layer.succeed(OnePasswordClient, { resolve(reference, _serviceAccountToken) {
|
|
23
|
+
const value = stubs[reference];
|
|
24
|
+
if (value === void 0) return Effect.fail(new OnePasswordError({ message: `Test stub: unknown reference ${reference}` }));
|
|
25
|
+
return Effect.succeed(value);
|
|
26
|
+
} });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { OnePasswordClient, OnePasswordClientLive, OnePasswordClientTest };
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import { ResolveError, SyncError } from "../errors.js";
|
|
2
|
+
import { buildRulesetPayload } from "../schemas/ruleset.js";
|
|
3
|
+
import { CredentialResolver } from "./CredentialResolver.js";
|
|
4
|
+
import { GitHubClient, ORG_ONLY_SETTINGS } from "./GitHubClient.js";
|
|
5
|
+
import { SyncLogger } from "./SyncLogger.js";
|
|
6
|
+
import { Context, Effect, Layer } from "effect";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { isAbsolute, resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
//#region src/services/SyncEngine.ts
|
|
11
|
+
/** Fields that GitHub only accepts on org-owned repositories. */
|
|
12
|
+
const ORG_ONLY_SAA_FIELDS = new Set([
|
|
13
|
+
"secret_scanning_delegated_alert_dismissal",
|
|
14
|
+
"secret_scanning_delegated_bypass",
|
|
15
|
+
"delegated_bypass_reviewers"
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Map GitHub repo language names (as returned by listLanguages) to the
|
|
19
|
+
* code scanning default-setup language enum. Unmapped languages are dropped
|
|
20
|
+
* from the detected set (callers warn about configured languages that don't
|
|
21
|
+
* appear in the mapped detected set).
|
|
22
|
+
*/
|
|
23
|
+
const REPO_LANG_TO_CODEQL = {
|
|
24
|
+
JavaScript: "javascript-typescript",
|
|
25
|
+
TypeScript: "javascript-typescript",
|
|
26
|
+
C: "c-cpp",
|
|
27
|
+
"C++": "c-cpp",
|
|
28
|
+
"C#": "csharp",
|
|
29
|
+
Go: "go",
|
|
30
|
+
Java: "java-kotlin",
|
|
31
|
+
Kotlin: "java-kotlin",
|
|
32
|
+
Python: "python",
|
|
33
|
+
Ruby: "ruby",
|
|
34
|
+
Swift: "swift"
|
|
35
|
+
};
|
|
36
|
+
var SyncEngine = class extends Context.Tag("SyncEngine")() {};
|
|
37
|
+
function isCleanupActive(scope) {
|
|
38
|
+
return scope !== false;
|
|
39
|
+
}
|
|
40
|
+
function getPreserveList(scope) {
|
|
41
|
+
if (typeof scope === "object" && "preserve" in scope) return new Set(scope.preserve);
|
|
42
|
+
return /* @__PURE__ */ new Set();
|
|
43
|
+
}
|
|
44
|
+
function resolveResourceGroup(group, credentialMap, basePath) {
|
|
45
|
+
return Effect.gen(function* () {
|
|
46
|
+
const result = /* @__PURE__ */ new Map();
|
|
47
|
+
if ("file" in group) for (const [name, filePath] of Object.entries(group.file)) {
|
|
48
|
+
const fullPath = isAbsolute(filePath) ? filePath : resolve(basePath, filePath);
|
|
49
|
+
const content = yield* Effect.try({
|
|
50
|
+
try: () => readFileSync(fullPath, "utf-8").trim(),
|
|
51
|
+
catch: (error) => new ResolveError({ message: `Failed to read file for '${name}': ${error instanceof Error ? error.message : String(error)}` })
|
|
52
|
+
});
|
|
53
|
+
result.set(name, content);
|
|
54
|
+
}
|
|
55
|
+
else if ("value" in group) for (const [name, val] of Object.entries(group.value)) result.set(name, typeof val === "string" ? val : JSON.stringify(val));
|
|
56
|
+
else if ("resolved" in group) for (const [name, label] of Object.entries(group.resolved)) {
|
|
57
|
+
const value = credentialMap.get(label);
|
|
58
|
+
if (value === void 0) yield* Effect.fail(new ResolveError({ message: `Credential label '${label}' not found for '${name}'` }));
|
|
59
|
+
else result.set(name, value);
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function resolveRulesetRefs(ruleset, credentialMap) {
|
|
65
|
+
const json = JSON.parse(JSON.stringify(ruleset));
|
|
66
|
+
substituteResolved(json, credentialMap);
|
|
67
|
+
return json;
|
|
68
|
+
}
|
|
69
|
+
function substituteResolved(obj, credentialMap) {
|
|
70
|
+
for (const [key, value] of Object.entries(obj)) if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
71
|
+
const rec = value;
|
|
72
|
+
if ("resolved" in rec && typeof rec.resolved === "string") {
|
|
73
|
+
const resolved = credentialMap.get(rec.resolved);
|
|
74
|
+
if (resolved === void 0) throw new Error(`Credential label '${rec.resolved}' not found for ruleset field '${key}'`);
|
|
75
|
+
const num = Number(resolved);
|
|
76
|
+
obj[key] = Number.isNaN(num) ? resolved : num;
|
|
77
|
+
} else substituteResolved(rec, credentialMap);
|
|
78
|
+
} else if (Array.isArray(value)) {
|
|
79
|
+
for (const item of value) if (item && typeof item === "object") substituteResolved(item, credentialMap);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function groupEntryNames(group) {
|
|
83
|
+
if ("file" in group) return Object.keys(group.file);
|
|
84
|
+
if ("value" in group) return Object.keys(group.value);
|
|
85
|
+
if ("resolved" in group) return Object.keys(group.resolved);
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
/** Last-write-wins merge of security_and_analysis blocks across setting groups. */
|
|
89
|
+
function mergeSecurityAndAnalysis(blocks) {
|
|
90
|
+
const merged = {};
|
|
91
|
+
let hasAny = false;
|
|
92
|
+
for (const block of blocks) {
|
|
93
|
+
if (!block) continue;
|
|
94
|
+
hasAny = true;
|
|
95
|
+
for (const [key, value] of Object.entries(block)) if (value !== void 0) merged[key] = value;
|
|
96
|
+
}
|
|
97
|
+
return hasAny ? merged : void 0;
|
|
98
|
+
}
|
|
99
|
+
/** Last-write-wins merge of SecurityGroup toggles across referenced groups. */
|
|
100
|
+
function mergeSecurityGroups(groups) {
|
|
101
|
+
const merged = {};
|
|
102
|
+
for (const group of groups) {
|
|
103
|
+
if (!group) continue;
|
|
104
|
+
for (const [key, value] of Object.entries(group)) if (typeof value === "boolean") merged[key] = value;
|
|
105
|
+
}
|
|
106
|
+
return merged;
|
|
107
|
+
}
|
|
108
|
+
/** Last-write-wins merge of CodeScanningGroup configs across referenced groups. */
|
|
109
|
+
function mergeCodeScanningGroups(groups) {
|
|
110
|
+
const merged = {};
|
|
111
|
+
for (const group of groups) {
|
|
112
|
+
if (!group) continue;
|
|
113
|
+
for (const [key, value] of Object.entries(group)) if (value !== void 0) merged[key] = value;
|
|
114
|
+
}
|
|
115
|
+
return merged;
|
|
116
|
+
}
|
|
117
|
+
const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function* () {
|
|
118
|
+
const github = yield* GitHubClient;
|
|
119
|
+
const credResolver = yield* CredentialResolver;
|
|
120
|
+
const logger = yield* SyncLogger;
|
|
121
|
+
return { syncAll(config, credentials, options) {
|
|
122
|
+
return Effect.gen(function* () {
|
|
123
|
+
const { dryRun, noCleanup, groupFilter, repoFilter } = options;
|
|
124
|
+
const profileEntries = Object.entries(credentials.profiles);
|
|
125
|
+
const defaultProfileName = profileEntries.length === 1 ? profileEntries[0]?.[0] ?? "default" : "default";
|
|
126
|
+
const defaultProfile = credentials.profiles[defaultProfileName];
|
|
127
|
+
const groups = Object.entries(config.groups);
|
|
128
|
+
for (const [groupName, group] of groups) {
|
|
129
|
+
if (groupFilter && groupName !== groupFilter) continue;
|
|
130
|
+
const owner = group.owner ?? config.owner ?? "";
|
|
131
|
+
const profileName = group.credentials ?? defaultProfileName;
|
|
132
|
+
const profile = credentials.profiles[profileName] ?? defaultProfile;
|
|
133
|
+
yield* logger.groupStart(groupName, group.repos.length);
|
|
134
|
+
const credentialMap = profile ? yield* credResolver.resolveAll(profile, options.configDir) : /* @__PURE__ */ new Map();
|
|
135
|
+
const ownerType = yield* github.getOwnerType(owner).pipe(Effect.catchTag("GitHubApiError", () => Effect.succeed("User")));
|
|
136
|
+
const secretScopes = [
|
|
137
|
+
"actions",
|
|
138
|
+
"dependabot",
|
|
139
|
+
"codespaces"
|
|
140
|
+
];
|
|
141
|
+
const resolvedSecrets = /* @__PURE__ */ new Map();
|
|
142
|
+
for (const scope of secretScopes) {
|
|
143
|
+
const groupRefs = group.secrets?.[scope] ?? [];
|
|
144
|
+
for (const groupRef of groupRefs) {
|
|
145
|
+
const secretGroup = config.secrets[groupRef];
|
|
146
|
+
if (!secretGroup) continue;
|
|
147
|
+
const entries = yield* resolveResourceGroup(secretGroup, credentialMap, options.configDir);
|
|
148
|
+
const scopeMap = resolvedSecrets.get(scope) ?? /* @__PURE__ */ new Map();
|
|
149
|
+
for (const [name, value] of entries) scopeMap.set(name, value);
|
|
150
|
+
resolvedSecrets.set(scope, scopeMap);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const resolvedVariables = /* @__PURE__ */ new Map();
|
|
154
|
+
const variableGroupRefs = group.variables?.actions ?? [];
|
|
155
|
+
for (const groupRef of variableGroupRefs) {
|
|
156
|
+
const variableGroup = config.variables[groupRef];
|
|
157
|
+
if (!variableGroup) continue;
|
|
158
|
+
const entries = yield* resolveResourceGroup(variableGroup, credentialMap, options.configDir);
|
|
159
|
+
for (const [name, value] of entries) resolvedVariables.set(name, value);
|
|
160
|
+
}
|
|
161
|
+
const groupRulesetRefs = group.rulesets ?? [];
|
|
162
|
+
const rulesetMap = /* @__PURE__ */ new Map();
|
|
163
|
+
for (const ref of groupRulesetRefs) {
|
|
164
|
+
const ruleset = config.rulesets[ref];
|
|
165
|
+
if (ruleset) {
|
|
166
|
+
const payload = buildRulesetPayload(resolveRulesetRefs(ruleset, credentialMap));
|
|
167
|
+
rulesetMap.set(ref, payload);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const envRefs = group.environments ?? [];
|
|
171
|
+
const envSecretMapping = group.secrets?.environments ?? {};
|
|
172
|
+
const envVariableMapping = group.variables?.environments ?? {};
|
|
173
|
+
const resolvedEnvSecrets = /* @__PURE__ */ new Map();
|
|
174
|
+
for (const [envName, groupRefs] of Object.entries(envSecretMapping)) {
|
|
175
|
+
const envMap = /* @__PURE__ */ new Map();
|
|
176
|
+
for (const groupRef of groupRefs) {
|
|
177
|
+
const secretGroup = config.secrets[groupRef];
|
|
178
|
+
if (!secretGroup) continue;
|
|
179
|
+
const entries = yield* resolveResourceGroup(secretGroup, credentialMap, options.configDir);
|
|
180
|
+
for (const [name, value] of entries) envMap.set(name, value);
|
|
181
|
+
}
|
|
182
|
+
resolvedEnvSecrets.set(envName, envMap);
|
|
183
|
+
}
|
|
184
|
+
const resolvedEnvVariables = /* @__PURE__ */ new Map();
|
|
185
|
+
for (const [envName, groupRefs] of Object.entries(envVariableMapping)) {
|
|
186
|
+
const envMap = /* @__PURE__ */ new Map();
|
|
187
|
+
for (const groupRef of groupRefs) {
|
|
188
|
+
const variableGroup = config.variables[groupRef];
|
|
189
|
+
if (!variableGroup) continue;
|
|
190
|
+
const entries = yield* resolveResourceGroup(variableGroup, credentialMap, options.configDir);
|
|
191
|
+
for (const [name, value] of entries) envMap.set(name, value);
|
|
192
|
+
}
|
|
193
|
+
resolvedEnvVariables.set(envName, envMap);
|
|
194
|
+
}
|
|
195
|
+
const effectiveCleanup = group.cleanup ?? {
|
|
196
|
+
secrets: {
|
|
197
|
+
actions: false,
|
|
198
|
+
dependabot: false,
|
|
199
|
+
codespaces: false,
|
|
200
|
+
environments: false
|
|
201
|
+
},
|
|
202
|
+
variables: {
|
|
203
|
+
actions: false,
|
|
204
|
+
environments: false
|
|
205
|
+
},
|
|
206
|
+
rulesets: false,
|
|
207
|
+
environments: false
|
|
208
|
+
};
|
|
209
|
+
const configuredSecretNames = (scope) => {
|
|
210
|
+
const refs = group.secrets?.[scope] ?? [];
|
|
211
|
+
const names = /* @__PURE__ */ new Set();
|
|
212
|
+
for (const ref of refs) {
|
|
213
|
+
const grp = config.secrets[ref];
|
|
214
|
+
if (grp) for (const name of groupEntryNames(grp)) names.add(name);
|
|
215
|
+
}
|
|
216
|
+
return names;
|
|
217
|
+
};
|
|
218
|
+
const configuredVariableNames = () => {
|
|
219
|
+
const refs = group.variables?.actions ?? [];
|
|
220
|
+
const names = /* @__PURE__ */ new Set();
|
|
221
|
+
for (const ref of refs) {
|
|
222
|
+
const grp = config.variables[ref];
|
|
223
|
+
if (grp) for (const name of groupEntryNames(grp)) names.add(name);
|
|
224
|
+
}
|
|
225
|
+
return names;
|
|
226
|
+
};
|
|
227
|
+
const configuredRulesetNames = () => {
|
|
228
|
+
const refs = group.rulesets ?? [];
|
|
229
|
+
const names = /* @__PURE__ */ new Set();
|
|
230
|
+
for (const ref of refs) {
|
|
231
|
+
const ruleset = config.rulesets[ref];
|
|
232
|
+
if (ruleset) names.add(ruleset.name);
|
|
233
|
+
}
|
|
234
|
+
return names;
|
|
235
|
+
};
|
|
236
|
+
const settingGroupRefs = group.settings ?? [];
|
|
237
|
+
const mergedSettings = {};
|
|
238
|
+
const skippedSettings = [];
|
|
239
|
+
const saaBlocks = [];
|
|
240
|
+
for (const ref of settingGroupRefs) {
|
|
241
|
+
const settingGroup = config.settings[ref];
|
|
242
|
+
if (settingGroup) {
|
|
243
|
+
const { security_and_analysis, ...rest } = settingGroup;
|
|
244
|
+
Object.assign(mergedSettings, rest);
|
|
245
|
+
saaBlocks.push(security_and_analysis);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (ownerType === "User") {
|
|
249
|
+
for (const key of ORG_ONLY_SETTINGS) if (key in mergedSettings) {
|
|
250
|
+
delete mergedSettings[key];
|
|
251
|
+
skippedSettings.push(key);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const mergedSAA = mergeSecurityAndAnalysis(saaBlocks);
|
|
255
|
+
const skippedSAA = [];
|
|
256
|
+
if (mergedSAA) {
|
|
257
|
+
const saaOut = { ...mergedSAA };
|
|
258
|
+
if (ownerType === "User") {
|
|
259
|
+
for (const key of ORG_ONLY_SAA_FIELDS) if (key in saaOut) {
|
|
260
|
+
delete saaOut[key];
|
|
261
|
+
skippedSAA.push(key);
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
const reviewers = saaOut.delegated_bypass_reviewers;
|
|
265
|
+
if (Array.isArray(reviewers)) {
|
|
266
|
+
const resolved = [];
|
|
267
|
+
for (const reviewer of reviewers) if (typeof reviewer.team === "string") {
|
|
268
|
+
const teamId = yield* github.resolveTeamId(owner, reviewer.team).pipe(Effect.catchTag("GitHubApiError", (err) => Effect.gen(function* () {
|
|
269
|
+
yield* logger.syncError(`resolve team '${reviewer.team}'`, err.message);
|
|
270
|
+
})));
|
|
271
|
+
if (teamId !== void 0) {
|
|
272
|
+
const entry = {
|
|
273
|
+
reviewer_id: teamId,
|
|
274
|
+
reviewer_type: "TEAM"
|
|
275
|
+
};
|
|
276
|
+
if (reviewer.mode !== void 0) entry.mode = reviewer.mode;
|
|
277
|
+
resolved.push(entry);
|
|
278
|
+
}
|
|
279
|
+
} else if (typeof reviewer.role === "string") {
|
|
280
|
+
const roleId = yield* github.resolveRoleId(owner, reviewer.role).pipe(Effect.catchTag("GitHubApiError", (err) => Effect.gen(function* () {
|
|
281
|
+
yield* logger.syncError(`resolve role '${reviewer.role}'`, err.message);
|
|
282
|
+
})));
|
|
283
|
+
if (roleId !== void 0) {
|
|
284
|
+
const entry = {
|
|
285
|
+
reviewer_id: roleId,
|
|
286
|
+
reviewer_type: "ROLE"
|
|
287
|
+
};
|
|
288
|
+
if (reviewer.mode !== void 0) entry.mode = reviewer.mode;
|
|
289
|
+
resolved.push(entry);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
saaOut.delegated_bypass_reviewers = resolved;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (Object.keys(saaOut).length > 0) mergedSettings.security_and_analysis = saaOut;
|
|
296
|
+
}
|
|
297
|
+
const mergedSecurity = mergeSecurityGroups((group.security ?? []).map((ref) => config.security[ref]));
|
|
298
|
+
const mergedCodeScanning = mergeCodeScanningGroups((group.code_scanning ?? []).map((ref) => config.code_scanning[ref]));
|
|
299
|
+
const hasSecurity = Object.keys(mergedSecurity).length > 0;
|
|
300
|
+
const securityContradiction = mergedSecurity.automated_security_fixes === true && mergedSecurity.vulnerability_alerts === false;
|
|
301
|
+
const hasCodeScanning = Object.keys(mergedCodeScanning).length > 0;
|
|
302
|
+
const hasSecrets = secretScopes.some((s) => (resolvedSecrets.get(s)?.size ?? 0) > 0);
|
|
303
|
+
const hasVariables = resolvedVariables.size > 0;
|
|
304
|
+
const hasRulesets = rulesetMap.size > 0;
|
|
305
|
+
const hasSettings = Object.keys(mergedSettings).length > 0;
|
|
306
|
+
const hasEnvironments = envRefs.length > 0;
|
|
307
|
+
const hasEnvSecrets = resolvedEnvSecrets.size > 0;
|
|
308
|
+
const hasEnvVariables = resolvedEnvVariables.size > 0;
|
|
309
|
+
const hasCleanup = !noCleanup && (isCleanupActive(effectiveCleanup.secrets.actions) || isCleanupActive(effectiveCleanup.secrets.dependabot) || isCleanupActive(effectiveCleanup.secrets.codespaces) || isCleanupActive(effectiveCleanup.secrets.environments) || isCleanupActive(effectiveCleanup.variables.actions) || isCleanupActive(effectiveCleanup.variables.environments) || isCleanupActive(effectiveCleanup.rulesets) || isCleanupActive(effectiveCleanup.environments));
|
|
310
|
+
for (const repoName of group.repos) {
|
|
311
|
+
if (repoFilter && repoName !== repoFilter) continue;
|
|
312
|
+
if (!hasSecrets && !hasVariables && !hasRulesets && !hasSettings && !hasEnvironments && !hasEnvSecrets && !hasEnvVariables && !hasSecurity && !hasCodeScanning && !hasCleanup) {
|
|
313
|
+
yield* logger.repoSkip(owner, repoName, "no changes configured");
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
yield* logger.repoStart(owner, repoName);
|
|
317
|
+
if (!dryRun) {
|
|
318
|
+
if (hasSettings) {
|
|
319
|
+
yield* logger.syncOperation("apply", "settings", "");
|
|
320
|
+
yield* github.syncSettings(owner, repoName, mergedSettings).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError("settings", err.message)));
|
|
321
|
+
}
|
|
322
|
+
for (const key of skippedSettings) yield* logger.syncOperation("skip", "setting", key, "(org-only, owner is a personal account)");
|
|
323
|
+
for (const key of skippedSAA) yield* logger.syncOperation("skip", "security_and_analysis", key, "(org-only, owner is a personal account)");
|
|
324
|
+
if (hasSecurity && securityContradiction) yield* logger.syncError("security merge", "automated_security_fixes = true requires vulnerability_alerts to be enabled (or omitted); skipping security sync");
|
|
325
|
+
else if (hasSecurity) {
|
|
326
|
+
if (mergedSecurity.vulnerability_alerts !== void 0) {
|
|
327
|
+
const desired = mergedSecurity.vulnerability_alerts;
|
|
328
|
+
if ((yield* github.getVulnerabilityAlerts(owner, repoName).pipe(Effect.catchTag("GitHubApiError", (err) => Effect.gen(function* () {
|
|
329
|
+
yield* logger.syncError("get vulnerability_alerts", err.message);
|
|
330
|
+
return desired;
|
|
331
|
+
})))) !== desired) {
|
|
332
|
+
yield* logger.syncOperation("sync", "vulnerability_alerts", desired ? "enable" : "disable");
|
|
333
|
+
yield* github.setVulnerabilityAlerts(owner, repoName, desired).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError("vulnerability_alerts", err.message)));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (mergedSecurity.automated_security_fixes !== void 0) {
|
|
337
|
+
const desired = mergedSecurity.automated_security_fixes;
|
|
338
|
+
if ((yield* github.getAutomatedSecurityFixes(owner, repoName).pipe(Effect.catchTag("GitHubApiError", (err) => Effect.gen(function* () {
|
|
339
|
+
yield* logger.syncError("get automated_security_fixes", err.message);
|
|
340
|
+
return desired;
|
|
341
|
+
})))) !== desired) {
|
|
342
|
+
yield* logger.syncOperation("sync", "automated_security_fixes", desired ? "enable" : "disable");
|
|
343
|
+
yield* github.setAutomatedSecurityFixes(owner, repoName, desired).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError("automated_security_fixes", err.message)));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (mergedSecurity.private_vulnerability_reporting !== void 0) {
|
|
347
|
+
const desired = mergedSecurity.private_vulnerability_reporting;
|
|
348
|
+
if ((yield* github.getPrivateVulnerabilityReporting(owner, repoName).pipe(Effect.catchTag("GitHubApiError", (err) => Effect.gen(function* () {
|
|
349
|
+
yield* logger.syncError("get private_vulnerability_reporting", err.message);
|
|
350
|
+
return desired;
|
|
351
|
+
})))) !== desired) {
|
|
352
|
+
yield* logger.syncOperation("sync", "private_vulnerability_reporting", desired ? "enable" : "disable");
|
|
353
|
+
yield* github.setPrivateVulnerabilityReporting(owner, repoName, desired).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError("private_vulnerability_reporting", err.message)));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (hasCodeScanning) {
|
|
358
|
+
let desiredConfig = mergedCodeScanning;
|
|
359
|
+
if (mergedCodeScanning.languages !== void 0) {
|
|
360
|
+
const detected = yield* github.listRepoLanguages(owner, repoName).pipe(Effect.catchTag("GitHubApiError", (err) => Effect.gen(function* () {
|
|
361
|
+
yield* logger.syncError("list repo languages", err.message);
|
|
362
|
+
return [];
|
|
363
|
+
})));
|
|
364
|
+
const detectedCodeQL = /* @__PURE__ */ new Set();
|
|
365
|
+
for (const lang of detected) {
|
|
366
|
+
const mapped = REPO_LANG_TO_CODEQL[lang];
|
|
367
|
+
if (mapped) detectedCodeQL.add(mapped);
|
|
368
|
+
}
|
|
369
|
+
const filtered = [];
|
|
370
|
+
for (const lang of mergedCodeScanning.languages) if (lang === "actions" || detectedCodeQL.has(lang)) filtered.push(lang);
|
|
371
|
+
else yield* logger.syncOperation("skip", "code_scanning language", lang, "(not detected in repository)");
|
|
372
|
+
desiredConfig = {
|
|
373
|
+
...mergedCodeScanning,
|
|
374
|
+
languages: filtered
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
yield* logger.syncOperation("sync", "code_scanning", desiredConfig.state ?? "default-setup");
|
|
378
|
+
yield* github.updateCodeScanningDefaultSetup(owner, repoName, desiredConfig).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError("code_scanning default setup", err.message)));
|
|
379
|
+
}
|
|
380
|
+
for (const envName of envRefs) {
|
|
381
|
+
const envConfig = config.environments[envName];
|
|
382
|
+
if (!envConfig) continue;
|
|
383
|
+
yield* logger.syncOperation("sync", "environment", envName);
|
|
384
|
+
yield* github.syncEnvironment(owner, repoName, envName, envConfig).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError(`environment ${envName}`, err.message)));
|
|
385
|
+
}
|
|
386
|
+
for (const scope of secretScopes) {
|
|
387
|
+
const scopeMap = resolvedSecrets.get(scope);
|
|
388
|
+
if (scopeMap) for (const [name, value] of scopeMap) {
|
|
389
|
+
yield* logger.syncOperation("sync", "secret", name, `(${scope})`);
|
|
390
|
+
yield* github.syncSecret(owner, repoName, name, value, scope).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError(`secret ${name} (${scope})`, err.message)));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
for (const [envName, envMap] of resolvedEnvSecrets) for (const [name, value] of envMap) {
|
|
394
|
+
yield* logger.syncOperation("sync", "secret", name, `(env: ${envName})`);
|
|
395
|
+
yield* github.syncEnvironmentSecret(owner, repoName, envName, name, value).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError(`secret ${name} (env: ${envName})`, err.message)));
|
|
396
|
+
}
|
|
397
|
+
for (const [name, value] of resolvedVariables) {
|
|
398
|
+
yield* logger.syncOperation("sync", "variable", name);
|
|
399
|
+
yield* github.syncVariable(owner, repoName, name, value).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError(`variable ${name}`, err.message)));
|
|
400
|
+
}
|
|
401
|
+
for (const [envName, envMap] of resolvedEnvVariables) for (const [name, value] of envMap) {
|
|
402
|
+
yield* logger.syncOperation("sync", "variable", name, `(env: ${envName})`);
|
|
403
|
+
yield* github.syncEnvironmentVariable(owner, repoName, envName, name, value).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError(`variable ${name} (env: ${envName})`, err.message)));
|
|
404
|
+
}
|
|
405
|
+
for (const [_key, ruleset] of rulesetMap) {
|
|
406
|
+
yield* logger.syncOperation("sync", "ruleset", ruleset.name);
|
|
407
|
+
yield* github.syncRuleset(owner, repoName, ruleset.name, ruleset).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError(`ruleset ${ruleset.name}`, err.message)));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (hasSecurity) {
|
|
411
|
+
const securityCount = Object.values(mergedSecurity).filter((v) => v !== void 0).length;
|
|
412
|
+
yield* logger.syncSummary("security feature", securityCount, "");
|
|
413
|
+
}
|
|
414
|
+
if (hasCodeScanning) yield* logger.syncSummary("code scanning", 1, mergedCodeScanning.state ?? "applied");
|
|
415
|
+
if (hasEnvironments) yield* logger.syncSummary("environment", envRefs.length, "");
|
|
416
|
+
if (hasSecrets) {
|
|
417
|
+
const scopeCounts = [];
|
|
418
|
+
let totalSecrets = 0;
|
|
419
|
+
for (const scope of secretScopes) {
|
|
420
|
+
const count = resolvedSecrets.get(scope)?.size ?? 0;
|
|
421
|
+
if (count > 0) {
|
|
422
|
+
scopeCounts.push(`${scope}: ${count}`);
|
|
423
|
+
totalSecrets += count;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
yield* logger.syncSummary("secret", totalSecrets, scopeCounts.join(", "));
|
|
427
|
+
}
|
|
428
|
+
if (hasVariables) yield* logger.syncSummary("variable", resolvedVariables.size, "");
|
|
429
|
+
if (hasSettings) yield* logger.settingsApplied();
|
|
430
|
+
if (rulesetMap.size > 0) yield* logger.syncSummary("ruleset", rulesetMap.size, "");
|
|
431
|
+
if (!noCleanup) {
|
|
432
|
+
if (isCleanupActive(effectiveCleanup.secrets.actions)) {
|
|
433
|
+
const configured = configuredSecretNames("actions");
|
|
434
|
+
const preserved = getPreserveList(effectiveCleanup.secrets.actions);
|
|
435
|
+
const existing = yield* github.listSecrets(owner, repoName, "actions").pipe(Effect.catchTag("GitHubApiError", (err) => Effect.gen(function* () {
|
|
436
|
+
yield* logger.syncError("list secrets (actions)", err.message);
|
|
437
|
+
return [];
|
|
438
|
+
})));
|
|
439
|
+
const toDelete = [];
|
|
440
|
+
for (const { name } of existing) if (!configured.has(name) && !preserved.has(name)) {
|
|
441
|
+
toDelete.push(name);
|
|
442
|
+
yield* logger.syncOperation("delete", "secret", name, "(actions)");
|
|
443
|
+
if (!dryRun) yield* github.deleteSecret(owner, repoName, name, "actions").pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError(`delete secret ${name} (actions)`, err.message)));
|
|
444
|
+
}
|
|
445
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("secret", toDelete.length, toDelete);
|
|
446
|
+
}
|
|
447
|
+
/* v8 ignore start -- repetitive scope-specific cleanup blocks, structurally identical to actions cleanup above */
|
|
448
|
+
if (isCleanupActive(effectiveCleanup.secrets.dependabot)) {
|
|
449
|
+
const configured = configuredSecretNames("dependabot");
|
|
450
|
+
const preserved = getPreserveList(effectiveCleanup.secrets.dependabot);
|
|
451
|
+
const existing = yield* github.listSecrets(owner, repoName, "dependabot").pipe(Effect.catchTag("GitHubApiError", (err) => Effect.gen(function* () {
|
|
452
|
+
yield* logger.syncError("list secrets (dependabot)", err.message);
|
|
453
|
+
return [];
|
|
454
|
+
})));
|
|
455
|
+
const toDelete = [];
|
|
456
|
+
for (const { name } of existing) if (!configured.has(name) && !preserved.has(name)) {
|
|
457
|
+
toDelete.push(name);
|
|
458
|
+
yield* logger.syncOperation("delete", "secret", name, "(dependabot)");
|
|
459
|
+
if (!dryRun) yield* github.deleteSecret(owner, repoName, name, "dependabot").pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError(`delete secret ${name} (dependabot)`, err.message)));
|
|
460
|
+
}
|
|
461
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("secret", toDelete.length, toDelete);
|
|
462
|
+
}
|
|
463
|
+
if (isCleanupActive(effectiveCleanup.secrets.codespaces)) {
|
|
464
|
+
const configured = configuredSecretNames("codespaces");
|
|
465
|
+
const preserved = getPreserveList(effectiveCleanup.secrets.codespaces);
|
|
466
|
+
const existing = yield* github.listSecrets(owner, repoName, "codespaces").pipe(Effect.catchTag("GitHubApiError", (err) => Effect.gen(function* () {
|
|
467
|
+
yield* logger.syncError("list secrets (codespaces)", err.message);
|
|
468
|
+
return [];
|
|
469
|
+
})));
|
|
470
|
+
const toDelete = [];
|
|
471
|
+
for (const { name } of existing) if (!configured.has(name) && !preserved.has(name)) {
|
|
472
|
+
toDelete.push(name);
|
|
473
|
+
yield* logger.syncOperation("delete", "secret", name, "(codespaces)");
|
|
474
|
+
if (!dryRun) yield* github.deleteSecret(owner, repoName, name, "codespaces").pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError(`delete secret ${name} (codespaces)`, err.message)));
|
|
475
|
+
}
|
|
476
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("secret", toDelete.length, toDelete);
|
|
477
|
+
}
|
|
478
|
+
if (isCleanupActive(effectiveCleanup.secrets.environments)) {
|
|
479
|
+
const preserved = getPreserveList(effectiveCleanup.secrets.environments);
|
|
480
|
+
const allEnvNames = new Set([...envRefs, ...Object.keys(envSecretMapping)]);
|
|
481
|
+
for (const envName of allEnvNames) {
|
|
482
|
+
const configuredNames = /* @__PURE__ */ new Set();
|
|
483
|
+
for (const ref of envSecretMapping[envName] ?? []) {
|
|
484
|
+
const grp = config.secrets[ref];
|
|
485
|
+
if (grp) for (const name of groupEntryNames(grp)) configuredNames.add(name);
|
|
486
|
+
}
|
|
487
|
+
const existing = yield* github.listEnvironmentSecrets(owner, repoName, envName).pipe(Effect.catchTag("GitHubApiError", (err) => Effect.gen(function* () {
|
|
488
|
+
yield* logger.syncError(`list secrets (env: ${envName})`, err.message);
|
|
489
|
+
return [];
|
|
490
|
+
})));
|
|
491
|
+
const toDelete = [];
|
|
492
|
+
for (const { name } of existing) if (!configuredNames.has(name) && !preserved.has(name)) {
|
|
493
|
+
toDelete.push(name);
|
|
494
|
+
yield* logger.syncOperation("delete", "secret", name, `(env: ${envName})`);
|
|
495
|
+
if (!dryRun) yield* github.deleteEnvironmentSecret(owner, repoName, envName, name).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError(`delete secret ${name} (env: ${envName})`, err.message)));
|
|
496
|
+
}
|
|
497
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("secret", toDelete.length, toDelete);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (isCleanupActive(effectiveCleanup.variables.actions)) {
|
|
501
|
+
const configured = configuredVariableNames();
|
|
502
|
+
const preserved = getPreserveList(effectiveCleanup.variables.actions);
|
|
503
|
+
const existing = yield* github.listVariables(owner, repoName).pipe(Effect.catchTag("GitHubApiError", (err) => Effect.gen(function* () {
|
|
504
|
+
yield* logger.syncError("list variables", err.message);
|
|
505
|
+
return [];
|
|
506
|
+
})));
|
|
507
|
+
const toDelete = [];
|
|
508
|
+
for (const { name } of existing) if (!configured.has(name) && !preserved.has(name)) {
|
|
509
|
+
toDelete.push(name);
|
|
510
|
+
yield* logger.syncOperation("delete", "variable", name);
|
|
511
|
+
if (!dryRun) yield* github.deleteVariable(owner, repoName, name).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError(`delete variable ${name}`, err.message)));
|
|
512
|
+
}
|
|
513
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("variable", toDelete.length, toDelete);
|
|
514
|
+
}
|
|
515
|
+
if (isCleanupActive(effectiveCleanup.variables.environments)) {
|
|
516
|
+
const preserved = getPreserveList(effectiveCleanup.variables.environments);
|
|
517
|
+
const allEnvVarNames = new Set([...envRefs, ...Object.keys(envVariableMapping)]);
|
|
518
|
+
for (const envName of allEnvVarNames) {
|
|
519
|
+
const configuredNames = /* @__PURE__ */ new Set();
|
|
520
|
+
for (const ref of envVariableMapping[envName] ?? []) {
|
|
521
|
+
const grp = config.variables[ref];
|
|
522
|
+
if (grp) for (const name of groupEntryNames(grp)) configuredNames.add(name);
|
|
523
|
+
}
|
|
524
|
+
const existing = yield* github.listEnvironmentVariables(owner, repoName, envName).pipe(Effect.catchTag("GitHubApiError", (err) => Effect.gen(function* () {
|
|
525
|
+
yield* logger.syncError(`list variables (env: ${envName})`, err.message);
|
|
526
|
+
return [];
|
|
527
|
+
})));
|
|
528
|
+
const toDelete = [];
|
|
529
|
+
for (const { name } of existing) if (!configuredNames.has(name) && !preserved.has(name)) {
|
|
530
|
+
toDelete.push(name);
|
|
531
|
+
yield* logger.syncOperation("delete", "variable", name, `(env: ${envName})`);
|
|
532
|
+
if (!dryRun) yield* github.deleteEnvironmentVariable(owner, repoName, envName, name).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError(`delete variable ${name} (env: ${envName})`, err.message)));
|
|
533
|
+
}
|
|
534
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("variable", toDelete.length, toDelete);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (isCleanupActive(effectiveCleanup.rulesets)) {
|
|
538
|
+
const configured = configuredRulesetNames();
|
|
539
|
+
const preserved = getPreserveList(effectiveCleanup.rulesets);
|
|
540
|
+
const existing = yield* github.listRulesets(owner, repoName).pipe(Effect.catchTag("GitHubApiError", (err) => Effect.gen(function* () {
|
|
541
|
+
yield* logger.syncError("list rulesets", err.message);
|
|
542
|
+
return [];
|
|
543
|
+
})));
|
|
544
|
+
const toDelete = [];
|
|
545
|
+
for (const { name, id, source_type } of existing) {
|
|
546
|
+
if (source_type !== "Repository") continue;
|
|
547
|
+
if (!configured.has(name) && !preserved.has(name)) {
|
|
548
|
+
toDelete.push(name);
|
|
549
|
+
yield* logger.syncOperation("delete", "ruleset", name);
|
|
550
|
+
if (!dryRun) yield* github.deleteRuleset(owner, repoName, id).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError(`delete ruleset ${name}`, err.message)));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("ruleset", toDelete.length, toDelete);
|
|
554
|
+
}
|
|
555
|
+
if (isCleanupActive(effectiveCleanup.environments)) {
|
|
556
|
+
const configuredEnvNames = new Set(envRefs);
|
|
557
|
+
const preserved = getPreserveList(effectiveCleanup.environments);
|
|
558
|
+
const existing = yield* github.listEnvironments(owner, repoName).pipe(Effect.catchTag("GitHubApiError", (err) => Effect.gen(function* () {
|
|
559
|
+
yield* logger.syncError("list environments", err.message);
|
|
560
|
+
return [];
|
|
561
|
+
})));
|
|
562
|
+
const toDelete = [];
|
|
563
|
+
for (const { name } of existing) if (!configuredEnvNames.has(name) && !preserved.has(name)) {
|
|
564
|
+
toDelete.push(name);
|
|
565
|
+
yield* logger.syncOperation("delete", "environment", name);
|
|
566
|
+
if (!dryRun) yield* github.deleteEnvironment(owner, repoName, name).pipe(Effect.catchTag("GitHubApiError", (err) => logger.syncError(`delete environment ${name}`, err.message)));
|
|
567
|
+
}
|
|
568
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("environment", toDelete.length, toDelete);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/* v8 ignore stop */
|
|
574
|
+
yield* logger.finish();
|
|
575
|
+
}).pipe(Effect.catchAll((error) => Effect.fail(error instanceof SyncError ? error : new SyncError({ message: error instanceof Error ? error.message : String(error) }))));
|
|
576
|
+
} };
|
|
577
|
+
}));
|
|
578
|
+
|
|
579
|
+
//#endregion
|
|
580
|
+
export { SyncEngine, SyncEngineLive };
|