reposets 0.1.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/500.js +2545 -0
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/bin/reposets.js +500 -0
- package/index.d.ts +1680 -0
- package/index.js +2 -0
- package/package.json +75 -0
- package/tsdoc-metadata.json +11 -0
package/500.js
ADDED
|
@@ -0,0 +1,2545 @@
|
|
|
1
|
+
import { Context, Data, Effect, Layer, Option, Ref, Schema } from "effect";
|
|
2
|
+
import { blake2b } from "blakejs";
|
|
3
|
+
import tweetnacl from "tweetnacl";
|
|
4
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { AppDirsConfig, ConfigError, ConfigFile, FirstMatch, TomlCodec, UpwardWalk, XdgConfig, XdgConfigLive, XdgSavePath } from "xdg-effect";
|
|
7
|
+
import { Octokit } from "@octokit/rest";
|
|
8
|
+
class ResolveError extends Data.TaggedError("ResolveError") {
|
|
9
|
+
}
|
|
10
|
+
class OnePasswordError extends Data.TaggedError("OnePasswordError") {
|
|
11
|
+
}
|
|
12
|
+
class GitHubApiError extends Data.TaggedError("GitHubApiError") {
|
|
13
|
+
}
|
|
14
|
+
class SyncError extends Data.TaggedError("SyncError") {
|
|
15
|
+
}
|
|
16
|
+
function encryptSecret(publicKey, secretValue) {
|
|
17
|
+
const messageBytes = Buffer.from(secretValue);
|
|
18
|
+
const publicKeyBytes = Buffer.from(publicKey, "base64");
|
|
19
|
+
const ephemeralKeyPair = tweetnacl.box.keyPair();
|
|
20
|
+
const nonceInput = new Uint8Array(64);
|
|
21
|
+
nonceInput.set(ephemeralKeyPair.publicKey);
|
|
22
|
+
nonceInput.set(publicKeyBytes, 32);
|
|
23
|
+
const nonce = blake2b(nonceInput, void 0, 24);
|
|
24
|
+
const ciphertext = tweetnacl.box(messageBytes, nonce, publicKeyBytes, ephemeralKeyPair.secretKey);
|
|
25
|
+
const sealed = new Uint8Array(ephemeralKeyPair.publicKey.length + ciphertext.length);
|
|
26
|
+
sealed.set(ephemeralKeyPair.publicKey);
|
|
27
|
+
sealed.set(ciphertext, ephemeralKeyPair.publicKey.length);
|
|
28
|
+
return Buffer.from(sealed).toString("base64");
|
|
29
|
+
}
|
|
30
|
+
const ResourceFileKind = Schema.Struct({
|
|
31
|
+
file: Schema.Record({
|
|
32
|
+
key: Schema.String,
|
|
33
|
+
value: Schema.String
|
|
34
|
+
}).annotations({
|
|
35
|
+
title: "File entries",
|
|
36
|
+
description: "Named entries with file path values, resolved relative to config directory",
|
|
37
|
+
jsonSchema: {
|
|
38
|
+
"x-tombi-additional-key-label": "name"
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
const ResourceValueKind = Schema.Struct({
|
|
43
|
+
value: Schema.Record({
|
|
44
|
+
key: Schema.String,
|
|
45
|
+
value: Schema.Union(Schema.String, Schema.Record({
|
|
46
|
+
key: Schema.String,
|
|
47
|
+
value: Schema.Unknown
|
|
48
|
+
}))
|
|
49
|
+
}).annotations({
|
|
50
|
+
title: "Value entries",
|
|
51
|
+
description: "Named entries with inline values. Strings used as-is, objects JSON-stringified.",
|
|
52
|
+
jsonSchema: {
|
|
53
|
+
"x-tombi-additional-key-label": "name"
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
});
|
|
57
|
+
const ResourceResolvedKind = Schema.Struct({
|
|
58
|
+
resolved: Schema.Record({
|
|
59
|
+
key: Schema.String,
|
|
60
|
+
value: Schema.String
|
|
61
|
+
}).annotations({
|
|
62
|
+
title: "Resolved entries",
|
|
63
|
+
description: "Named entries mapped to credential labels. Values come from the active credential profile.",
|
|
64
|
+
jsonSchema: {
|
|
65
|
+
"x-tombi-additional-key-label": "name"
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
});
|
|
69
|
+
const SecretGroupSchema = Schema.Union(ResourceFileKind, ResourceValueKind, ResourceResolvedKind).annotations({
|
|
70
|
+
identifier: "SecretGroup",
|
|
71
|
+
title: "Secret group",
|
|
72
|
+
description: "A group of secrets. Must be exactly one kind: file, value, or resolved."
|
|
73
|
+
});
|
|
74
|
+
const VariableGroupSchema = Schema.Union(ResourceFileKind, ResourceValueKind, ResourceResolvedKind).annotations({
|
|
75
|
+
identifier: "VariableGroup",
|
|
76
|
+
title: "Variable group",
|
|
77
|
+
description: "A group of variables. Must be exactly one kind: file, value, or resolved."
|
|
78
|
+
});
|
|
79
|
+
const CleanupScopeSchema = Schema.Union(Schema.Boolean, Schema.Struct({
|
|
80
|
+
preserve: Schema.Array(Schema.String).annotations({
|
|
81
|
+
title: "Preserve list",
|
|
82
|
+
description: "Resource names that should never be deleted during cleanup",
|
|
83
|
+
examples: [
|
|
84
|
+
[
|
|
85
|
+
"LEGACY_TOKEN",
|
|
86
|
+
"DEPLOY_KEY"
|
|
87
|
+
]
|
|
88
|
+
]
|
|
89
|
+
})
|
|
90
|
+
})).annotations({
|
|
91
|
+
identifier: "CleanupScope",
|
|
92
|
+
title: "Cleanup scope",
|
|
93
|
+
description: "Controls cleanup for a single resource scope. false disables cleanup, true enables full cleanup, or specify names to preserve."
|
|
94
|
+
});
|
|
95
|
+
const CleanupSecretsSchema = Schema.Struct({
|
|
96
|
+
actions: Schema.optionalWith(CleanupScopeSchema, {
|
|
97
|
+
default: ()=>false
|
|
98
|
+
}).annotations({
|
|
99
|
+
title: "Clean up Actions secrets",
|
|
100
|
+
description: "Delete Actions secrets not declared in any referenced secret group",
|
|
101
|
+
default: false
|
|
102
|
+
}),
|
|
103
|
+
dependabot: Schema.optionalWith(CleanupScopeSchema, {
|
|
104
|
+
default: ()=>false
|
|
105
|
+
}).annotations({
|
|
106
|
+
title: "Clean up Dependabot secrets",
|
|
107
|
+
description: "Delete Dependabot secrets not declared in any referenced secret group",
|
|
108
|
+
default: false
|
|
109
|
+
}),
|
|
110
|
+
codespaces: Schema.optionalWith(CleanupScopeSchema, {
|
|
111
|
+
default: ()=>false
|
|
112
|
+
}).annotations({
|
|
113
|
+
title: "Clean up Codespaces secrets",
|
|
114
|
+
description: "Delete Codespaces secrets not declared in any referenced secret group",
|
|
115
|
+
default: false
|
|
116
|
+
}),
|
|
117
|
+
environments: Schema.optionalWith(CleanupScopeSchema, {
|
|
118
|
+
default: ()=>false
|
|
119
|
+
}).annotations({
|
|
120
|
+
title: "Clean up environment secrets",
|
|
121
|
+
description: "Delete environment secrets not declared in any referenced secret group",
|
|
122
|
+
default: false
|
|
123
|
+
})
|
|
124
|
+
}).annotations({
|
|
125
|
+
identifier: "CleanupSecrets",
|
|
126
|
+
title: "Secrets cleanup configuration",
|
|
127
|
+
description: "Controls deletion of secrets by scope (Actions, Dependabot, Codespaces, environments)."
|
|
128
|
+
});
|
|
129
|
+
const CleanupVariablesSchema = Schema.Struct({
|
|
130
|
+
actions: Schema.optionalWith(CleanupScopeSchema, {
|
|
131
|
+
default: ()=>false
|
|
132
|
+
}).annotations({
|
|
133
|
+
title: "Clean up Actions variables",
|
|
134
|
+
description: "Delete Actions variables not declared in any referenced variable group",
|
|
135
|
+
default: false
|
|
136
|
+
}),
|
|
137
|
+
environments: Schema.optionalWith(CleanupScopeSchema, {
|
|
138
|
+
default: ()=>false
|
|
139
|
+
}).annotations({
|
|
140
|
+
title: "Clean up environment variables",
|
|
141
|
+
description: "Delete environment variables not declared in any referenced variable group",
|
|
142
|
+
default: false
|
|
143
|
+
})
|
|
144
|
+
}).annotations({
|
|
145
|
+
identifier: "CleanupVariables",
|
|
146
|
+
title: "Variables cleanup configuration",
|
|
147
|
+
description: "Controls deletion of variables by scope (Actions, environments)."
|
|
148
|
+
});
|
|
149
|
+
const CleanupSchema = Schema.Struct({
|
|
150
|
+
secrets: Schema.optionalWith(CleanupSecretsSchema, {
|
|
151
|
+
default: ()=>({
|
|
152
|
+
actions: false,
|
|
153
|
+
dependabot: false,
|
|
154
|
+
codespaces: false,
|
|
155
|
+
environments: false
|
|
156
|
+
})
|
|
157
|
+
}).annotations({
|
|
158
|
+
title: "Secrets cleanup",
|
|
159
|
+
description: "Controls cleanup of secrets by scope"
|
|
160
|
+
}),
|
|
161
|
+
variables: Schema.optionalWith(CleanupVariablesSchema, {
|
|
162
|
+
default: ()=>({
|
|
163
|
+
actions: false,
|
|
164
|
+
environments: false
|
|
165
|
+
})
|
|
166
|
+
}).annotations({
|
|
167
|
+
title: "Variables cleanup",
|
|
168
|
+
description: "Controls cleanup of variables by scope"
|
|
169
|
+
}),
|
|
170
|
+
rulesets: Schema.optionalWith(CleanupScopeSchema, {
|
|
171
|
+
default: ()=>false
|
|
172
|
+
}).annotations({
|
|
173
|
+
title: "Clean up rulesets",
|
|
174
|
+
description: "Delete repository rulesets not declared in any referenced ruleset group",
|
|
175
|
+
default: false
|
|
176
|
+
}),
|
|
177
|
+
environments: Schema.optionalWith(CleanupScopeSchema, {
|
|
178
|
+
default: ()=>false
|
|
179
|
+
}).annotations({
|
|
180
|
+
title: "Clean up environments",
|
|
181
|
+
description: "Delete repository environments not declared in config",
|
|
182
|
+
default: false
|
|
183
|
+
})
|
|
184
|
+
}).annotations({
|
|
185
|
+
identifier: "Cleanup",
|
|
186
|
+
title: "Cleanup configuration",
|
|
187
|
+
description: "Controls deletion of resources not declared in config. All disabled by default."
|
|
188
|
+
});
|
|
189
|
+
const ReviewerTypeSchema = Schema.Literal("User", "Team").annotations({
|
|
190
|
+
title: "Reviewer type",
|
|
191
|
+
description: "Whether the reviewer is an individual user or a team"
|
|
192
|
+
});
|
|
193
|
+
const ReviewerSchema = Schema.Struct({
|
|
194
|
+
type: ReviewerTypeSchema,
|
|
195
|
+
id: Schema.Int.annotations({
|
|
196
|
+
title: "Reviewer ID",
|
|
197
|
+
description: "The ID of the user or team"
|
|
198
|
+
})
|
|
199
|
+
}).annotations({
|
|
200
|
+
identifier: "Reviewer",
|
|
201
|
+
title: "Reviewer",
|
|
202
|
+
description: "A user or team required to review deployments"
|
|
203
|
+
});
|
|
204
|
+
const DeploymentBranchPolicySchema = Schema.Struct({
|
|
205
|
+
name: Schema.String.annotations({
|
|
206
|
+
title: "Pattern",
|
|
207
|
+
description: "The name pattern (branch name, tag name, or glob) to allow deployments from"
|
|
208
|
+
}),
|
|
209
|
+
type: Schema.optionalWith(Schema.Literal("branch", "tag").annotations({
|
|
210
|
+
title: "Policy type",
|
|
211
|
+
description: 'Whether this policy matches branches or tags. Defaults to "branch".'
|
|
212
|
+
}), {
|
|
213
|
+
default: ()=>"branch"
|
|
214
|
+
})
|
|
215
|
+
}).annotations({
|
|
216
|
+
identifier: "DeploymentBranchPolicy",
|
|
217
|
+
title: "Deployment branch policy",
|
|
218
|
+
description: "A custom branch or tag pattern that deployments are allowed from"
|
|
219
|
+
});
|
|
220
|
+
const DeploymentBranchesSchema = Schema.Union(Schema.Literal("all", "protected").annotations({
|
|
221
|
+
title: "Deployment branch preset",
|
|
222
|
+
description: '"all" allows any branch, "protected" allows only protected branches'
|
|
223
|
+
}), Schema.Array(DeploymentBranchPolicySchema).annotations({
|
|
224
|
+
title: "Custom deployment policies",
|
|
225
|
+
description: "Array of branch or tag name patterns allowed to deploy to this environment"
|
|
226
|
+
})).annotations({
|
|
227
|
+
identifier: "DeploymentBranches",
|
|
228
|
+
title: "Deployment branches",
|
|
229
|
+
description: 'Controls which branches can deploy. Use "all", "protected", or a list of custom policies.'
|
|
230
|
+
});
|
|
231
|
+
const EnvironmentSchema = Schema.Struct({
|
|
232
|
+
wait_timer: Schema.optional(Schema.Int.pipe(Schema.between(0, 43200)).annotations({
|
|
233
|
+
title: "Wait timer (minutes)",
|
|
234
|
+
description: "Number of minutes to wait before allowing deployments to proceed (0-43200)"
|
|
235
|
+
})),
|
|
236
|
+
prevent_self_review: Schema.optional(Schema.Boolean.annotations({
|
|
237
|
+
title: "Prevent self-review",
|
|
238
|
+
description: "Prevent the user who triggered the deployment from approving it"
|
|
239
|
+
})),
|
|
240
|
+
reviewers: Schema.optional(Schema.Array(ReviewerSchema).annotations({
|
|
241
|
+
title: "Required reviewers",
|
|
242
|
+
description: "Users or teams required to approve deployments to this environment"
|
|
243
|
+
})),
|
|
244
|
+
deployment_branches: Schema.optional(DeploymentBranchesSchema)
|
|
245
|
+
}).annotations({
|
|
246
|
+
identifier: "Environment",
|
|
247
|
+
title: "Deployment environment",
|
|
248
|
+
description: "Configuration for a GitHub deployment environment",
|
|
249
|
+
jsonSchema: {
|
|
250
|
+
"x-tombi-table-keys-order": "schema"
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
const ResolvedRefSchema = Schema.Struct({
|
|
254
|
+
resolved: Schema.String.annotations({
|
|
255
|
+
title: "Credential label",
|
|
256
|
+
description: "Reference to a named value in the active credential profile's resolve section"
|
|
257
|
+
})
|
|
258
|
+
}).annotations({
|
|
259
|
+
identifier: "ResolvedRef",
|
|
260
|
+
title: "Resolved reference",
|
|
261
|
+
description: "A reference to a credential-resolved value"
|
|
262
|
+
});
|
|
263
|
+
const ActorTypeSchema = Schema.Literal("Integration", "OrganizationAdmin", "RepositoryRole", "Team", "DeployKey").annotations({
|
|
264
|
+
title: "Actor type",
|
|
265
|
+
description: "The type of actor that can bypass a ruleset"
|
|
266
|
+
});
|
|
267
|
+
const BypassModeSchema = Schema.Literal("always", "pull_request", "exempt").annotations({
|
|
268
|
+
title: "Bypass mode",
|
|
269
|
+
description: "When the specified actor can bypass the ruleset"
|
|
270
|
+
});
|
|
271
|
+
const BypassActorSchema = Schema.Struct({
|
|
272
|
+
actor_id: Schema.optional(Schema.Union(Schema.Int, ResolvedRefSchema).annotations({
|
|
273
|
+
title: "Actor ID",
|
|
274
|
+
description: "The ID of the actor, or a { resolved } reference to a credential label."
|
|
275
|
+
})),
|
|
276
|
+
actor_type: ActorTypeSchema,
|
|
277
|
+
bypass_mode: Schema.optionalWith(BypassModeSchema, {
|
|
278
|
+
default: ()=>"always"
|
|
279
|
+
})
|
|
280
|
+
}).annotations({
|
|
281
|
+
identifier: "BypassActor",
|
|
282
|
+
title: "Bypass actor",
|
|
283
|
+
description: "An actor that can bypass rules in a ruleset"
|
|
284
|
+
});
|
|
285
|
+
const RefNameConditionSchema = Schema.Struct({
|
|
286
|
+
include: Schema.optionalWith(Schema.Array(Schema.String).annotations({
|
|
287
|
+
title: "Include patterns",
|
|
288
|
+
description: "Ref name patterns to include. Accepts ~DEFAULT_BRANCH, ~ALL, or glob patterns.",
|
|
289
|
+
examples: [
|
|
290
|
+
[
|
|
291
|
+
"~DEFAULT_BRANCH"
|
|
292
|
+
]
|
|
293
|
+
]
|
|
294
|
+
}), {
|
|
295
|
+
default: ()=>[]
|
|
296
|
+
}),
|
|
297
|
+
exclude: Schema.optionalWith(Schema.Array(Schema.String).annotations({
|
|
298
|
+
title: "Exclude patterns",
|
|
299
|
+
description: "Ref name patterns to exclude"
|
|
300
|
+
}), {
|
|
301
|
+
default: ()=>[]
|
|
302
|
+
})
|
|
303
|
+
}).annotations({
|
|
304
|
+
identifier: "RefNameCondition",
|
|
305
|
+
title: "Ref name condition",
|
|
306
|
+
description: "Conditions for matching ref names (branches or tags)"
|
|
307
|
+
});
|
|
308
|
+
const RulesetConditionsSchema = Schema.Struct({
|
|
309
|
+
ref_name: Schema.optional(RefNameConditionSchema)
|
|
310
|
+
}).annotations({
|
|
311
|
+
identifier: "RulesetConditions",
|
|
312
|
+
title: "Ruleset conditions",
|
|
313
|
+
description: "Conditions that determine when the ruleset applies"
|
|
314
|
+
});
|
|
315
|
+
const RequiredReviewerSchema = Schema.Struct({
|
|
316
|
+
file_patterns: Schema.Array(Schema.String).annotations({
|
|
317
|
+
description: "File patterns this reviewer must approve (fnmatch syntax)"
|
|
318
|
+
}),
|
|
319
|
+
minimum_approvals: Schema.Int.annotations({
|
|
320
|
+
description: "Minimum approvals required from this team (0 = optional)"
|
|
321
|
+
}),
|
|
322
|
+
reviewer: Schema.Struct({
|
|
323
|
+
id: Schema.Int.annotations({
|
|
324
|
+
description: "Team ID"
|
|
325
|
+
}),
|
|
326
|
+
type: Schema.Literal("Team")
|
|
327
|
+
})
|
|
328
|
+
});
|
|
329
|
+
const StatusCheckSchema = Schema.Struct({
|
|
330
|
+
context: Schema.String.annotations({
|
|
331
|
+
description: "The status check context name that must be present on the commit"
|
|
332
|
+
}),
|
|
333
|
+
integration_id: Schema.optional(Schema.Union(Schema.Int, ResolvedRefSchema).annotations({
|
|
334
|
+
description: "The integration ID, or a { resolved } reference to a credential label"
|
|
335
|
+
}))
|
|
336
|
+
});
|
|
337
|
+
const WorkflowFileSchema = Schema.Struct({
|
|
338
|
+
path: Schema.String.annotations({
|
|
339
|
+
description: "Path to the workflow file"
|
|
340
|
+
}),
|
|
341
|
+
ref: Schema.optional(Schema.String.annotations({
|
|
342
|
+
description: "Branch or tag of the workflow file"
|
|
343
|
+
})),
|
|
344
|
+
repository_id: Schema.Union(Schema.Int, ResolvedRefSchema).annotations({
|
|
345
|
+
description: "Repository ID, or a { resolved } reference to a credential label"
|
|
346
|
+
}),
|
|
347
|
+
sha: Schema.optional(Schema.String.annotations({
|
|
348
|
+
description: "Commit SHA of the workflow file"
|
|
349
|
+
}))
|
|
350
|
+
});
|
|
351
|
+
const TargetPatternSchema = Schema.Union(Schema.Struct({
|
|
352
|
+
include: Schema.String.annotations({
|
|
353
|
+
title: "Include pattern",
|
|
354
|
+
description: "Glob pattern to include"
|
|
355
|
+
})
|
|
356
|
+
}), Schema.Struct({
|
|
357
|
+
exclude: Schema.String.annotations({
|
|
358
|
+
title: "Exclude pattern",
|
|
359
|
+
description: "Glob pattern to exclude"
|
|
360
|
+
})
|
|
361
|
+
})).annotations({
|
|
362
|
+
identifier: "TargetPattern",
|
|
363
|
+
title: "Target pattern",
|
|
364
|
+
description: "An include or exclude pattern for ref matching"
|
|
365
|
+
});
|
|
366
|
+
const TargetsSchema = Schema.Union(Schema.Literal("default", "all").annotations({
|
|
367
|
+
title: "Target preset",
|
|
368
|
+
description: "'default' targets the default branch; 'all' targets all branches/tags"
|
|
369
|
+
}), Schema.Array(TargetPatternSchema).annotations({
|
|
370
|
+
title: "Custom target patterns",
|
|
371
|
+
description: "Array of include/exclude patterns for fine-grained ref targeting"
|
|
372
|
+
})).annotations({
|
|
373
|
+
identifier: "Targets",
|
|
374
|
+
title: "Targets shorthand",
|
|
375
|
+
description: "Shorthand for specifying ref_name conditions: 'default', 'all', or custom patterns"
|
|
376
|
+
});
|
|
377
|
+
const PullRequestsShorthandSchema = Schema.Struct({
|
|
378
|
+
approvals: Schema.optionalWith(Schema.Int.pipe(Schema.between(0, 10)).annotations({
|
|
379
|
+
title: "Required approvals",
|
|
380
|
+
description: "Number of approving reviews required (0-10)"
|
|
381
|
+
}), {
|
|
382
|
+
default: ()=>0
|
|
383
|
+
}),
|
|
384
|
+
dismiss_stale_reviews: Schema.optionalWith(Schema.Boolean.annotations({
|
|
385
|
+
title: "Dismiss stale reviews",
|
|
386
|
+
description: "Dismiss previous approvals when new commits are pushed"
|
|
387
|
+
}), {
|
|
388
|
+
default: ()=>false
|
|
389
|
+
}),
|
|
390
|
+
code_owner_review: Schema.optionalWith(Schema.Boolean.annotations({
|
|
391
|
+
title: "Code owner review",
|
|
392
|
+
description: "Require review from code owners for files they own"
|
|
393
|
+
}), {
|
|
394
|
+
default: ()=>false
|
|
395
|
+
}),
|
|
396
|
+
last_push_approval: Schema.optionalWith(Schema.Boolean.annotations({
|
|
397
|
+
title: "Last push approval",
|
|
398
|
+
description: "Most recent push must be approved by someone other than the pusher"
|
|
399
|
+
}), {
|
|
400
|
+
default: ()=>false
|
|
401
|
+
}),
|
|
402
|
+
resolve_threads: Schema.optionalWith(Schema.Boolean.annotations({
|
|
403
|
+
title: "Resolve threads",
|
|
404
|
+
description: "All review conversations must be resolved before merging"
|
|
405
|
+
}), {
|
|
406
|
+
default: ()=>false
|
|
407
|
+
}),
|
|
408
|
+
merge_methods: Schema.optional(Schema.Array(Schema.Literal("merge", "squash", "rebase")).annotations({
|
|
409
|
+
title: "Merge methods",
|
|
410
|
+
description: "Allowed merge methods. At least one must be enabled."
|
|
411
|
+
})),
|
|
412
|
+
reviewers: Schema.optional(Schema.Array(RequiredReviewerSchema).annotations({
|
|
413
|
+
title: "Required reviewers",
|
|
414
|
+
description: "Teams that must approve specific file patterns"
|
|
415
|
+
}))
|
|
416
|
+
}).annotations({
|
|
417
|
+
identifier: "PullRequestsShorthand",
|
|
418
|
+
title: "Pull requests shorthand",
|
|
419
|
+
description: "Simplified pull request configuration (branch rulesets only)"
|
|
420
|
+
});
|
|
421
|
+
const StatusChecksShorthandSchema = Schema.Struct({
|
|
422
|
+
update_branch: Schema.optional(Schema.Boolean.annotations({
|
|
423
|
+
title: "Strict status checks",
|
|
424
|
+
description: "PRs must be tested with the latest code"
|
|
425
|
+
})),
|
|
426
|
+
on_creation: Schema.optional(Schema.Boolean.annotations({
|
|
427
|
+
title: "Enforce on create",
|
|
428
|
+
description: "When false, allows branch creation even if checks would prohibit it"
|
|
429
|
+
})),
|
|
430
|
+
default_integration_id: Schema.optional(Schema.Union(Schema.Int, ResolvedRefSchema).annotations({
|
|
431
|
+
title: "Default integration ID",
|
|
432
|
+
description: "Default integration ID applied to all checks that do not specify one"
|
|
433
|
+
})),
|
|
434
|
+
required: Schema.Array(StatusCheckSchema).annotations({
|
|
435
|
+
title: "Required checks",
|
|
436
|
+
description: "Status checks that must pass"
|
|
437
|
+
})
|
|
438
|
+
}).annotations({
|
|
439
|
+
identifier: "StatusChecksShorthand",
|
|
440
|
+
title: "Status checks shorthand",
|
|
441
|
+
description: "Simplified status checks configuration"
|
|
442
|
+
});
|
|
443
|
+
const EnforcementSchema = Schema.Literal("disabled", "active", "evaluate").annotations({
|
|
444
|
+
title: "Enforcement level",
|
|
445
|
+
description: "disabled = off, active = enforced, evaluate = test mode (GitHub Enterprise only)"
|
|
446
|
+
});
|
|
447
|
+
const PatternEntrySchema = Schema.Struct({
|
|
448
|
+
operator: Schema.Literal("starts_with", "ends_with", "contains", "regex").annotations({
|
|
449
|
+
title: "Operator",
|
|
450
|
+
description: "The operator to use for matching"
|
|
451
|
+
}),
|
|
452
|
+
pattern: Schema.String.annotations({
|
|
453
|
+
title: "Pattern",
|
|
454
|
+
description: "The pattern to match"
|
|
455
|
+
}),
|
|
456
|
+
name: Schema.optional(Schema.String.annotations({
|
|
457
|
+
title: "Rule name",
|
|
458
|
+
description: "Display name for this pattern rule"
|
|
459
|
+
})),
|
|
460
|
+
negate: Schema.optional(Schema.Boolean.annotations({
|
|
461
|
+
title: "Negate",
|
|
462
|
+
description: "If true, the rule fails when the pattern matches"
|
|
463
|
+
}))
|
|
464
|
+
}).annotations({
|
|
465
|
+
identifier: "PatternEntry",
|
|
466
|
+
title: "Pattern entry",
|
|
467
|
+
description: "A pattern matching rule with operator, pattern, and optional name/negate"
|
|
468
|
+
});
|
|
469
|
+
const MergeQueueShorthandSchema = Schema.Struct({
|
|
470
|
+
check_timeout: Schema.Int.pipe(Schema.between(1, 360)).annotations({
|
|
471
|
+
title: "Check timeout (minutes)",
|
|
472
|
+
description: "Max time for status checks to report"
|
|
473
|
+
}),
|
|
474
|
+
grouping: Schema.Literal("ALLGREEN", "HEADGREEN").annotations({
|
|
475
|
+
title: "Grouping strategy",
|
|
476
|
+
description: "Whether all commits or only the head commit must pass checks"
|
|
477
|
+
}),
|
|
478
|
+
max_build: Schema.Int.pipe(Schema.between(0, 100)).annotations({
|
|
479
|
+
title: "Max entries to build",
|
|
480
|
+
description: "Max queued PRs requesting checks simultaneously"
|
|
481
|
+
}),
|
|
482
|
+
max_merge: Schema.Int.pipe(Schema.between(0, 100)).annotations({
|
|
483
|
+
title: "Max entries to merge",
|
|
484
|
+
description: "Max PRs merged together in a group"
|
|
485
|
+
}),
|
|
486
|
+
merge_method: Schema.Literal("MERGE", "SQUASH", "REBASE").annotations({
|
|
487
|
+
title: "Merge method",
|
|
488
|
+
description: "Merge method for queued PRs"
|
|
489
|
+
}),
|
|
490
|
+
min_merge: Schema.Int.pipe(Schema.between(0, 100)).annotations({
|
|
491
|
+
title: "Min entries to merge",
|
|
492
|
+
description: "Min PRs merged together in a group"
|
|
493
|
+
}),
|
|
494
|
+
min_wait: Schema.Int.pipe(Schema.between(0, 360)).annotations({
|
|
495
|
+
title: "Min wait time (minutes)",
|
|
496
|
+
description: "Wait time for min group size after first PR is added"
|
|
497
|
+
})
|
|
498
|
+
}).annotations({
|
|
499
|
+
identifier: "MergeQueueShorthand",
|
|
500
|
+
title: "Merge queue",
|
|
501
|
+
description: "Merge queue configuration"
|
|
502
|
+
});
|
|
503
|
+
const CopilotReviewShorthandSchema = Schema.Struct({
|
|
504
|
+
draft_prs: Schema.optional(Schema.Boolean.annotations({
|
|
505
|
+
title: "Review draft PRs",
|
|
506
|
+
description: "Review draft PRs before they are marked ready"
|
|
507
|
+
})),
|
|
508
|
+
on_push: Schema.optional(Schema.Boolean.annotations({
|
|
509
|
+
title: "Review on push",
|
|
510
|
+
description: "Review each new push to the PR"
|
|
511
|
+
}))
|
|
512
|
+
}).annotations({
|
|
513
|
+
identifier: "CopilotReviewShorthand",
|
|
514
|
+
title: "Copilot review",
|
|
515
|
+
description: "Copilot code review configuration"
|
|
516
|
+
});
|
|
517
|
+
const CodeScanningEntrySchema = Schema.Struct({
|
|
518
|
+
tool: Schema.String.annotations({
|
|
519
|
+
title: "Tool name",
|
|
520
|
+
description: "Name of the code scanning tool"
|
|
521
|
+
}),
|
|
522
|
+
alerts: Schema.Literal("none", "errors", "errors_and_warnings", "all").annotations({
|
|
523
|
+
title: "Alerts threshold",
|
|
524
|
+
description: "Severity level at which alerts block updates"
|
|
525
|
+
}),
|
|
526
|
+
security_alerts: Schema.Literal("none", "critical", "high_or_higher", "medium_or_higher", "all").annotations({
|
|
527
|
+
title: "Security alerts threshold",
|
|
528
|
+
description: "Severity level at which security alerts block updates"
|
|
529
|
+
})
|
|
530
|
+
}).annotations({
|
|
531
|
+
identifier: "CodeScanningEntry",
|
|
532
|
+
title: "Code scanning tool",
|
|
533
|
+
description: "A code scanning tool with alert thresholds"
|
|
534
|
+
});
|
|
535
|
+
const WorkflowsShorthandSchema = Schema.Struct({
|
|
536
|
+
on_creation: Schema.optional(Schema.Boolean.annotations({
|
|
537
|
+
title: "Enforce on creation",
|
|
538
|
+
description: "Enforce workflows when a branch is created (false = skip on creation)"
|
|
539
|
+
})),
|
|
540
|
+
required: Schema.Array(WorkflowFileSchema).annotations({
|
|
541
|
+
title: "Required workflows",
|
|
542
|
+
description: "Workflows that must pass for this rule"
|
|
543
|
+
})
|
|
544
|
+
}).annotations({
|
|
545
|
+
identifier: "WorkflowsShorthand",
|
|
546
|
+
title: "Workflows",
|
|
547
|
+
description: "Required workflow configuration"
|
|
548
|
+
});
|
|
549
|
+
const sharedRulesetFields = {
|
|
550
|
+
name: Schema.String.annotations({
|
|
551
|
+
title: "Ruleset name",
|
|
552
|
+
description: "The name of the ruleset (used for matching when creating or updating)"
|
|
553
|
+
}),
|
|
554
|
+
enforcement: EnforcementSchema,
|
|
555
|
+
conditions: Schema.optional(RulesetConditionsSchema),
|
|
556
|
+
bypass_actors: Schema.optional(Schema.Array(BypassActorSchema)),
|
|
557
|
+
creation: Schema.optional(Schema.Boolean.annotations({
|
|
558
|
+
title: "Creation shorthand",
|
|
559
|
+
description: "When true, adds a creation rule"
|
|
560
|
+
})),
|
|
561
|
+
update: Schema.optional(Schema.Boolean.annotations({
|
|
562
|
+
title: "Update shorthand",
|
|
563
|
+
description: "When true, adds an update rule with update_allows_fetch_and_merge: true"
|
|
564
|
+
})),
|
|
565
|
+
deletion: Schema.optional(Schema.Boolean.annotations({
|
|
566
|
+
title: "Deletion shorthand",
|
|
567
|
+
description: "When true, adds a deletion rule"
|
|
568
|
+
})),
|
|
569
|
+
required_linear_history: Schema.optional(Schema.Boolean.annotations({
|
|
570
|
+
title: "Required linear history shorthand",
|
|
571
|
+
description: "When true, adds a required_linear_history rule"
|
|
572
|
+
})),
|
|
573
|
+
required_signatures: Schema.optional(Schema.Boolean.annotations({
|
|
574
|
+
title: "Required signatures shorthand",
|
|
575
|
+
description: "When true, adds a required_signatures rule"
|
|
576
|
+
})),
|
|
577
|
+
non_fast_forward: Schema.optional(Schema.Boolean.annotations({
|
|
578
|
+
title: "Non-fast-forward shorthand",
|
|
579
|
+
description: "When true, adds a non_fast_forward rule"
|
|
580
|
+
})),
|
|
581
|
+
deployments: Schema.optional(Schema.Array(Schema.String).annotations({
|
|
582
|
+
title: "Deployments shorthand",
|
|
583
|
+
description: "Deployment environments that must succeed; converts to required_deployments rule"
|
|
584
|
+
})),
|
|
585
|
+
targets: Schema.optional(TargetsSchema),
|
|
586
|
+
status_checks: Schema.optional(StatusChecksShorthandSchema),
|
|
587
|
+
commit_message: Schema.optional(Schema.Array(PatternEntrySchema).annotations({
|
|
588
|
+
title: "Commit message patterns",
|
|
589
|
+
description: "Commit message pattern rules"
|
|
590
|
+
})),
|
|
591
|
+
commit_author_email: Schema.optional(Schema.Array(PatternEntrySchema).annotations({
|
|
592
|
+
title: "Commit author email patterns",
|
|
593
|
+
description: "Commit author email pattern rules"
|
|
594
|
+
})),
|
|
595
|
+
committer_email: Schema.optional(Schema.Array(PatternEntrySchema).annotations({
|
|
596
|
+
title: "Committer email patterns",
|
|
597
|
+
description: "Committer email pattern rules"
|
|
598
|
+
}))
|
|
599
|
+
};
|
|
600
|
+
const BranchRulesetSchema = Schema.Struct({
|
|
601
|
+
...sharedRulesetFields,
|
|
602
|
+
type: Schema.Literal("branch").annotations({
|
|
603
|
+
title: "Ruleset type",
|
|
604
|
+
description: "This ruleset applies to branches"
|
|
605
|
+
}),
|
|
606
|
+
pull_requests: Schema.optional(PullRequestsShorthandSchema),
|
|
607
|
+
merge_queue: Schema.optional(MergeQueueShorthandSchema),
|
|
608
|
+
copilot_review: Schema.optional(CopilotReviewShorthandSchema),
|
|
609
|
+
code_scanning: Schema.optional(Schema.Array(CodeScanningEntrySchema).annotations({
|
|
610
|
+
title: "Code scanning tools",
|
|
611
|
+
description: "Code scanning tool requirements"
|
|
612
|
+
})),
|
|
613
|
+
workflows: Schema.optional(WorkflowsShorthandSchema),
|
|
614
|
+
branch_name: Schema.optional(Schema.Array(PatternEntrySchema).annotations({
|
|
615
|
+
title: "Branch name patterns",
|
|
616
|
+
description: "Branch name pattern rules"
|
|
617
|
+
}))
|
|
618
|
+
}).annotations({
|
|
619
|
+
identifier: "BranchRuleset",
|
|
620
|
+
title: "Branch ruleset",
|
|
621
|
+
description: "A ruleset that applies to branches",
|
|
622
|
+
jsonSchema: {
|
|
623
|
+
"x-tombi-table-keys-order": "schema"
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
const TagRulesetSchema = Schema.Struct({
|
|
627
|
+
...sharedRulesetFields,
|
|
628
|
+
type: Schema.Literal("tag").annotations({
|
|
629
|
+
title: "Ruleset type",
|
|
630
|
+
description: "This ruleset applies to tags"
|
|
631
|
+
}),
|
|
632
|
+
tag_name: Schema.optional(Schema.Array(PatternEntrySchema).annotations({
|
|
633
|
+
title: "Tag name patterns",
|
|
634
|
+
description: "Tag name pattern rules"
|
|
635
|
+
}))
|
|
636
|
+
}).annotations({
|
|
637
|
+
identifier: "TagRuleset",
|
|
638
|
+
title: "Tag ruleset",
|
|
639
|
+
description: "A ruleset that applies to tags",
|
|
640
|
+
jsonSchema: {
|
|
641
|
+
"x-tombi-table-keys-order": "schema"
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
const RulesetSchema = Schema.Union(BranchRulesetSchema, TagRulesetSchema).annotations({
|
|
645
|
+
identifier: "Ruleset",
|
|
646
|
+
title: "Repository ruleset",
|
|
647
|
+
description: "A set of rules to apply when specified conditions are met",
|
|
648
|
+
jsonSchema: {
|
|
649
|
+
"x-tombi-table-keys-order": "schema"
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
function buildRulesetPayload(ruleset) {
|
|
653
|
+
const rules = [];
|
|
654
|
+
if (true === ruleset.creation) rules.push({
|
|
655
|
+
type: "creation"
|
|
656
|
+
});
|
|
657
|
+
if (true === ruleset.update) rules.push({
|
|
658
|
+
type: "update",
|
|
659
|
+
parameters: {
|
|
660
|
+
update_allows_fetch_and_merge: true
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
if (true === ruleset.deletion) rules.push({
|
|
664
|
+
type: "deletion"
|
|
665
|
+
});
|
|
666
|
+
if (true === ruleset.required_linear_history) rules.push({
|
|
667
|
+
type: "required_linear_history"
|
|
668
|
+
});
|
|
669
|
+
if (true === ruleset.required_signatures) rules.push({
|
|
670
|
+
type: "required_signatures"
|
|
671
|
+
});
|
|
672
|
+
if (true === ruleset.non_fast_forward) rules.push({
|
|
673
|
+
type: "non_fast_forward"
|
|
674
|
+
});
|
|
675
|
+
if (void 0 !== ruleset.deployments && ruleset.deployments.length > 0) rules.push({
|
|
676
|
+
type: "required_deployments",
|
|
677
|
+
parameters: {
|
|
678
|
+
required_deployment_environments: ruleset.deployments
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
if ("branch" === ruleset.type && void 0 !== ruleset.pull_requests) {
|
|
682
|
+
const pr = ruleset.pull_requests;
|
|
683
|
+
rules.push({
|
|
684
|
+
type: "pull_request",
|
|
685
|
+
parameters: {
|
|
686
|
+
required_approving_review_count: pr.approvals,
|
|
687
|
+
dismiss_stale_reviews_on_push: pr.dismiss_stale_reviews,
|
|
688
|
+
require_code_owner_review: pr.code_owner_review,
|
|
689
|
+
require_last_push_approval: pr.last_push_approval,
|
|
690
|
+
required_review_thread_resolution: pr.resolve_threads,
|
|
691
|
+
...void 0 !== pr.merge_methods ? {
|
|
692
|
+
allowed_merge_methods: pr.merge_methods
|
|
693
|
+
} : {},
|
|
694
|
+
...void 0 !== pr.reviewers ? {
|
|
695
|
+
required_reviewers: pr.reviewers
|
|
696
|
+
} : {}
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
if (void 0 !== ruleset.status_checks) {
|
|
701
|
+
const sc = ruleset.status_checks;
|
|
702
|
+
const checks = sc.required.map((check)=>{
|
|
703
|
+
if (void 0 === check.integration_id && void 0 !== sc.default_integration_id) return {
|
|
704
|
+
...check,
|
|
705
|
+
integration_id: sc.default_integration_id
|
|
706
|
+
};
|
|
707
|
+
return check;
|
|
708
|
+
});
|
|
709
|
+
rules.push({
|
|
710
|
+
type: "required_status_checks",
|
|
711
|
+
parameters: {
|
|
712
|
+
strict_required_status_checks_policy: sc.update_branch ?? true,
|
|
713
|
+
...false === sc.on_creation ? {
|
|
714
|
+
do_not_enforce_on_create: true
|
|
715
|
+
} : {},
|
|
716
|
+
required_status_checks: checks
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
if ("branch" === ruleset.type && void 0 !== ruleset.merge_queue) {
|
|
721
|
+
const mq = ruleset.merge_queue;
|
|
722
|
+
rules.push({
|
|
723
|
+
type: "merge_queue",
|
|
724
|
+
parameters: {
|
|
725
|
+
check_response_timeout_minutes: mq.check_timeout,
|
|
726
|
+
grouping_strategy: mq.grouping,
|
|
727
|
+
max_entries_to_build: mq.max_build,
|
|
728
|
+
max_entries_to_merge: mq.max_merge,
|
|
729
|
+
merge_method: mq.merge_method,
|
|
730
|
+
min_entries_to_merge: mq.min_merge,
|
|
731
|
+
min_entries_to_merge_wait_minutes: mq.min_wait
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if ("branch" === ruleset.type && void 0 !== ruleset.copilot_review) {
|
|
736
|
+
const cr = ruleset.copilot_review;
|
|
737
|
+
rules.push({
|
|
738
|
+
type: "copilot_code_review",
|
|
739
|
+
parameters: {
|
|
740
|
+
...void 0 !== cr.draft_prs ? {
|
|
741
|
+
review_draft_pull_requests: cr.draft_prs
|
|
742
|
+
} : {},
|
|
743
|
+
...void 0 !== cr.on_push ? {
|
|
744
|
+
review_on_push: cr.on_push
|
|
745
|
+
} : {}
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
if ("branch" === ruleset.type && void 0 !== ruleset.code_scanning) rules.push({
|
|
750
|
+
type: "code_scanning",
|
|
751
|
+
parameters: {
|
|
752
|
+
code_scanning_tools: ruleset.code_scanning.map((entry)=>({
|
|
753
|
+
tool: entry.tool,
|
|
754
|
+
alerts_threshold: entry.alerts,
|
|
755
|
+
security_alerts_threshold: entry.security_alerts
|
|
756
|
+
}))
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
if ("branch" === ruleset.type && void 0 !== ruleset.workflows) {
|
|
760
|
+
const wf = ruleset.workflows;
|
|
761
|
+
rules.push({
|
|
762
|
+
type: "workflows",
|
|
763
|
+
parameters: {
|
|
764
|
+
...false === wf.on_creation ? {
|
|
765
|
+
do_not_enforce_on_create: true
|
|
766
|
+
} : {},
|
|
767
|
+
workflows: wf.required
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
for (const [field, ruleType] of [
|
|
772
|
+
[
|
|
773
|
+
"commit_message",
|
|
774
|
+
"commit_message_pattern"
|
|
775
|
+
],
|
|
776
|
+
[
|
|
777
|
+
"commit_author_email",
|
|
778
|
+
"commit_author_email_pattern"
|
|
779
|
+
],
|
|
780
|
+
[
|
|
781
|
+
"committer_email",
|
|
782
|
+
"committer_email_pattern"
|
|
783
|
+
]
|
|
784
|
+
]){
|
|
785
|
+
const patterns = ruleset[field];
|
|
786
|
+
if (void 0 !== patterns) for (const entry of patterns)rules.push({
|
|
787
|
+
type: ruleType,
|
|
788
|
+
parameters: entry
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
if ("branch" === ruleset.type && void 0 !== ruleset.branch_name) for (const entry of ruleset.branch_name)rules.push({
|
|
792
|
+
type: "branch_name_pattern",
|
|
793
|
+
parameters: entry
|
|
794
|
+
});
|
|
795
|
+
if ("tag" === ruleset.type && void 0 !== ruleset.tag_name) for (const entry of ruleset.tag_name)rules.push({
|
|
796
|
+
type: "tag_name_pattern",
|
|
797
|
+
parameters: entry
|
|
798
|
+
});
|
|
799
|
+
let conditions = ruleset.conditions;
|
|
800
|
+
if (void 0 !== ruleset.targets) if ("default" === ruleset.targets) conditions = {
|
|
801
|
+
ref_name: {
|
|
802
|
+
include: [
|
|
803
|
+
"~DEFAULT_BRANCH"
|
|
804
|
+
],
|
|
805
|
+
exclude: []
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
else if ("all" === ruleset.targets) conditions = {
|
|
809
|
+
ref_name: {
|
|
810
|
+
include: [
|
|
811
|
+
"~ALL"
|
|
812
|
+
],
|
|
813
|
+
exclude: []
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
else {
|
|
817
|
+
const include = [];
|
|
818
|
+
const exclude = [];
|
|
819
|
+
for (const pattern of ruleset.targets)if ("include" in pattern) include.push(pattern.include);
|
|
820
|
+
else exclude.push(pattern.exclude);
|
|
821
|
+
conditions = {
|
|
822
|
+
ref_name: {
|
|
823
|
+
include,
|
|
824
|
+
exclude
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
name: ruleset.name,
|
|
830
|
+
target: ruleset.type,
|
|
831
|
+
enforcement: ruleset.enforcement,
|
|
832
|
+
...void 0 !== conditions ? {
|
|
833
|
+
conditions
|
|
834
|
+
} : {},
|
|
835
|
+
...void 0 !== ruleset.bypass_actors ? {
|
|
836
|
+
bypass_actors: ruleset.bypass_actors
|
|
837
|
+
} : {},
|
|
838
|
+
...rules.length > 0 ? {
|
|
839
|
+
rules
|
|
840
|
+
} : {}
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
const SecretScopesSchema = Schema.Struct({
|
|
844
|
+
actions: Schema.optional(Schema.Array(Schema.String).annotations({
|
|
845
|
+
title: "Action secret groups",
|
|
846
|
+
description: "Secret groups to sync as GitHub Actions repository secrets",
|
|
847
|
+
examples: [
|
|
848
|
+
[
|
|
849
|
+
"deploy",
|
|
850
|
+
"app"
|
|
851
|
+
]
|
|
852
|
+
]
|
|
853
|
+
})),
|
|
854
|
+
dependabot: Schema.optional(Schema.Array(Schema.String).annotations({
|
|
855
|
+
title: "Dependabot secret groups",
|
|
856
|
+
description: "Secret groups to sync as Dependabot secrets",
|
|
857
|
+
examples: [
|
|
858
|
+
[
|
|
859
|
+
"deploy"
|
|
860
|
+
]
|
|
861
|
+
]
|
|
862
|
+
})),
|
|
863
|
+
codespaces: Schema.optional(Schema.Array(Schema.String).annotations({
|
|
864
|
+
title: "Codespaces secret groups",
|
|
865
|
+
description: "Secret groups to sync as Codespaces secrets",
|
|
866
|
+
examples: [
|
|
867
|
+
[
|
|
868
|
+
"deploy"
|
|
869
|
+
]
|
|
870
|
+
]
|
|
871
|
+
})),
|
|
872
|
+
environments: Schema.optional(Schema.Record({
|
|
873
|
+
key: Schema.String,
|
|
874
|
+
value: Schema.Array(Schema.String).annotations({
|
|
875
|
+
title: "Environment secret groups",
|
|
876
|
+
description: "Secret groups to sync as environment secrets"
|
|
877
|
+
})
|
|
878
|
+
}).annotations({
|
|
879
|
+
title: "Environment secret scopes",
|
|
880
|
+
description: "Map of environment names to secret group references",
|
|
881
|
+
jsonSchema: {
|
|
882
|
+
"x-tombi-additional-key-label": "environment_name"
|
|
883
|
+
}
|
|
884
|
+
}))
|
|
885
|
+
}).annotations({
|
|
886
|
+
identifier: "SecretScopes",
|
|
887
|
+
title: "Secret scopes",
|
|
888
|
+
description: "Assign secret groups to GitHub secret scopes (actions, dependabot, codespaces, environments)"
|
|
889
|
+
});
|
|
890
|
+
const VariableScopesSchema = Schema.Struct({
|
|
891
|
+
actions: Schema.optional(Schema.Array(Schema.String).annotations({
|
|
892
|
+
title: "Action variable groups",
|
|
893
|
+
description: "Variable groups to sync as GitHub Actions repository variables",
|
|
894
|
+
examples: [
|
|
895
|
+
[
|
|
896
|
+
"common"
|
|
897
|
+
]
|
|
898
|
+
]
|
|
899
|
+
})),
|
|
900
|
+
environments: Schema.optional(Schema.Record({
|
|
901
|
+
key: Schema.String,
|
|
902
|
+
value: Schema.Array(Schema.String).annotations({
|
|
903
|
+
title: "Environment variable groups",
|
|
904
|
+
description: "Variable groups to sync as environment variables"
|
|
905
|
+
})
|
|
906
|
+
}).annotations({
|
|
907
|
+
title: "Environment variable scopes",
|
|
908
|
+
description: "Map of environment names to variable group references",
|
|
909
|
+
jsonSchema: {
|
|
910
|
+
"x-tombi-additional-key-label": "environment_name"
|
|
911
|
+
}
|
|
912
|
+
}))
|
|
913
|
+
}).annotations({
|
|
914
|
+
identifier: "VariableScopes",
|
|
915
|
+
title: "Variable scopes",
|
|
916
|
+
description: "Assign variable groups to GitHub variable scopes (actions, environments)"
|
|
917
|
+
});
|
|
918
|
+
const GroupSchema = Schema.Struct({
|
|
919
|
+
owner: Schema.optional(Schema.String.annotations({
|
|
920
|
+
title: "Owner override",
|
|
921
|
+
description: "GitHub user or organization that owns these repos. Overrides the top-level owner.",
|
|
922
|
+
examples: [
|
|
923
|
+
"savvy-web"
|
|
924
|
+
]
|
|
925
|
+
})),
|
|
926
|
+
repos: Schema.Array(Schema.String).annotations({
|
|
927
|
+
title: "Repository names",
|
|
928
|
+
description: "List of repository names (without owner prefix) to sync in this group",
|
|
929
|
+
examples: [
|
|
930
|
+
[
|
|
931
|
+
"repo-one",
|
|
932
|
+
"repo-two",
|
|
933
|
+
"repo-three"
|
|
934
|
+
]
|
|
935
|
+
],
|
|
936
|
+
jsonSchema: {
|
|
937
|
+
"x-tombi-array-values-order": "ascending"
|
|
938
|
+
}
|
|
939
|
+
}),
|
|
940
|
+
credentials: Schema.optional(Schema.String.annotations({
|
|
941
|
+
title: "Credential profile",
|
|
942
|
+
description: "Name of the credential profile to use. If only one profile exists, it is used automatically.",
|
|
943
|
+
examples: [
|
|
944
|
+
"personal",
|
|
945
|
+
"work"
|
|
946
|
+
]
|
|
947
|
+
})),
|
|
948
|
+
settings: Schema.optional(Schema.Array(Schema.String).annotations({
|
|
949
|
+
title: "Settings groups",
|
|
950
|
+
description: "Names of settings groups to apply to these repos",
|
|
951
|
+
examples: [
|
|
952
|
+
[
|
|
953
|
+
"oss-defaults"
|
|
954
|
+
]
|
|
955
|
+
]
|
|
956
|
+
})),
|
|
957
|
+
environments: Schema.optional(Schema.Array(Schema.String).annotations({
|
|
958
|
+
title: "Environments",
|
|
959
|
+
description: "Names of environment definitions to create/update for these repos",
|
|
960
|
+
examples: [
|
|
961
|
+
[
|
|
962
|
+
"staging",
|
|
963
|
+
"production"
|
|
964
|
+
]
|
|
965
|
+
]
|
|
966
|
+
})),
|
|
967
|
+
secrets: Schema.optional(SecretScopesSchema),
|
|
968
|
+
variables: Schema.optional(VariableScopesSchema),
|
|
969
|
+
rulesets: Schema.optional(Schema.Array(Schema.String).annotations({
|
|
970
|
+
title: "Rulesets",
|
|
971
|
+
description: "Names of rulesets to apply to these repos",
|
|
972
|
+
examples: [
|
|
973
|
+
[
|
|
974
|
+
"workflow",
|
|
975
|
+
"release"
|
|
976
|
+
]
|
|
977
|
+
]
|
|
978
|
+
})),
|
|
979
|
+
cleanup: Schema.optional(CleanupSchema)
|
|
980
|
+
}).annotations({
|
|
981
|
+
identifier: "Group",
|
|
982
|
+
title: "Repository group",
|
|
983
|
+
description: "A named group of repositories with their resource assignments",
|
|
984
|
+
jsonSchema: {
|
|
985
|
+
"x-tombi-table-keys-order": "schema"
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
const SquashMergeCommitTitleSchema = Schema.Literal("PR_TITLE", "COMMIT_OR_PR_TITLE").annotations({
|
|
989
|
+
title: "Squash merge commit title",
|
|
990
|
+
description: "Default title for squash merge commits: PR_TITLE uses the pull request title, COMMIT_OR_PR_TITLE uses the commit message if only one commit, otherwise the PR title"
|
|
991
|
+
});
|
|
992
|
+
const SquashMergeCommitMessageSchema = Schema.Literal("PR_BODY", "COMMIT_MESSAGES", "BLANK").annotations({
|
|
993
|
+
title: "Squash merge commit message",
|
|
994
|
+
description: "Default message body for squash merge commits: PR_BODY uses the pull request body, COMMIT_MESSAGES concatenates all commit messages, BLANK leaves it empty"
|
|
995
|
+
});
|
|
996
|
+
const MergeCommitTitleSchema = Schema.Literal("PR_TITLE", "MERGE_MESSAGE").annotations({
|
|
997
|
+
title: "Merge commit title",
|
|
998
|
+
description: "Default title for merge commits: PR_TITLE uses the pull request title, MERGE_MESSAGE uses the classic merge message"
|
|
999
|
+
});
|
|
1000
|
+
const MergeCommitMessageSchema = Schema.Literal("PR_BODY", "PR_TITLE", "BLANK").annotations({
|
|
1001
|
+
title: "Merge commit message",
|
|
1002
|
+
description: "Default message body for merge commits: PR_BODY uses the pull request body, PR_TITLE uses the PR title, BLANK leaves it empty"
|
|
1003
|
+
});
|
|
1004
|
+
const SettingsGroupSchema = Schema.Struct({
|
|
1005
|
+
is_template: Schema.optional(Schema.Boolean.annotations({
|
|
1006
|
+
title: "Template repository",
|
|
1007
|
+
description: "Whether the repository is a template that can be used to generate new repositories"
|
|
1008
|
+
})),
|
|
1009
|
+
has_wiki: Schema.optional(Schema.Boolean.annotations({
|
|
1010
|
+
title: "Wikis",
|
|
1011
|
+
description: "Enable the wiki feature for the repository"
|
|
1012
|
+
})),
|
|
1013
|
+
has_issues: Schema.optional(Schema.Boolean.annotations({
|
|
1014
|
+
title: "Issues",
|
|
1015
|
+
description: "Enable the issues feature for the repository"
|
|
1016
|
+
})),
|
|
1017
|
+
has_projects: Schema.optional(Schema.Boolean.annotations({
|
|
1018
|
+
title: "Projects",
|
|
1019
|
+
description: "Enable the projects feature for the repository"
|
|
1020
|
+
})),
|
|
1021
|
+
has_discussions: Schema.optional(Schema.Boolean.annotations({
|
|
1022
|
+
title: "Discussions",
|
|
1023
|
+
description: "Enable the discussions feature for the repository"
|
|
1024
|
+
})),
|
|
1025
|
+
has_sponsorships: Schema.optional(Schema.Boolean.annotations({
|
|
1026
|
+
title: "Sponsorships",
|
|
1027
|
+
description: "Display a Sponsor button for the repository (synced via GraphQL)"
|
|
1028
|
+
})),
|
|
1029
|
+
has_pull_requests: Schema.optional(Schema.Boolean.annotations({
|
|
1030
|
+
title: "Pull requests",
|
|
1031
|
+
description: "Enable the pull requests feature for the repository (synced via GraphQL)"
|
|
1032
|
+
})),
|
|
1033
|
+
allow_forking: Schema.optional(Schema.Boolean.annotations({
|
|
1034
|
+
title: "Allow forking",
|
|
1035
|
+
description: "Allow forking of the repository"
|
|
1036
|
+
})),
|
|
1037
|
+
allow_merge_commit: Schema.optional(Schema.Boolean.annotations({
|
|
1038
|
+
title: "Allow merge commits",
|
|
1039
|
+
description: "Allow merge commits when merging pull requests"
|
|
1040
|
+
})),
|
|
1041
|
+
allow_squash_merge: Schema.optional(Schema.Boolean.annotations({
|
|
1042
|
+
title: "Allow squash merging",
|
|
1043
|
+
description: "Allow squash merging when merging pull requests"
|
|
1044
|
+
})),
|
|
1045
|
+
allow_rebase_merge: Schema.optional(Schema.Boolean.annotations({
|
|
1046
|
+
title: "Allow rebase merging",
|
|
1047
|
+
description: "Allow rebase merging when merging pull requests"
|
|
1048
|
+
})),
|
|
1049
|
+
allow_auto_merge: Schema.optional(Schema.Boolean.annotations({
|
|
1050
|
+
title: "Allow auto-merge",
|
|
1051
|
+
description: "Allow pull requests to be automatically merged once all requirements are met"
|
|
1052
|
+
})),
|
|
1053
|
+
allow_update_branch: Schema.optional(Schema.Boolean.annotations({
|
|
1054
|
+
title: "Always suggest updating pull request branches",
|
|
1055
|
+
description: "Show the update branch button on pull requests"
|
|
1056
|
+
})),
|
|
1057
|
+
squash_merge_commit_title: Schema.optional(SquashMergeCommitTitleSchema),
|
|
1058
|
+
squash_merge_commit_message: Schema.optional(SquashMergeCommitMessageSchema),
|
|
1059
|
+
merge_commit_title: Schema.optional(MergeCommitTitleSchema),
|
|
1060
|
+
merge_commit_message: Schema.optional(MergeCommitMessageSchema),
|
|
1061
|
+
delete_branch_on_merge: Schema.optional(Schema.Boolean.annotations({
|
|
1062
|
+
title: "Automatically delete head branches",
|
|
1063
|
+
description: "Automatically delete head branches after pull requests are merged"
|
|
1064
|
+
})),
|
|
1065
|
+
web_commit_signoff_required: Schema.optional(Schema.Boolean.annotations({
|
|
1066
|
+
title: "Require commit signoff",
|
|
1067
|
+
description: "Require contributors to sign off on web-based commits"
|
|
1068
|
+
}))
|
|
1069
|
+
}, {
|
|
1070
|
+
key: Schema.String,
|
|
1071
|
+
value: Schema.Unknown
|
|
1072
|
+
}).annotations({
|
|
1073
|
+
identifier: "SettingsGroup",
|
|
1074
|
+
title: "Settings group",
|
|
1075
|
+
description: "GitHub repository settings to apply. Known fields are typed; additional fields are passed through to the API.",
|
|
1076
|
+
jsonSchema: {
|
|
1077
|
+
"x-tombi-table-keys-order": "schema"
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
const LogLevelSchema = Schema.Literal("silent", "info", "verbose", "debug").annotations({
|
|
1081
|
+
identifier: "LogLevel",
|
|
1082
|
+
title: "Log level",
|
|
1083
|
+
description: "Controls output verbosity: silent (none), info (summaries), verbose (per-operation), debug (with sources)"
|
|
1084
|
+
});
|
|
1085
|
+
const ConfigSchema = Schema.Struct({
|
|
1086
|
+
owner: Schema.optional(Schema.String.annotations({
|
|
1087
|
+
title: "Default owner",
|
|
1088
|
+
description: "Default GitHub user or organization for all groups. Can be overridden per group.",
|
|
1089
|
+
examples: [
|
|
1090
|
+
"spencerbeggs",
|
|
1091
|
+
"savvy-web"
|
|
1092
|
+
]
|
|
1093
|
+
})),
|
|
1094
|
+
log_level: Schema.optionalWith(LogLevelSchema, {
|
|
1095
|
+
default: ()=>"info"
|
|
1096
|
+
}).annotations({
|
|
1097
|
+
title: "Log level",
|
|
1098
|
+
description: "Default output verbosity. Can be overridden with --log-level CLI flag."
|
|
1099
|
+
}),
|
|
1100
|
+
settings: Schema.optionalWith(Schema.Record({
|
|
1101
|
+
key: Schema.String,
|
|
1102
|
+
value: SettingsGroupSchema
|
|
1103
|
+
}).annotations({
|
|
1104
|
+
title: "Settings groups",
|
|
1105
|
+
description: "Named groups of GitHub repository settings (passed to the repos.update API)",
|
|
1106
|
+
jsonSchema: {
|
|
1107
|
+
"x-tombi-additional-key-label": "setting_group"
|
|
1108
|
+
}
|
|
1109
|
+
}), {
|
|
1110
|
+
default: ()=>({})
|
|
1111
|
+
}),
|
|
1112
|
+
secrets: Schema.optionalWith(Schema.Record({
|
|
1113
|
+
key: Schema.String,
|
|
1114
|
+
value: SecretGroupSchema
|
|
1115
|
+
}).annotations({
|
|
1116
|
+
title: "Secret groups",
|
|
1117
|
+
description: "Named groups of secrets. Each group is one kind: file, value, or resolved.",
|
|
1118
|
+
jsonSchema: {
|
|
1119
|
+
"x-tombi-additional-key-label": "secret_group"
|
|
1120
|
+
}
|
|
1121
|
+
}), {
|
|
1122
|
+
default: ()=>({})
|
|
1123
|
+
}),
|
|
1124
|
+
variables: Schema.optionalWith(Schema.Record({
|
|
1125
|
+
key: Schema.String,
|
|
1126
|
+
value: VariableGroupSchema
|
|
1127
|
+
}).annotations({
|
|
1128
|
+
title: "Variable groups",
|
|
1129
|
+
description: "Named groups of variables. Each group is one kind: file, value, or resolved.",
|
|
1130
|
+
jsonSchema: {
|
|
1131
|
+
"x-tombi-additional-key-label": "variable_group"
|
|
1132
|
+
}
|
|
1133
|
+
}), {
|
|
1134
|
+
default: ()=>({})
|
|
1135
|
+
}),
|
|
1136
|
+
rulesets: Schema.optionalWith(Schema.Record({
|
|
1137
|
+
key: Schema.String,
|
|
1138
|
+
value: RulesetSchema
|
|
1139
|
+
}).annotations({
|
|
1140
|
+
title: "Rulesets",
|
|
1141
|
+
description: "Named rulesets defining branch/tag/push protection rules",
|
|
1142
|
+
jsonSchema: {
|
|
1143
|
+
"x-tombi-additional-key-label": "ruleset_name"
|
|
1144
|
+
}
|
|
1145
|
+
}), {
|
|
1146
|
+
default: ()=>({})
|
|
1147
|
+
}),
|
|
1148
|
+
environments: Schema.optionalWith(Schema.Record({
|
|
1149
|
+
key: Schema.String,
|
|
1150
|
+
value: EnvironmentSchema
|
|
1151
|
+
}).annotations({
|
|
1152
|
+
title: "Environments",
|
|
1153
|
+
description: "Named deployment environment configurations",
|
|
1154
|
+
jsonSchema: {
|
|
1155
|
+
"x-tombi-additional-key-label": "environment_name"
|
|
1156
|
+
}
|
|
1157
|
+
}), {
|
|
1158
|
+
default: ()=>({})
|
|
1159
|
+
}),
|
|
1160
|
+
groups: Schema.Record({
|
|
1161
|
+
key: Schema.String,
|
|
1162
|
+
value: GroupSchema
|
|
1163
|
+
}).annotations({
|
|
1164
|
+
title: "Groups",
|
|
1165
|
+
description: "Named groups of repositories with their settings, secrets, variables, and ruleset assignments",
|
|
1166
|
+
jsonSchema: {
|
|
1167
|
+
"x-tombi-additional-key-label": "group_name"
|
|
1168
|
+
}
|
|
1169
|
+
})
|
|
1170
|
+
}).annotations({
|
|
1171
|
+
identifier: "Config",
|
|
1172
|
+
title: "reposets Configuration",
|
|
1173
|
+
description: "Configuration for syncing GitHub repository settings, secrets, variables, and rulesets",
|
|
1174
|
+
jsonSchema: {
|
|
1175
|
+
"x-tombi-table-keys-order": "schema"
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
const ResolveSectionSchema = Schema.Struct({
|
|
1179
|
+
op: Schema.optional(Schema.Record({
|
|
1180
|
+
key: Schema.String,
|
|
1181
|
+
value: Schema.String
|
|
1182
|
+
}).annotations({
|
|
1183
|
+
title: "1Password references",
|
|
1184
|
+
description: "Named values resolved via 1Password SDK. Values are op:// reference strings.",
|
|
1185
|
+
jsonSchema: {
|
|
1186
|
+
"x-tombi-additional-key-label": "label"
|
|
1187
|
+
}
|
|
1188
|
+
})),
|
|
1189
|
+
file: Schema.optional(Schema.Record({
|
|
1190
|
+
key: Schema.String,
|
|
1191
|
+
value: Schema.String
|
|
1192
|
+
}).annotations({
|
|
1193
|
+
title: "File references",
|
|
1194
|
+
description: "Named values read from files. Values are file paths relative to the credentials directory.",
|
|
1195
|
+
jsonSchema: {
|
|
1196
|
+
"x-tombi-additional-key-label": "label"
|
|
1197
|
+
}
|
|
1198
|
+
})),
|
|
1199
|
+
value: Schema.optional(Schema.Record({
|
|
1200
|
+
key: Schema.String,
|
|
1201
|
+
value: Schema.Union(Schema.String, Schema.Record({
|
|
1202
|
+
key: Schema.String,
|
|
1203
|
+
value: Schema.Unknown
|
|
1204
|
+
}))
|
|
1205
|
+
}).annotations({
|
|
1206
|
+
title: "Inline values",
|
|
1207
|
+
description: "Named inline values. Strings are used as-is, objects are JSON-stringified.",
|
|
1208
|
+
jsonSchema: {
|
|
1209
|
+
"x-tombi-additional-key-label": "label"
|
|
1210
|
+
}
|
|
1211
|
+
}))
|
|
1212
|
+
}).annotations({
|
|
1213
|
+
identifier: "ResolveSection",
|
|
1214
|
+
title: "Resolve section",
|
|
1215
|
+
description: "Named values resolved from 1Password, files, or inline. Referenced by config templates."
|
|
1216
|
+
});
|
|
1217
|
+
const CredentialProfileSchema = Schema.Struct({
|
|
1218
|
+
github_token: Schema.String.annotations({
|
|
1219
|
+
title: "GitHub token",
|
|
1220
|
+
description: "A GitHub personal access token (fine-grained) with repo administration and secrets permissions",
|
|
1221
|
+
examples: [
|
|
1222
|
+
"ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
1223
|
+
]
|
|
1224
|
+
}),
|
|
1225
|
+
op_service_account_token: Schema.optional(Schema.String.annotations({
|
|
1226
|
+
title: "1Password service account token",
|
|
1227
|
+
description: "A 1Password service account token for resolving op:// secret references",
|
|
1228
|
+
examples: [
|
|
1229
|
+
"ops_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
1230
|
+
]
|
|
1231
|
+
})),
|
|
1232
|
+
resolve: Schema.optional(ResolveSectionSchema)
|
|
1233
|
+
}).annotations({
|
|
1234
|
+
identifier: "CredentialProfile",
|
|
1235
|
+
title: "Credential profile",
|
|
1236
|
+
description: "Authentication credentials for a GitHub account with optional resolved value definitions"
|
|
1237
|
+
});
|
|
1238
|
+
const CredentialsSchema = Schema.Struct({
|
|
1239
|
+
profiles: Schema.optionalWith(Schema.Record({
|
|
1240
|
+
key: Schema.String,
|
|
1241
|
+
value: CredentialProfileSchema
|
|
1242
|
+
}).annotations({
|
|
1243
|
+
title: "Credential profiles",
|
|
1244
|
+
description: "Named credential profiles. If only one profile is defined, it is used automatically for all repo groups.",
|
|
1245
|
+
jsonSchema: {
|
|
1246
|
+
"x-tombi-additional-key-label": "profile_name"
|
|
1247
|
+
}
|
|
1248
|
+
}), {
|
|
1249
|
+
default: ()=>({})
|
|
1250
|
+
})
|
|
1251
|
+
}).annotations({
|
|
1252
|
+
identifier: "Credentials",
|
|
1253
|
+
title: "reposets Credentials",
|
|
1254
|
+
description: "Authentication profiles for reposets. This file should be gitignored."
|
|
1255
|
+
});
|
|
1256
|
+
const CONFIG_FILENAME = "reposets.config.toml";
|
|
1257
|
+
const CREDENTIALS_FILENAME = "reposets.credentials.toml";
|
|
1258
|
+
const ReposetsConfigFile = ConfigFile.Tag("reposets/Config");
|
|
1259
|
+
const ReposetsCredentialsFile = ConfigFile.Tag("reposets/Credentials");
|
|
1260
|
+
const xdgLayer = XdgConfigLive({
|
|
1261
|
+
app: new AppDirsConfig({
|
|
1262
|
+
namespace: "reposets"
|
|
1263
|
+
}),
|
|
1264
|
+
config: {
|
|
1265
|
+
tag: ReposetsConfigFile,
|
|
1266
|
+
schema: ConfigSchema,
|
|
1267
|
+
codec: TomlCodec,
|
|
1268
|
+
strategy: FirstMatch,
|
|
1269
|
+
resolvers: [
|
|
1270
|
+
UpwardWalk({
|
|
1271
|
+
filename: CONFIG_FILENAME
|
|
1272
|
+
}),
|
|
1273
|
+
XdgConfig({
|
|
1274
|
+
filename: CONFIG_FILENAME
|
|
1275
|
+
})
|
|
1276
|
+
]
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
const credentialsLayer = ConfigFile.Live({
|
|
1280
|
+
tag: ReposetsCredentialsFile,
|
|
1281
|
+
schema: CredentialsSchema,
|
|
1282
|
+
codec: TomlCodec,
|
|
1283
|
+
strategy: FirstMatch,
|
|
1284
|
+
resolvers: [
|
|
1285
|
+
UpwardWalk({
|
|
1286
|
+
filename: CREDENTIALS_FILENAME
|
|
1287
|
+
}),
|
|
1288
|
+
XdgConfig({
|
|
1289
|
+
filename: CREDENTIALS_FILENAME
|
|
1290
|
+
})
|
|
1291
|
+
],
|
|
1292
|
+
defaultPath: XdgSavePath(CREDENTIALS_FILENAME)
|
|
1293
|
+
}).pipe(Layer.provide(xdgLayer));
|
|
1294
|
+
const ConfigFilesLive = Layer.mergeAll(xdgLayer, credentialsLayer);
|
|
1295
|
+
function resolveConfigFlag(configFlag) {
|
|
1296
|
+
if (Option.isNone(configFlag)) return;
|
|
1297
|
+
const flag = configFlag.value;
|
|
1298
|
+
if (existsSync(flag) && statSync(flag).isDirectory()) return join(flag, CONFIG_FILENAME);
|
|
1299
|
+
return flag;
|
|
1300
|
+
}
|
|
1301
|
+
function loadConfigWithDir(configFile, configFlag) {
|
|
1302
|
+
const resolved = resolveConfigFlag(configFlag);
|
|
1303
|
+
if (resolved) return Effect.map(configFile.loadFrom(resolved), (config)=>({
|
|
1304
|
+
config,
|
|
1305
|
+
configDir: dirname(resolved)
|
|
1306
|
+
}));
|
|
1307
|
+
return Effect.flatMap(configFile.discover, (sources)=>{
|
|
1308
|
+
if (0 === sources.length) return Effect.fail(new ConfigError({
|
|
1309
|
+
operation: "discover",
|
|
1310
|
+
reason: "No config file found"
|
|
1311
|
+
}));
|
|
1312
|
+
return Effect.succeed({
|
|
1313
|
+
config: sources[0].value,
|
|
1314
|
+
configDir: dirname(sources[0].path)
|
|
1315
|
+
});
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
class OnePasswordClient extends Context.Tag("OnePasswordClient")() {
|
|
1319
|
+
}
|
|
1320
|
+
const OnePasswordClientLive = Layer.succeed(OnePasswordClient, {
|
|
1321
|
+
resolve (reference, serviceAccountToken) {
|
|
1322
|
+
return Effect.tryPromise({
|
|
1323
|
+
try: async ()=>{
|
|
1324
|
+
const { createClient } = await import("@1password/sdk");
|
|
1325
|
+
const client = await createClient({
|
|
1326
|
+
auth: serviceAccountToken,
|
|
1327
|
+
integrationName: "reposets",
|
|
1328
|
+
integrationVersion: "1.0.0"
|
|
1329
|
+
});
|
|
1330
|
+
return await client.secrets.resolve(reference);
|
|
1331
|
+
},
|
|
1332
|
+
catch: (error)=>new OnePasswordError({
|
|
1333
|
+
message: `Failed to resolve ${reference}: ${error instanceof Error ? error.message : String(error)}`
|
|
1334
|
+
})
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
function OnePasswordClientTest(stubs) {
|
|
1339
|
+
return Layer.succeed(OnePasswordClient, {
|
|
1340
|
+
resolve (reference, _serviceAccountToken) {
|
|
1341
|
+
const value = stubs[reference];
|
|
1342
|
+
if (void 0 === value) return Effect.fail(new OnePasswordError({
|
|
1343
|
+
message: `Test stub: unknown reference ${reference}`
|
|
1344
|
+
}));
|
|
1345
|
+
return Effect.succeed(value);
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
class CredentialResolver extends Context.Tag("CredentialResolver")() {
|
|
1350
|
+
}
|
|
1351
|
+
const CredentialResolverLive = Layer.effect(CredentialResolver, Effect.gen(function*() {
|
|
1352
|
+
const opClient = yield* OnePasswordClient;
|
|
1353
|
+
return {
|
|
1354
|
+
resolveAll (profile, basePath) {
|
|
1355
|
+
return Effect.gen(function*() {
|
|
1356
|
+
const result = new Map();
|
|
1357
|
+
const resolveSection = profile.resolve;
|
|
1358
|
+
if (!resolveSection) return result;
|
|
1359
|
+
if (resolveSection.value) for (const [label, val] of Object.entries(resolveSection.value))if ("string" == typeof val) result.set(label, val);
|
|
1360
|
+
else result.set(label, JSON.stringify(val));
|
|
1361
|
+
if (resolveSection.file) for (const [label, filePath] of Object.entries(resolveSection.file)){
|
|
1362
|
+
const fullPath = isAbsolute(filePath) ? filePath : resolve(basePath, filePath);
|
|
1363
|
+
const content = yield* Effect["try"]({
|
|
1364
|
+
try: ()=>readFileSync(fullPath, "utf-8").trim(),
|
|
1365
|
+
catch: (error)=>new ResolveError({
|
|
1366
|
+
message: `Failed to read file for label '${label}': ${error instanceof Error ? error.message : String(error)}`
|
|
1367
|
+
})
|
|
1368
|
+
});
|
|
1369
|
+
result.set(label, content);
|
|
1370
|
+
}
|
|
1371
|
+
if (resolveSection.op) {
|
|
1372
|
+
const opToken = profile.op_service_account_token;
|
|
1373
|
+
if (!opToken) return yield* Effect.fail(new ResolveError({
|
|
1374
|
+
message: "No 1Password service account token provided but resolve.op entries are defined"
|
|
1375
|
+
}));
|
|
1376
|
+
for (const [label, reference] of Object.entries(resolveSection.op)){
|
|
1377
|
+
const value = yield* opClient.resolve(reference, opToken).pipe(Effect.mapError((err)=>new ResolveError({
|
|
1378
|
+
message: `Failed to resolve label '${label}': ${err.message}`
|
|
1379
|
+
})));
|
|
1380
|
+
result.set(label, value);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return result;
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
}));
|
|
1388
|
+
class GitHubClient extends Context.Tag("GitHubClient")() {
|
|
1389
|
+
}
|
|
1390
|
+
const ORG_ONLY_SETTINGS = new Set([
|
|
1391
|
+
"allow_forking"
|
|
1392
|
+
]);
|
|
1393
|
+
const GRAPHQL_SETTINGS = {
|
|
1394
|
+
has_sponsorships: "hasSponsorshipsEnabled",
|
|
1395
|
+
has_pull_requests: "hasPullRequestsEnabled"
|
|
1396
|
+
};
|
|
1397
|
+
function GitHubClientLive(token) {
|
|
1398
|
+
return Layer.succeed(GitHubClient, (()=>{
|
|
1399
|
+
const octokit = new Octokit({
|
|
1400
|
+
auth: token
|
|
1401
|
+
});
|
|
1402
|
+
function wrapError(error) {
|
|
1403
|
+
if (error instanceof Error) {
|
|
1404
|
+
const asAny = error;
|
|
1405
|
+
const status = "number" == typeof asAny.status ? asAny.status : void 0;
|
|
1406
|
+
if (void 0 !== status) return new GitHubApiError({
|
|
1407
|
+
message: error.message,
|
|
1408
|
+
status
|
|
1409
|
+
});
|
|
1410
|
+
return new GitHubApiError({
|
|
1411
|
+
message: error.message
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
return new GitHubApiError({
|
|
1415
|
+
message: String(error)
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
const ownerTypeCache = new Map();
|
|
1419
|
+
return {
|
|
1420
|
+
getOwnerType (owner) {
|
|
1421
|
+
return Effect.tryPromise({
|
|
1422
|
+
try: async ()=>{
|
|
1423
|
+
const cached = ownerTypeCache.get(owner);
|
|
1424
|
+
if (cached) return cached;
|
|
1425
|
+
const { data } = await octokit.users.getByUsername({
|
|
1426
|
+
username: owner
|
|
1427
|
+
});
|
|
1428
|
+
const ownerType = "Organization" === data.type ? "Organization" : "User";
|
|
1429
|
+
ownerTypeCache.set(owner, ownerType);
|
|
1430
|
+
return ownerType;
|
|
1431
|
+
},
|
|
1432
|
+
catch: wrapError
|
|
1433
|
+
});
|
|
1434
|
+
},
|
|
1435
|
+
syncSecret (owner, repo, name, value, scope) {
|
|
1436
|
+
return Effect.tryPromise({
|
|
1437
|
+
try: async ()=>{
|
|
1438
|
+
if ("actions" === scope) {
|
|
1439
|
+
const { data: publicKey } = await octokit.actions.getRepoPublicKey({
|
|
1440
|
+
owner,
|
|
1441
|
+
repo
|
|
1442
|
+
});
|
|
1443
|
+
const encryptedValue = encryptSecret(publicKey.key, value);
|
|
1444
|
+
await octokit.actions.createOrUpdateRepoSecret({
|
|
1445
|
+
owner,
|
|
1446
|
+
repo,
|
|
1447
|
+
secret_name: name,
|
|
1448
|
+
encrypted_value: encryptedValue,
|
|
1449
|
+
key_id: publicKey.key_id
|
|
1450
|
+
});
|
|
1451
|
+
} else if ("dependabot" === scope) {
|
|
1452
|
+
const { data: publicKey } = await octokit.dependabot.getRepoPublicKey({
|
|
1453
|
+
owner,
|
|
1454
|
+
repo
|
|
1455
|
+
});
|
|
1456
|
+
const encryptedValue = encryptSecret(publicKey.key, value);
|
|
1457
|
+
await octokit.dependabot.createOrUpdateRepoSecret({
|
|
1458
|
+
owner,
|
|
1459
|
+
repo,
|
|
1460
|
+
secret_name: name,
|
|
1461
|
+
encrypted_value: encryptedValue,
|
|
1462
|
+
key_id: publicKey.key_id
|
|
1463
|
+
});
|
|
1464
|
+
} else {
|
|
1465
|
+
const { data: publicKey } = await octokit.codespaces.getRepoPublicKey({
|
|
1466
|
+
owner,
|
|
1467
|
+
repo
|
|
1468
|
+
});
|
|
1469
|
+
const encryptedValue = encryptSecret(publicKey.key, value);
|
|
1470
|
+
await octokit.codespaces.createOrUpdateRepoSecret({
|
|
1471
|
+
owner,
|
|
1472
|
+
repo,
|
|
1473
|
+
secret_name: name,
|
|
1474
|
+
encrypted_value: encryptedValue,
|
|
1475
|
+
key_id: publicKey.key_id
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
},
|
|
1479
|
+
catch: wrapError
|
|
1480
|
+
});
|
|
1481
|
+
},
|
|
1482
|
+
syncVariable (owner, repo, name, value) {
|
|
1483
|
+
return Effect.tryPromise({
|
|
1484
|
+
try: async ()=>{
|
|
1485
|
+
const { data } = await octokit.actions.listRepoVariables({
|
|
1486
|
+
owner,
|
|
1487
|
+
repo
|
|
1488
|
+
});
|
|
1489
|
+
const exists = data.variables.some((v)=>v.name === name);
|
|
1490
|
+
if (exists) await octokit.actions.updateRepoVariable({
|
|
1491
|
+
owner,
|
|
1492
|
+
repo,
|
|
1493
|
+
name,
|
|
1494
|
+
value
|
|
1495
|
+
});
|
|
1496
|
+
else await octokit.actions.createRepoVariable({
|
|
1497
|
+
owner,
|
|
1498
|
+
repo,
|
|
1499
|
+
name,
|
|
1500
|
+
value
|
|
1501
|
+
});
|
|
1502
|
+
},
|
|
1503
|
+
catch: wrapError
|
|
1504
|
+
});
|
|
1505
|
+
},
|
|
1506
|
+
syncSettings (owner, repo, settings) {
|
|
1507
|
+
return Effect.tryPromise({
|
|
1508
|
+
try: async ()=>{
|
|
1509
|
+
const restSettings = {};
|
|
1510
|
+
const graphqlInput = {};
|
|
1511
|
+
for (const [key, value] of Object.entries(settings)){
|
|
1512
|
+
const graphqlField = GRAPHQL_SETTINGS[key];
|
|
1513
|
+
if (void 0 !== graphqlField) graphqlInput[graphqlField] = value;
|
|
1514
|
+
else restSettings[key] = value;
|
|
1515
|
+
}
|
|
1516
|
+
if (false === restSettings.allow_merge_commit) {
|
|
1517
|
+
delete restSettings.merge_commit_title;
|
|
1518
|
+
delete restSettings.merge_commit_message;
|
|
1519
|
+
}
|
|
1520
|
+
if (false === restSettings.allow_squash_merge) {
|
|
1521
|
+
delete restSettings.squash_merge_commit_title;
|
|
1522
|
+
delete restSettings.squash_merge_commit_message;
|
|
1523
|
+
}
|
|
1524
|
+
if (Object.keys(restSettings).length > 0) await octokit.repos.update({
|
|
1525
|
+
owner,
|
|
1526
|
+
repo,
|
|
1527
|
+
...restSettings
|
|
1528
|
+
});
|
|
1529
|
+
if (Object.keys(graphqlInput).length > 0) {
|
|
1530
|
+
const { data: repoData } = await octokit.repos.get({
|
|
1531
|
+
owner,
|
|
1532
|
+
repo
|
|
1533
|
+
});
|
|
1534
|
+
await octokit.graphql(`mutation UpdateRepository($input: UpdateRepositoryInput!) {
|
|
1535
|
+
updateRepository(input: $input) {
|
|
1536
|
+
repository { id }
|
|
1537
|
+
}
|
|
1538
|
+
}`, {
|
|
1539
|
+
input: {
|
|
1540
|
+
repositoryId: repoData.node_id,
|
|
1541
|
+
...graphqlInput
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
},
|
|
1546
|
+
catch: wrapError
|
|
1547
|
+
});
|
|
1548
|
+
},
|
|
1549
|
+
syncRuleset (owner, repo, name, payload) {
|
|
1550
|
+
return Effect.tryPromise({
|
|
1551
|
+
try: async ()=>{
|
|
1552
|
+
const { data: existing } = await octokit.repos.getRepoRulesets({
|
|
1553
|
+
owner,
|
|
1554
|
+
repo
|
|
1555
|
+
});
|
|
1556
|
+
const match = existing.find((r)=>r.name === name);
|
|
1557
|
+
const body = {
|
|
1558
|
+
name: payload.name,
|
|
1559
|
+
target: payload.target,
|
|
1560
|
+
enforcement: payload.enforcement,
|
|
1561
|
+
...void 0 !== payload.conditions ? {
|
|
1562
|
+
conditions: payload.conditions
|
|
1563
|
+
} : {},
|
|
1564
|
+
...void 0 !== payload.rules ? {
|
|
1565
|
+
rules: payload.rules
|
|
1566
|
+
} : {},
|
|
1567
|
+
...void 0 !== payload.bypass_actors ? {
|
|
1568
|
+
bypass_actors: payload.bypass_actors
|
|
1569
|
+
} : {}
|
|
1570
|
+
};
|
|
1571
|
+
if (match) await octokit.repos.updateRepoRuleset({
|
|
1572
|
+
owner,
|
|
1573
|
+
repo,
|
|
1574
|
+
ruleset_id: match.id,
|
|
1575
|
+
...body
|
|
1576
|
+
});
|
|
1577
|
+
else await octokit.repos.createRepoRuleset({
|
|
1578
|
+
owner,
|
|
1579
|
+
repo,
|
|
1580
|
+
...body
|
|
1581
|
+
});
|
|
1582
|
+
},
|
|
1583
|
+
catch: wrapError
|
|
1584
|
+
});
|
|
1585
|
+
},
|
|
1586
|
+
listSecrets (owner, repo, scope) {
|
|
1587
|
+
return Effect.tryPromise({
|
|
1588
|
+
try: async ()=>{
|
|
1589
|
+
if ("actions" === scope) {
|
|
1590
|
+
const { data } = await octokit.actions.listRepoSecrets({
|
|
1591
|
+
owner,
|
|
1592
|
+
repo
|
|
1593
|
+
});
|
|
1594
|
+
return data.secrets.map((s)=>({
|
|
1595
|
+
name: s.name
|
|
1596
|
+
}));
|
|
1597
|
+
}
|
|
1598
|
+
if ("dependabot" === scope) {
|
|
1599
|
+
const { data } = await octokit.dependabot.listRepoSecrets({
|
|
1600
|
+
owner,
|
|
1601
|
+
repo
|
|
1602
|
+
});
|
|
1603
|
+
return data.secrets.map((s)=>({
|
|
1604
|
+
name: s.name
|
|
1605
|
+
}));
|
|
1606
|
+
}
|
|
1607
|
+
{
|
|
1608
|
+
const { data } = await octokit.codespaces.listRepoSecrets({
|
|
1609
|
+
owner,
|
|
1610
|
+
repo
|
|
1611
|
+
});
|
|
1612
|
+
return data.secrets.map((s)=>({
|
|
1613
|
+
name: s.name
|
|
1614
|
+
}));
|
|
1615
|
+
}
|
|
1616
|
+
},
|
|
1617
|
+
catch: wrapError
|
|
1618
|
+
});
|
|
1619
|
+
},
|
|
1620
|
+
listVariables (owner, repo) {
|
|
1621
|
+
return Effect.tryPromise({
|
|
1622
|
+
try: async ()=>{
|
|
1623
|
+
const { data } = await octokit.actions.listRepoVariables({
|
|
1624
|
+
owner,
|
|
1625
|
+
repo
|
|
1626
|
+
});
|
|
1627
|
+
return data.variables.map((v)=>({
|
|
1628
|
+
name: v.name
|
|
1629
|
+
}));
|
|
1630
|
+
},
|
|
1631
|
+
catch: wrapError
|
|
1632
|
+
});
|
|
1633
|
+
},
|
|
1634
|
+
listRulesets (owner, repo) {
|
|
1635
|
+
return Effect.tryPromise({
|
|
1636
|
+
try: async ()=>{
|
|
1637
|
+
const { data } = await octokit.repos.getRepoRulesets({
|
|
1638
|
+
owner,
|
|
1639
|
+
repo
|
|
1640
|
+
});
|
|
1641
|
+
return data.map((r)=>({
|
|
1642
|
+
name: r.name,
|
|
1643
|
+
id: r.id,
|
|
1644
|
+
source_type: r.source_type
|
|
1645
|
+
}));
|
|
1646
|
+
},
|
|
1647
|
+
catch: wrapError
|
|
1648
|
+
});
|
|
1649
|
+
},
|
|
1650
|
+
deleteSecret (owner, repo, name, scope) {
|
|
1651
|
+
return Effect.tryPromise({
|
|
1652
|
+
try: async ()=>{
|
|
1653
|
+
if ("actions" === scope) await octokit.actions.deleteRepoSecret({
|
|
1654
|
+
owner,
|
|
1655
|
+
repo,
|
|
1656
|
+
secret_name: name
|
|
1657
|
+
});
|
|
1658
|
+
else if ("dependabot" === scope) await octokit.dependabot.deleteRepoSecret({
|
|
1659
|
+
owner,
|
|
1660
|
+
repo,
|
|
1661
|
+
secret_name: name
|
|
1662
|
+
});
|
|
1663
|
+
else await octokit.codespaces.deleteRepoSecret({
|
|
1664
|
+
owner,
|
|
1665
|
+
repo,
|
|
1666
|
+
secret_name: name
|
|
1667
|
+
});
|
|
1668
|
+
},
|
|
1669
|
+
catch: wrapError
|
|
1670
|
+
});
|
|
1671
|
+
},
|
|
1672
|
+
deleteVariable (owner, repo, name) {
|
|
1673
|
+
return Effect.tryPromise({
|
|
1674
|
+
try: async ()=>{
|
|
1675
|
+
await octokit.actions.deleteRepoVariable({
|
|
1676
|
+
owner,
|
|
1677
|
+
repo,
|
|
1678
|
+
name
|
|
1679
|
+
});
|
|
1680
|
+
},
|
|
1681
|
+
catch: wrapError
|
|
1682
|
+
});
|
|
1683
|
+
},
|
|
1684
|
+
deleteRuleset (owner, repo, rulesetId) {
|
|
1685
|
+
return Effect.tryPromise({
|
|
1686
|
+
try: async ()=>{
|
|
1687
|
+
await octokit.repos.deleteRepoRuleset({
|
|
1688
|
+
owner,
|
|
1689
|
+
repo,
|
|
1690
|
+
ruleset_id: rulesetId
|
|
1691
|
+
});
|
|
1692
|
+
},
|
|
1693
|
+
catch: wrapError
|
|
1694
|
+
});
|
|
1695
|
+
},
|
|
1696
|
+
syncEnvironment (owner, repo, name, config) {
|
|
1697
|
+
return Effect.tryPromise({
|
|
1698
|
+
try: async ()=>{
|
|
1699
|
+
await octokit.repos.createOrUpdateEnvironment({
|
|
1700
|
+
owner,
|
|
1701
|
+
repo,
|
|
1702
|
+
environment_name: name,
|
|
1703
|
+
...config
|
|
1704
|
+
});
|
|
1705
|
+
},
|
|
1706
|
+
catch: wrapError
|
|
1707
|
+
});
|
|
1708
|
+
},
|
|
1709
|
+
syncEnvironmentSecret (owner, repo, envName, name, value) {
|
|
1710
|
+
return Effect.tryPromise({
|
|
1711
|
+
try: async ()=>{
|
|
1712
|
+
const { data: publicKey } = await octokit.actions.getEnvironmentPublicKey({
|
|
1713
|
+
owner,
|
|
1714
|
+
repo,
|
|
1715
|
+
environment_name: envName
|
|
1716
|
+
});
|
|
1717
|
+
const encrypted_value = encryptSecret(publicKey.key, value);
|
|
1718
|
+
await octokit.actions.createOrUpdateEnvironmentSecret({
|
|
1719
|
+
owner,
|
|
1720
|
+
repo,
|
|
1721
|
+
environment_name: envName,
|
|
1722
|
+
secret_name: name,
|
|
1723
|
+
encrypted_value,
|
|
1724
|
+
key_id: publicKey.key_id
|
|
1725
|
+
});
|
|
1726
|
+
},
|
|
1727
|
+
catch: wrapError
|
|
1728
|
+
});
|
|
1729
|
+
},
|
|
1730
|
+
syncEnvironmentVariable (owner, repo, envName, name, value) {
|
|
1731
|
+
return Effect.tryPromise({
|
|
1732
|
+
try: async ()=>{
|
|
1733
|
+
const { data } = await octokit.actions.listEnvironmentVariables({
|
|
1734
|
+
owner,
|
|
1735
|
+
repo,
|
|
1736
|
+
environment_name: envName
|
|
1737
|
+
});
|
|
1738
|
+
const exists = data.variables.some((v)=>v.name === name);
|
|
1739
|
+
if (exists) await octokit.actions.updateEnvironmentVariable({
|
|
1740
|
+
owner,
|
|
1741
|
+
repo,
|
|
1742
|
+
environment_name: envName,
|
|
1743
|
+
name,
|
|
1744
|
+
value
|
|
1745
|
+
});
|
|
1746
|
+
else await octokit.actions.createEnvironmentVariable({
|
|
1747
|
+
owner,
|
|
1748
|
+
repo,
|
|
1749
|
+
environment_name: envName,
|
|
1750
|
+
name,
|
|
1751
|
+
value
|
|
1752
|
+
});
|
|
1753
|
+
},
|
|
1754
|
+
catch: wrapError
|
|
1755
|
+
});
|
|
1756
|
+
},
|
|
1757
|
+
listEnvironments (owner, repo) {
|
|
1758
|
+
return Effect.tryPromise({
|
|
1759
|
+
try: async ()=>{
|
|
1760
|
+
const { data } = await octokit.repos.getAllEnvironments({
|
|
1761
|
+
owner,
|
|
1762
|
+
repo
|
|
1763
|
+
});
|
|
1764
|
+
return (data.environments ?? []).map((e)=>({
|
|
1765
|
+
name: e.name
|
|
1766
|
+
}));
|
|
1767
|
+
},
|
|
1768
|
+
catch: wrapError
|
|
1769
|
+
});
|
|
1770
|
+
},
|
|
1771
|
+
listEnvironmentSecrets (owner, repo, envName) {
|
|
1772
|
+
return Effect.tryPromise({
|
|
1773
|
+
try: async ()=>{
|
|
1774
|
+
const { data } = await octokit.actions.listEnvironmentSecrets({
|
|
1775
|
+
owner,
|
|
1776
|
+
repo,
|
|
1777
|
+
environment_name: envName
|
|
1778
|
+
});
|
|
1779
|
+
return data.secrets.map((s)=>({
|
|
1780
|
+
name: s.name
|
|
1781
|
+
}));
|
|
1782
|
+
},
|
|
1783
|
+
catch: wrapError
|
|
1784
|
+
});
|
|
1785
|
+
},
|
|
1786
|
+
listEnvironmentVariables (owner, repo, envName) {
|
|
1787
|
+
return Effect.tryPromise({
|
|
1788
|
+
try: async ()=>{
|
|
1789
|
+
const { data } = await octokit.actions.listEnvironmentVariables({
|
|
1790
|
+
owner,
|
|
1791
|
+
repo,
|
|
1792
|
+
environment_name: envName
|
|
1793
|
+
});
|
|
1794
|
+
return data.variables.map((v)=>({
|
|
1795
|
+
name: v.name
|
|
1796
|
+
}));
|
|
1797
|
+
},
|
|
1798
|
+
catch: wrapError
|
|
1799
|
+
});
|
|
1800
|
+
},
|
|
1801
|
+
deleteEnvironment (owner, repo, name) {
|
|
1802
|
+
return Effect.tryPromise({
|
|
1803
|
+
try: async ()=>{
|
|
1804
|
+
await octokit.repos.deleteAnEnvironment({
|
|
1805
|
+
owner,
|
|
1806
|
+
repo,
|
|
1807
|
+
environment_name: name
|
|
1808
|
+
});
|
|
1809
|
+
},
|
|
1810
|
+
catch: wrapError
|
|
1811
|
+
});
|
|
1812
|
+
},
|
|
1813
|
+
deleteEnvironmentSecret (owner, repo, envName, name) {
|
|
1814
|
+
return Effect.tryPromise({
|
|
1815
|
+
try: async ()=>{
|
|
1816
|
+
await octokit.actions.deleteEnvironmentSecret({
|
|
1817
|
+
owner,
|
|
1818
|
+
repo,
|
|
1819
|
+
environment_name: envName,
|
|
1820
|
+
secret_name: name
|
|
1821
|
+
});
|
|
1822
|
+
},
|
|
1823
|
+
catch: wrapError
|
|
1824
|
+
});
|
|
1825
|
+
},
|
|
1826
|
+
deleteEnvironmentVariable (owner, repo, envName, name) {
|
|
1827
|
+
return Effect.tryPromise({
|
|
1828
|
+
try: async ()=>{
|
|
1829
|
+
await octokit.actions.deleteEnvironmentVariable({
|
|
1830
|
+
owner,
|
|
1831
|
+
repo,
|
|
1832
|
+
environment_name: envName,
|
|
1833
|
+
name
|
|
1834
|
+
});
|
|
1835
|
+
},
|
|
1836
|
+
catch: wrapError
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
})());
|
|
1841
|
+
}
|
|
1842
|
+
function GitHubClientTest() {
|
|
1843
|
+
const recorded = [];
|
|
1844
|
+
const layer = Layer.succeed(GitHubClient, {
|
|
1845
|
+
getOwnerType (_owner) {
|
|
1846
|
+
return Effect.succeed("User");
|
|
1847
|
+
},
|
|
1848
|
+
syncSecret (owner, repo, name, _value, scope) {
|
|
1849
|
+
recorded.push({
|
|
1850
|
+
method: "syncSecret",
|
|
1851
|
+
args: {
|
|
1852
|
+
owner,
|
|
1853
|
+
repo,
|
|
1854
|
+
name,
|
|
1855
|
+
scope
|
|
1856
|
+
}
|
|
1857
|
+
});
|
|
1858
|
+
return Effect["void"];
|
|
1859
|
+
},
|
|
1860
|
+
syncVariable (owner, repo, name, _value) {
|
|
1861
|
+
recorded.push({
|
|
1862
|
+
method: "syncVariable",
|
|
1863
|
+
args: {
|
|
1864
|
+
owner,
|
|
1865
|
+
repo,
|
|
1866
|
+
name
|
|
1867
|
+
}
|
|
1868
|
+
});
|
|
1869
|
+
return Effect["void"];
|
|
1870
|
+
},
|
|
1871
|
+
syncSettings (owner, repo, settings) {
|
|
1872
|
+
recorded.push({
|
|
1873
|
+
method: "syncSettings",
|
|
1874
|
+
args: {
|
|
1875
|
+
owner,
|
|
1876
|
+
repo,
|
|
1877
|
+
settings
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
return Effect["void"];
|
|
1881
|
+
},
|
|
1882
|
+
syncRuleset (owner, repo, name, _payload) {
|
|
1883
|
+
recorded.push({
|
|
1884
|
+
method: "syncRuleset",
|
|
1885
|
+
args: {
|
|
1886
|
+
owner,
|
|
1887
|
+
repo,
|
|
1888
|
+
name
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1891
|
+
return Effect["void"];
|
|
1892
|
+
},
|
|
1893
|
+
listSecrets (_owner, _repo, _scope) {
|
|
1894
|
+
return Effect.succeed([]);
|
|
1895
|
+
},
|
|
1896
|
+
listVariables (_owner, _repo) {
|
|
1897
|
+
return Effect.succeed([]);
|
|
1898
|
+
},
|
|
1899
|
+
listRulesets (_owner, _repo) {
|
|
1900
|
+
return Effect.succeed([]);
|
|
1901
|
+
},
|
|
1902
|
+
deleteSecret (owner, repo, name, scope) {
|
|
1903
|
+
recorded.push({
|
|
1904
|
+
method: "deleteSecret",
|
|
1905
|
+
args: {
|
|
1906
|
+
owner,
|
|
1907
|
+
repo,
|
|
1908
|
+
name,
|
|
1909
|
+
scope
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
return Effect["void"];
|
|
1913
|
+
},
|
|
1914
|
+
deleteVariable (owner, repo, name) {
|
|
1915
|
+
recorded.push({
|
|
1916
|
+
method: "deleteVariable",
|
|
1917
|
+
args: {
|
|
1918
|
+
owner,
|
|
1919
|
+
repo,
|
|
1920
|
+
name
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
return Effect["void"];
|
|
1924
|
+
},
|
|
1925
|
+
deleteRuleset (owner, repo, rulesetId) {
|
|
1926
|
+
recorded.push({
|
|
1927
|
+
method: "deleteRuleset",
|
|
1928
|
+
args: {
|
|
1929
|
+
owner,
|
|
1930
|
+
repo,
|
|
1931
|
+
rulesetId
|
|
1932
|
+
}
|
|
1933
|
+
});
|
|
1934
|
+
return Effect["void"];
|
|
1935
|
+
},
|
|
1936
|
+
syncEnvironment (owner, repo, name, _config) {
|
|
1937
|
+
recorded.push({
|
|
1938
|
+
method: "syncEnvironment",
|
|
1939
|
+
args: {
|
|
1940
|
+
owner,
|
|
1941
|
+
repo,
|
|
1942
|
+
name
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
return Effect["void"];
|
|
1946
|
+
},
|
|
1947
|
+
syncEnvironmentSecret (owner, repo, envName, name, _value) {
|
|
1948
|
+
recorded.push({
|
|
1949
|
+
method: "syncEnvironmentSecret",
|
|
1950
|
+
args: {
|
|
1951
|
+
owner,
|
|
1952
|
+
repo,
|
|
1953
|
+
envName,
|
|
1954
|
+
name
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1957
|
+
return Effect["void"];
|
|
1958
|
+
},
|
|
1959
|
+
syncEnvironmentVariable (owner, repo, envName, name, _value) {
|
|
1960
|
+
recorded.push({
|
|
1961
|
+
method: "syncEnvironmentVariable",
|
|
1962
|
+
args: {
|
|
1963
|
+
owner,
|
|
1964
|
+
repo,
|
|
1965
|
+
envName,
|
|
1966
|
+
name
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
return Effect["void"];
|
|
1970
|
+
},
|
|
1971
|
+
listEnvironments (_owner, _repo) {
|
|
1972
|
+
return Effect.succeed([]);
|
|
1973
|
+
},
|
|
1974
|
+
listEnvironmentSecrets (_owner, _repo, _envName) {
|
|
1975
|
+
return Effect.succeed([]);
|
|
1976
|
+
},
|
|
1977
|
+
listEnvironmentVariables (_owner, _repo, _envName) {
|
|
1978
|
+
return Effect.succeed([]);
|
|
1979
|
+
},
|
|
1980
|
+
deleteEnvironment (owner, repo, name) {
|
|
1981
|
+
recorded.push({
|
|
1982
|
+
method: "deleteEnvironment",
|
|
1983
|
+
args: {
|
|
1984
|
+
owner,
|
|
1985
|
+
repo,
|
|
1986
|
+
name
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
return Effect["void"];
|
|
1990
|
+
},
|
|
1991
|
+
deleteEnvironmentSecret (owner, repo, envName, name) {
|
|
1992
|
+
recorded.push({
|
|
1993
|
+
method: "deleteEnvironmentSecret",
|
|
1994
|
+
args: {
|
|
1995
|
+
owner,
|
|
1996
|
+
repo,
|
|
1997
|
+
envName,
|
|
1998
|
+
name
|
|
1999
|
+
}
|
|
2000
|
+
});
|
|
2001
|
+
return Effect["void"];
|
|
2002
|
+
},
|
|
2003
|
+
deleteEnvironmentVariable (owner, repo, envName, name) {
|
|
2004
|
+
recorded.push({
|
|
2005
|
+
method: "deleteEnvironmentVariable",
|
|
2006
|
+
args: {
|
|
2007
|
+
owner,
|
|
2008
|
+
repo,
|
|
2009
|
+
envName,
|
|
2010
|
+
name
|
|
2011
|
+
}
|
|
2012
|
+
});
|
|
2013
|
+
return Effect["void"];
|
|
2014
|
+
}
|
|
2015
|
+
});
|
|
2016
|
+
return {
|
|
2017
|
+
layer,
|
|
2018
|
+
calls: ()=>[
|
|
2019
|
+
...recorded
|
|
2020
|
+
]
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
class SyncLogger extends Context.Tag("SyncLogger")() {
|
|
2024
|
+
}
|
|
2025
|
+
function pluralize(resource, count) {
|
|
2026
|
+
if (1 === count) return resource;
|
|
2027
|
+
if ("ruleset" === resource) return "rulesets";
|
|
2028
|
+
return `${resource}s`;
|
|
2029
|
+
}
|
|
2030
|
+
function SyncLoggerLive(config) {
|
|
2031
|
+
const { dryRun, logLevel, output } = config;
|
|
2032
|
+
return Layer.effect(SyncLogger, Effect.gen(function*() {
|
|
2033
|
+
const errors = yield* Ref.make([]);
|
|
2034
|
+
const currentRepo = yield* Ref.make("");
|
|
2035
|
+
function emit(line) {
|
|
2036
|
+
if (output) return Ref.update(output, (lines)=>[
|
|
2037
|
+
...lines,
|
|
2038
|
+
line
|
|
2039
|
+
]);
|
|
2040
|
+
return Effect.sync(()=>{
|
|
2041
|
+
process.stdout.write(`${line}\n`);
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
function isVisible(tier) {
|
|
2045
|
+
if ("silent" === logLevel) return false;
|
|
2046
|
+
const levels = [
|
|
2047
|
+
"info",
|
|
2048
|
+
"verbose",
|
|
2049
|
+
"debug"
|
|
2050
|
+
];
|
|
2051
|
+
return levels.indexOf(logLevel) >= levels.indexOf(tier);
|
|
2052
|
+
}
|
|
2053
|
+
function formatVerb(pastTense, presentTense) {
|
|
2054
|
+
if (dryRun) return `would ${presentTense}`.padEnd(14);
|
|
2055
|
+
return pastTense.padEnd(8);
|
|
2056
|
+
}
|
|
2057
|
+
return {
|
|
2058
|
+
groupStart (name, repoCount) {
|
|
2059
|
+
if (!isVisible("info")) return Effect["void"];
|
|
2060
|
+
return emit(`group: ${name} (${repoCount} ${1 === repoCount ? "repo" : "repos"})`);
|
|
2061
|
+
},
|
|
2062
|
+
repoStart (owner, repo) {
|
|
2063
|
+
const repoSlug = `${owner}/${repo}`;
|
|
2064
|
+
if (!isVisible("info")) return Ref.set(currentRepo, repoSlug);
|
|
2065
|
+
return Effect.gen(function*() {
|
|
2066
|
+
yield* Ref.set(currentRepo, repoSlug);
|
|
2067
|
+
yield* emit(` repo: ${repoSlug}`);
|
|
2068
|
+
});
|
|
2069
|
+
},
|
|
2070
|
+
repoSkip (owner, repo, reason) {
|
|
2071
|
+
if (!isVisible("info")) return Effect["void"];
|
|
2072
|
+
return Effect.gen(function*() {
|
|
2073
|
+
yield* emit(` repo: ${owner}/${repo}`);
|
|
2074
|
+
yield* emit(` skip ${reason}`);
|
|
2075
|
+
});
|
|
2076
|
+
},
|
|
2077
|
+
syncSummary (resource, count, detail) {
|
|
2078
|
+
if (!isVisible("info")) return Effect["void"];
|
|
2079
|
+
const verb = formatVerb("synced", "sync");
|
|
2080
|
+
const noun = pluralize(resource, count);
|
|
2081
|
+
const suffix = detail ? ` (${detail})` : "";
|
|
2082
|
+
return emit(` ${verb}${count} ${noun}${suffix}`);
|
|
2083
|
+
},
|
|
2084
|
+
settingsApplied () {
|
|
2085
|
+
if (!isVisible("info")) return Effect["void"];
|
|
2086
|
+
const verb = formatVerb("applied", "apply");
|
|
2087
|
+
return emit(` ${verb}settings`);
|
|
2088
|
+
},
|
|
2089
|
+
cleanupSummary (resource, count, names) {
|
|
2090
|
+
if (!isVisible("info")) return Effect["void"];
|
|
2091
|
+
const verb = formatVerb("deleted", "delete");
|
|
2092
|
+
const noun = pluralize(resource, count);
|
|
2093
|
+
const suffix = names.length > 0 ? ` (${names.join(", ")})` : "";
|
|
2094
|
+
return emit(` ${verb}${count} ${noun}${suffix}`);
|
|
2095
|
+
},
|
|
2096
|
+
syncOperation (verb, resource, name, detail, source) {
|
|
2097
|
+
if (!isVisible("verbose")) return Effect["void"];
|
|
2098
|
+
const formattedVerb = formatVerb(verb, verb);
|
|
2099
|
+
const nameStr = name ? ` ${name}` : "";
|
|
2100
|
+
const suffix = detail ? ` ${detail}` : "";
|
|
2101
|
+
const sourceSuffix = source && isVisible("debug") ? ` <- ${source}` : "";
|
|
2102
|
+
return emit(` ${formattedVerb}${resource}${nameStr}${suffix}${sourceSuffix}`);
|
|
2103
|
+
},
|
|
2104
|
+
syncError (context, message) {
|
|
2105
|
+
if (!isVisible("info")) return Effect["void"];
|
|
2106
|
+
return Effect.gen(function*() {
|
|
2107
|
+
const repo = yield* Ref.get(currentRepo);
|
|
2108
|
+
yield* Ref.update(errors, (errs)=>[
|
|
2109
|
+
...errs,
|
|
2110
|
+
{
|
|
2111
|
+
repo,
|
|
2112
|
+
context,
|
|
2113
|
+
message
|
|
2114
|
+
}
|
|
2115
|
+
]);
|
|
2116
|
+
yield* emit(` error ${context}: ${message}`);
|
|
2117
|
+
});
|
|
2118
|
+
},
|
|
2119
|
+
finish () {
|
|
2120
|
+
if (!isVisible("info")) return Effect["void"];
|
|
2121
|
+
return Effect.gen(function*() {
|
|
2122
|
+
const errs = yield* Ref.get(errors);
|
|
2123
|
+
if (0 === errs.length) yield* emit("Sync complete!");
|
|
2124
|
+
else {
|
|
2125
|
+
const label = 1 === errs.length ? "error" : "errors";
|
|
2126
|
+
yield* emit(`Sync complete with ${errs.length} ${label}:`);
|
|
2127
|
+
for (const err of errs)yield* emit(` ${err.repo}: ${err.context} \u2014 ${err.message}`);
|
|
2128
|
+
}
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
};
|
|
2132
|
+
}));
|
|
2133
|
+
}
|
|
2134
|
+
class SyncEngine extends Context.Tag("SyncEngine")() {
|
|
2135
|
+
}
|
|
2136
|
+
function isCleanupActive(scope) {
|
|
2137
|
+
return false !== scope;
|
|
2138
|
+
}
|
|
2139
|
+
function getPreserveList(scope) {
|
|
2140
|
+
if ("object" == typeof scope && "preserve" in scope) return new Set(scope.preserve);
|
|
2141
|
+
return new Set();
|
|
2142
|
+
}
|
|
2143
|
+
function resolveResourceGroup(group, credentialMap, basePath) {
|
|
2144
|
+
return Effect.gen(function*() {
|
|
2145
|
+
const result = new Map();
|
|
2146
|
+
if ("file" in group) for (const [name, filePath] of Object.entries(group.file)){
|
|
2147
|
+
const fullPath = isAbsolute(filePath) ? filePath : resolve(basePath, filePath);
|
|
2148
|
+
const content = yield* Effect["try"]({
|
|
2149
|
+
try: ()=>readFileSync(fullPath, "utf-8").trim(),
|
|
2150
|
+
catch: (error)=>new ResolveError({
|
|
2151
|
+
message: `Failed to read file for '${name}': ${error instanceof Error ? error.message : String(error)}`
|
|
2152
|
+
})
|
|
2153
|
+
});
|
|
2154
|
+
result.set(name, content);
|
|
2155
|
+
}
|
|
2156
|
+
else if ("value" in group) for (const [name, val] of Object.entries(group.value))result.set(name, "string" == typeof val ? val : JSON.stringify(val));
|
|
2157
|
+
else if ("resolved" in group) for (const [name, label] of Object.entries(group.resolved)){
|
|
2158
|
+
const value = credentialMap.get(label);
|
|
2159
|
+
if (void 0 === value) yield* Effect.fail(new ResolveError({
|
|
2160
|
+
message: `Credential label '${label}' not found for '${name}'`
|
|
2161
|
+
}));
|
|
2162
|
+
else result.set(name, value);
|
|
2163
|
+
}
|
|
2164
|
+
return result;
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
function resolveRulesetRefs(ruleset, credentialMap) {
|
|
2168
|
+
const json = JSON.parse(JSON.stringify(ruleset));
|
|
2169
|
+
substituteResolved(json, credentialMap);
|
|
2170
|
+
return json;
|
|
2171
|
+
}
|
|
2172
|
+
function substituteResolved(obj, credentialMap) {
|
|
2173
|
+
for (const [key, value] of Object.entries(obj))if (value && "object" == typeof value && !Array.isArray(value)) {
|
|
2174
|
+
const rec = value;
|
|
2175
|
+
if ("resolved" in rec && "string" == typeof rec.resolved) {
|
|
2176
|
+
const resolved = credentialMap.get(rec.resolved);
|
|
2177
|
+
if (void 0 === resolved) throw new Error(`Credential label '${rec.resolved}' not found for ruleset field '${key}'`);
|
|
2178
|
+
const num = Number(resolved);
|
|
2179
|
+
obj[key] = Number.isNaN(num) ? resolved : num;
|
|
2180
|
+
} else substituteResolved(rec, credentialMap);
|
|
2181
|
+
} else if (Array.isArray(value)) {
|
|
2182
|
+
for (const item of value)if (item && "object" == typeof item) substituteResolved(item, credentialMap);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
function groupEntryNames(group) {
|
|
2186
|
+
if ("file" in group) return Object.keys(group.file);
|
|
2187
|
+
if ("value" in group) return Object.keys(group.value);
|
|
2188
|
+
if ("resolved" in group) return Object.keys(group.resolved);
|
|
2189
|
+
return [];
|
|
2190
|
+
}
|
|
2191
|
+
const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
|
|
2192
|
+
const github = yield* GitHubClient;
|
|
2193
|
+
const credResolver = yield* CredentialResolver;
|
|
2194
|
+
const logger = yield* SyncLogger;
|
|
2195
|
+
return {
|
|
2196
|
+
syncAll (config, credentials, options) {
|
|
2197
|
+
return Effect.gen(function*() {
|
|
2198
|
+
const { dryRun, noCleanup, groupFilter, repoFilter } = options;
|
|
2199
|
+
const profileEntries = Object.entries(credentials.profiles);
|
|
2200
|
+
const defaultProfileName = 1 === profileEntries.length ? profileEntries[0]?.[0] ?? "default" : "default";
|
|
2201
|
+
const defaultProfile = credentials.profiles[defaultProfileName];
|
|
2202
|
+
const groups = Object.entries(config.groups);
|
|
2203
|
+
for (const [groupName, group] of groups){
|
|
2204
|
+
if (groupFilter && groupName !== groupFilter) continue;
|
|
2205
|
+
const owner = group.owner ?? config.owner ?? "";
|
|
2206
|
+
const profileName = group.credentials ?? defaultProfileName;
|
|
2207
|
+
const profile = credentials.profiles[profileName] ?? defaultProfile;
|
|
2208
|
+
yield* logger.groupStart(groupName, group.repos.length);
|
|
2209
|
+
const credentialMap = profile ? yield* credResolver.resolveAll(profile, options.configDir) : new Map();
|
|
2210
|
+
const ownerType = yield* github.getOwnerType(owner).pipe(Effect.catchTag("GitHubApiError", ()=>Effect.succeed("User")));
|
|
2211
|
+
const secretScopes = [
|
|
2212
|
+
"actions",
|
|
2213
|
+
"dependabot",
|
|
2214
|
+
"codespaces"
|
|
2215
|
+
];
|
|
2216
|
+
const resolvedSecrets = new Map();
|
|
2217
|
+
for (const scope of secretScopes){
|
|
2218
|
+
const groupRefs = group.secrets?.[scope] ?? [];
|
|
2219
|
+
for (const groupRef of groupRefs){
|
|
2220
|
+
const secretGroup = config.secrets[groupRef];
|
|
2221
|
+
if (!secretGroup) continue;
|
|
2222
|
+
const entries = yield* resolveResourceGroup(secretGroup, credentialMap, options.configDir);
|
|
2223
|
+
const scopeMap = resolvedSecrets.get(scope) ?? new Map();
|
|
2224
|
+
for (const [name, value] of entries)scopeMap.set(name, value);
|
|
2225
|
+
resolvedSecrets.set(scope, scopeMap);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
const resolvedVariables = new Map();
|
|
2229
|
+
const variableGroupRefs = group.variables?.actions ?? [];
|
|
2230
|
+
for (const groupRef of variableGroupRefs){
|
|
2231
|
+
const variableGroup = config.variables[groupRef];
|
|
2232
|
+
if (!variableGroup) continue;
|
|
2233
|
+
const entries = yield* resolveResourceGroup(variableGroup, credentialMap, options.configDir);
|
|
2234
|
+
for (const [name, value] of entries)resolvedVariables.set(name, value);
|
|
2235
|
+
}
|
|
2236
|
+
const groupRulesetRefs = group.rulesets ?? [];
|
|
2237
|
+
const rulesetMap = new Map();
|
|
2238
|
+
for (const ref of groupRulesetRefs){
|
|
2239
|
+
const ruleset = config.rulesets[ref];
|
|
2240
|
+
if (ruleset) {
|
|
2241
|
+
const resolved = resolveRulesetRefs(ruleset, credentialMap);
|
|
2242
|
+
const payload = buildRulesetPayload(resolved);
|
|
2243
|
+
rulesetMap.set(ref, payload);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
const envRefs = group.environments ?? [];
|
|
2247
|
+
const envSecretMapping = group.secrets?.environments ?? {};
|
|
2248
|
+
const envVariableMapping = group.variables?.environments ?? {};
|
|
2249
|
+
const resolvedEnvSecrets = new Map();
|
|
2250
|
+
for (const [envName, groupRefs] of Object.entries(envSecretMapping)){
|
|
2251
|
+
const envMap = new Map();
|
|
2252
|
+
for (const groupRef of groupRefs){
|
|
2253
|
+
const secretGroup = config.secrets[groupRef];
|
|
2254
|
+
if (!secretGroup) continue;
|
|
2255
|
+
const entries = yield* resolveResourceGroup(secretGroup, credentialMap, options.configDir);
|
|
2256
|
+
for (const [name, value] of entries)envMap.set(name, value);
|
|
2257
|
+
}
|
|
2258
|
+
resolvedEnvSecrets.set(envName, envMap);
|
|
2259
|
+
}
|
|
2260
|
+
const resolvedEnvVariables = new Map();
|
|
2261
|
+
for (const [envName, groupRefs] of Object.entries(envVariableMapping)){
|
|
2262
|
+
const envMap = new Map();
|
|
2263
|
+
for (const groupRef of groupRefs){
|
|
2264
|
+
const variableGroup = config.variables[groupRef];
|
|
2265
|
+
if (!variableGroup) continue;
|
|
2266
|
+
const entries = yield* resolveResourceGroup(variableGroup, credentialMap, options.configDir);
|
|
2267
|
+
for (const [name, value] of entries)envMap.set(name, value);
|
|
2268
|
+
}
|
|
2269
|
+
resolvedEnvVariables.set(envName, envMap);
|
|
2270
|
+
}
|
|
2271
|
+
const effectiveCleanup = group.cleanup ?? {
|
|
2272
|
+
secrets: {
|
|
2273
|
+
actions: false,
|
|
2274
|
+
dependabot: false,
|
|
2275
|
+
codespaces: false,
|
|
2276
|
+
environments: false
|
|
2277
|
+
},
|
|
2278
|
+
variables: {
|
|
2279
|
+
actions: false,
|
|
2280
|
+
environments: false
|
|
2281
|
+
},
|
|
2282
|
+
rulesets: false,
|
|
2283
|
+
environments: false
|
|
2284
|
+
};
|
|
2285
|
+
const configuredSecretNames = (scope)=>{
|
|
2286
|
+
const refs = group.secrets?.[scope] ?? [];
|
|
2287
|
+
const names = new Set();
|
|
2288
|
+
for (const ref of refs){
|
|
2289
|
+
const grp = config.secrets[ref];
|
|
2290
|
+
if (grp) for (const name of groupEntryNames(grp))names.add(name);
|
|
2291
|
+
}
|
|
2292
|
+
return names;
|
|
2293
|
+
};
|
|
2294
|
+
const configuredVariableNames = ()=>{
|
|
2295
|
+
const refs = group.variables?.actions ?? [];
|
|
2296
|
+
const names = new Set();
|
|
2297
|
+
for (const ref of refs){
|
|
2298
|
+
const grp = config.variables[ref];
|
|
2299
|
+
if (grp) for (const name of groupEntryNames(grp))names.add(name);
|
|
2300
|
+
}
|
|
2301
|
+
return names;
|
|
2302
|
+
};
|
|
2303
|
+
const configuredRulesetNames = ()=>{
|
|
2304
|
+
const refs = group.rulesets ?? [];
|
|
2305
|
+
const names = new Set();
|
|
2306
|
+
for (const ref of refs){
|
|
2307
|
+
const ruleset = config.rulesets[ref];
|
|
2308
|
+
if (ruleset) names.add(ruleset.name);
|
|
2309
|
+
}
|
|
2310
|
+
return names;
|
|
2311
|
+
};
|
|
2312
|
+
const settingGroupRefs = group.settings ?? [];
|
|
2313
|
+
const mergedSettings = {};
|
|
2314
|
+
const skippedSettings = [];
|
|
2315
|
+
for (const ref of settingGroupRefs){
|
|
2316
|
+
const settingGroup = config.settings[ref];
|
|
2317
|
+
if (settingGroup) Object.assign(mergedSettings, settingGroup);
|
|
2318
|
+
}
|
|
2319
|
+
if ("User" === ownerType) {
|
|
2320
|
+
for (const key of ORG_ONLY_SETTINGS)if (key in mergedSettings) {
|
|
2321
|
+
delete mergedSettings[key];
|
|
2322
|
+
skippedSettings.push(key);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
const hasSecrets = secretScopes.some((s)=>(resolvedSecrets.get(s)?.size ?? 0) > 0);
|
|
2326
|
+
const hasVariables = resolvedVariables.size > 0;
|
|
2327
|
+
const hasRulesets = rulesetMap.size > 0;
|
|
2328
|
+
const hasSettings = Object.keys(mergedSettings).length > 0;
|
|
2329
|
+
const hasEnvironments = envRefs.length > 0;
|
|
2330
|
+
const hasEnvSecrets = resolvedEnvSecrets.size > 0;
|
|
2331
|
+
const hasEnvVariables = resolvedEnvVariables.size > 0;
|
|
2332
|
+
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));
|
|
2333
|
+
for (const repoName of group.repos)if (!repoFilter || repoName === repoFilter) {
|
|
2334
|
+
if (!hasSecrets && !hasVariables && !hasRulesets && !hasSettings && !hasEnvironments && !hasEnvSecrets && !hasEnvVariables && !hasCleanup) {
|
|
2335
|
+
yield* logger.repoSkip(owner, repoName, "no changes configured");
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
yield* logger.repoStart(owner, repoName);
|
|
2339
|
+
if (!dryRun) {
|
|
2340
|
+
if (hasSettings) {
|
|
2341
|
+
yield* logger.syncOperation("apply", "settings", "");
|
|
2342
|
+
yield* github.syncSettings(owner, repoName, mergedSettings).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError("settings", err.message)));
|
|
2343
|
+
}
|
|
2344
|
+
for (const key of skippedSettings)yield* logger.syncOperation("skip", "setting", key, "(org-only, owner is a personal account)");
|
|
2345
|
+
for (const envName of envRefs){
|
|
2346
|
+
const envConfig = config.environments[envName];
|
|
2347
|
+
if (envConfig) {
|
|
2348
|
+
yield* logger.syncOperation("sync", "environment", envName);
|
|
2349
|
+
yield* github.syncEnvironment(owner, repoName, envName, envConfig).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`environment ${envName}`, err.message)));
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
for (const scope of secretScopes){
|
|
2353
|
+
const scopeMap = resolvedSecrets.get(scope);
|
|
2354
|
+
if (scopeMap) for (const [name, value] of scopeMap){
|
|
2355
|
+
yield* logger.syncOperation("sync", "secret", name, `(${scope})`);
|
|
2356
|
+
yield* github.syncSecret(owner, repoName, name, value, scope).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`secret ${name} (${scope})`, err.message)));
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
for (const [envName, envMap] of resolvedEnvSecrets)for (const [name, value] of envMap){
|
|
2360
|
+
yield* logger.syncOperation("sync", "secret", name, `(env: ${envName})`);
|
|
2361
|
+
yield* github.syncEnvironmentSecret(owner, repoName, envName, name, value).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`secret ${name} (env: ${envName})`, err.message)));
|
|
2362
|
+
}
|
|
2363
|
+
for (const [name, value] of resolvedVariables){
|
|
2364
|
+
yield* logger.syncOperation("sync", "variable", name);
|
|
2365
|
+
yield* github.syncVariable(owner, repoName, name, value).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`variable ${name}`, err.message)));
|
|
2366
|
+
}
|
|
2367
|
+
for (const [envName, envMap] of resolvedEnvVariables)for (const [name, value] of envMap){
|
|
2368
|
+
yield* logger.syncOperation("sync", "variable", name, `(env: ${envName})`);
|
|
2369
|
+
yield* github.syncEnvironmentVariable(owner, repoName, envName, name, value).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`variable ${name} (env: ${envName})`, err.message)));
|
|
2370
|
+
}
|
|
2371
|
+
for (const [_key, ruleset] of rulesetMap){
|
|
2372
|
+
yield* logger.syncOperation("sync", "ruleset", ruleset.name);
|
|
2373
|
+
yield* github.syncRuleset(owner, repoName, ruleset.name, ruleset).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`ruleset ${ruleset.name}`, err.message)));
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
if (hasEnvironments) yield* logger.syncSummary("environment", envRefs.length, "");
|
|
2377
|
+
if (hasSecrets) {
|
|
2378
|
+
const scopeCounts = [];
|
|
2379
|
+
let totalSecrets = 0;
|
|
2380
|
+
for (const scope of secretScopes){
|
|
2381
|
+
const count = resolvedSecrets.get(scope)?.size ?? 0;
|
|
2382
|
+
if (count > 0) {
|
|
2383
|
+
scopeCounts.push(`${scope}: ${count}`);
|
|
2384
|
+
totalSecrets += count;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
yield* logger.syncSummary("secret", totalSecrets, scopeCounts.join(", "));
|
|
2388
|
+
}
|
|
2389
|
+
if (hasVariables) yield* logger.syncSummary("variable", resolvedVariables.size, "");
|
|
2390
|
+
if (hasSettings) yield* logger.settingsApplied();
|
|
2391
|
+
if (rulesetMap.size > 0) yield* logger.syncSummary("ruleset", rulesetMap.size, "");
|
|
2392
|
+
if (!noCleanup) {
|
|
2393
|
+
if (isCleanupActive(effectiveCleanup.secrets.actions)) {
|
|
2394
|
+
const configured = configuredSecretNames("actions");
|
|
2395
|
+
const preserved = getPreserveList(effectiveCleanup.secrets.actions);
|
|
2396
|
+
const existing = yield* github.listSecrets(owner, repoName, "actions").pipe(Effect.catchTag("GitHubApiError", (err)=>Effect.gen(function*() {
|
|
2397
|
+
yield* logger.syncError("list secrets (actions)", err.message);
|
|
2398
|
+
return [];
|
|
2399
|
+
})));
|
|
2400
|
+
const toDelete = [];
|
|
2401
|
+
for (const { name } of existing)if (!configured.has(name) && !preserved.has(name)) {
|
|
2402
|
+
toDelete.push(name);
|
|
2403
|
+
yield* logger.syncOperation("delete", "secret", name, "(actions)");
|
|
2404
|
+
if (!dryRun) yield* github.deleteSecret(owner, repoName, name, "actions").pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`delete secret ${name} (actions)`, err.message)));
|
|
2405
|
+
}
|
|
2406
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("secret", toDelete.length, toDelete);
|
|
2407
|
+
}
|
|
2408
|
+
if (isCleanupActive(effectiveCleanup.secrets.dependabot)) {
|
|
2409
|
+
const configured = configuredSecretNames("dependabot");
|
|
2410
|
+
const preserved = getPreserveList(effectiveCleanup.secrets.dependabot);
|
|
2411
|
+
const existing = yield* github.listSecrets(owner, repoName, "dependabot").pipe(Effect.catchTag("GitHubApiError", (err)=>Effect.gen(function*() {
|
|
2412
|
+
yield* logger.syncError("list secrets (dependabot)", err.message);
|
|
2413
|
+
return [];
|
|
2414
|
+
})));
|
|
2415
|
+
const toDelete = [];
|
|
2416
|
+
for (const { name } of existing)if (!configured.has(name) && !preserved.has(name)) {
|
|
2417
|
+
toDelete.push(name);
|
|
2418
|
+
yield* logger.syncOperation("delete", "secret", name, "(dependabot)");
|
|
2419
|
+
if (!dryRun) yield* github.deleteSecret(owner, repoName, name, "dependabot").pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`delete secret ${name} (dependabot)`, err.message)));
|
|
2420
|
+
}
|
|
2421
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("secret", toDelete.length, toDelete);
|
|
2422
|
+
}
|
|
2423
|
+
if (isCleanupActive(effectiveCleanup.secrets.codespaces)) {
|
|
2424
|
+
const configured = configuredSecretNames("codespaces");
|
|
2425
|
+
const preserved = getPreserveList(effectiveCleanup.secrets.codespaces);
|
|
2426
|
+
const existing = yield* github.listSecrets(owner, repoName, "codespaces").pipe(Effect.catchTag("GitHubApiError", (err)=>Effect.gen(function*() {
|
|
2427
|
+
yield* logger.syncError("list secrets (codespaces)", err.message);
|
|
2428
|
+
return [];
|
|
2429
|
+
})));
|
|
2430
|
+
const toDelete = [];
|
|
2431
|
+
for (const { name } of existing)if (!configured.has(name) && !preserved.has(name)) {
|
|
2432
|
+
toDelete.push(name);
|
|
2433
|
+
yield* logger.syncOperation("delete", "secret", name, "(codespaces)");
|
|
2434
|
+
if (!dryRun) yield* github.deleteSecret(owner, repoName, name, "codespaces").pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`delete secret ${name} (codespaces)`, err.message)));
|
|
2435
|
+
}
|
|
2436
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("secret", toDelete.length, toDelete);
|
|
2437
|
+
}
|
|
2438
|
+
if (isCleanupActive(effectiveCleanup.secrets.environments)) {
|
|
2439
|
+
const preserved = getPreserveList(effectiveCleanup.secrets.environments);
|
|
2440
|
+
const allEnvNames = new Set([
|
|
2441
|
+
...envRefs,
|
|
2442
|
+
...Object.keys(envSecretMapping)
|
|
2443
|
+
]);
|
|
2444
|
+
for (const envName of allEnvNames){
|
|
2445
|
+
const configuredNames = new Set();
|
|
2446
|
+
for (const ref of envSecretMapping[envName] ?? []){
|
|
2447
|
+
const grp = config.secrets[ref];
|
|
2448
|
+
if (grp) for (const name of groupEntryNames(grp))configuredNames.add(name);
|
|
2449
|
+
}
|
|
2450
|
+
const existing = yield* github.listEnvironmentSecrets(owner, repoName, envName).pipe(Effect.catchTag("GitHubApiError", (err)=>Effect.gen(function*() {
|
|
2451
|
+
yield* logger.syncError(`list secrets (env: ${envName})`, err.message);
|
|
2452
|
+
return [];
|
|
2453
|
+
})));
|
|
2454
|
+
const toDelete = [];
|
|
2455
|
+
for (const { name } of existing)if (!configuredNames.has(name) && !preserved.has(name)) {
|
|
2456
|
+
toDelete.push(name);
|
|
2457
|
+
yield* logger.syncOperation("delete", "secret", name, `(env: ${envName})`);
|
|
2458
|
+
if (!dryRun) yield* github.deleteEnvironmentSecret(owner, repoName, envName, name).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`delete secret ${name} (env: ${envName})`, err.message)));
|
|
2459
|
+
}
|
|
2460
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("secret", toDelete.length, toDelete);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
if (isCleanupActive(effectiveCleanup.variables.actions)) {
|
|
2464
|
+
const configured = configuredVariableNames();
|
|
2465
|
+
const preserved = getPreserveList(effectiveCleanup.variables.actions);
|
|
2466
|
+
const existing = yield* github.listVariables(owner, repoName).pipe(Effect.catchTag("GitHubApiError", (err)=>Effect.gen(function*() {
|
|
2467
|
+
yield* logger.syncError("list variables", err.message);
|
|
2468
|
+
return [];
|
|
2469
|
+
})));
|
|
2470
|
+
const toDelete = [];
|
|
2471
|
+
for (const { name } of existing)if (!configured.has(name) && !preserved.has(name)) {
|
|
2472
|
+
toDelete.push(name);
|
|
2473
|
+
yield* logger.syncOperation("delete", "variable", name);
|
|
2474
|
+
if (!dryRun) yield* github.deleteVariable(owner, repoName, name).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`delete variable ${name}`, err.message)));
|
|
2475
|
+
}
|
|
2476
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("variable", toDelete.length, toDelete);
|
|
2477
|
+
}
|
|
2478
|
+
if (isCleanupActive(effectiveCleanup.variables.environments)) {
|
|
2479
|
+
const preserved = getPreserveList(effectiveCleanup.variables.environments);
|
|
2480
|
+
const allEnvVarNames = new Set([
|
|
2481
|
+
...envRefs,
|
|
2482
|
+
...Object.keys(envVariableMapping)
|
|
2483
|
+
]);
|
|
2484
|
+
for (const envName of allEnvVarNames){
|
|
2485
|
+
const configuredNames = new Set();
|
|
2486
|
+
for (const ref of envVariableMapping[envName] ?? []){
|
|
2487
|
+
const grp = config.variables[ref];
|
|
2488
|
+
if (grp) for (const name of groupEntryNames(grp))configuredNames.add(name);
|
|
2489
|
+
}
|
|
2490
|
+
const existing = yield* github.listEnvironmentVariables(owner, repoName, envName).pipe(Effect.catchTag("GitHubApiError", (err)=>Effect.gen(function*() {
|
|
2491
|
+
yield* logger.syncError(`list variables (env: ${envName})`, err.message);
|
|
2492
|
+
return [];
|
|
2493
|
+
})));
|
|
2494
|
+
const toDelete = [];
|
|
2495
|
+
for (const { name } of existing)if (!configuredNames.has(name) && !preserved.has(name)) {
|
|
2496
|
+
toDelete.push(name);
|
|
2497
|
+
yield* logger.syncOperation("delete", "variable", name, `(env: ${envName})`);
|
|
2498
|
+
if (!dryRun) yield* github.deleteEnvironmentVariable(owner, repoName, envName, name).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`delete variable ${name} (env: ${envName})`, err.message)));
|
|
2499
|
+
}
|
|
2500
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("variable", toDelete.length, toDelete);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
if (isCleanupActive(effectiveCleanup.rulesets)) {
|
|
2504
|
+
const configured = configuredRulesetNames();
|
|
2505
|
+
const preserved = getPreserveList(effectiveCleanup.rulesets);
|
|
2506
|
+
const existing = yield* github.listRulesets(owner, repoName).pipe(Effect.catchTag("GitHubApiError", (err)=>Effect.gen(function*() {
|
|
2507
|
+
yield* logger.syncError("list rulesets", err.message);
|
|
2508
|
+
return [];
|
|
2509
|
+
})));
|
|
2510
|
+
const toDelete = [];
|
|
2511
|
+
for (const { name, id, source_type } of existing)if ("Repository" === source_type) {
|
|
2512
|
+
if (!configured.has(name) && !preserved.has(name)) {
|
|
2513
|
+
toDelete.push(name);
|
|
2514
|
+
yield* logger.syncOperation("delete", "ruleset", name);
|
|
2515
|
+
if (!dryRun) yield* github.deleteRuleset(owner, repoName, id).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`delete ruleset ${name}`, err.message)));
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("ruleset", toDelete.length, toDelete);
|
|
2519
|
+
}
|
|
2520
|
+
if (isCleanupActive(effectiveCleanup.environments)) {
|
|
2521
|
+
const configuredEnvNames = new Set(envRefs);
|
|
2522
|
+
const preserved = getPreserveList(effectiveCleanup.environments);
|
|
2523
|
+
const existing = yield* github.listEnvironments(owner, repoName).pipe(Effect.catchTag("GitHubApiError", (err)=>Effect.gen(function*() {
|
|
2524
|
+
yield* logger.syncError("list environments", err.message);
|
|
2525
|
+
return [];
|
|
2526
|
+
})));
|
|
2527
|
+
const toDelete = [];
|
|
2528
|
+
for (const { name } of existing)if (!configuredEnvNames.has(name) && !preserved.has(name)) {
|
|
2529
|
+
toDelete.push(name);
|
|
2530
|
+
yield* logger.syncOperation("delete", "environment", name);
|
|
2531
|
+
if (!dryRun) yield* github.deleteEnvironment(owner, repoName, name).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`delete environment ${name}`, err.message)));
|
|
2532
|
+
}
|
|
2533
|
+
if (toDelete.length > 0) yield* logger.cleanupSummary("environment", toDelete.length, toDelete);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
yield* logger.finish();
|
|
2539
|
+
}).pipe(Effect.catchAll((error)=>Effect.fail(error instanceof SyncError ? error : new SyncError({
|
|
2540
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2541
|
+
}))));
|
|
2542
|
+
}
|
|
2543
|
+
};
|
|
2544
|
+
}));
|
|
2545
|
+
export { BypassActorSchema, CleanupSchema, CleanupScopeSchema, ConfigFilesLive, ConfigSchema, CredentialProfileSchema, CredentialResolver, CredentialResolverLive, CredentialsSchema, GitHubApiError, GitHubClient, GitHubClientLive, GitHubClientTest, GroupSchema, LogLevelSchema, OnePasswordClient, OnePasswordClientLive, OnePasswordClientTest, OnePasswordError, ReposetsConfigFile, ReposetsCredentialsFile, ResolveError, ResolveSectionSchema, ResolvedRefSchema, RulesetSchema, SecretGroupSchema, SyncEngine, SyncEngineLive, SyncError, SyncLogger, SyncLoggerLive, VariableGroupSchema, buildRulesetPayload, encryptSecret, loadConfigWithDir, resolveConfigFlag };
|