reposets 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -14
- package/bin/reposets.d.ts +1 -0
- package/bin/reposets.js +25 -507
- package/cli/commands/credentials.js +74 -0
- package/cli/commands/doctor.js +154 -0
- package/cli/commands/init.js +147 -0
- package/cli/commands/list.js +49 -0
- package/cli/commands/sync.js +70 -0
- package/cli/commands/validate.js +53 -0
- package/errors.js +12 -0
- package/index.d.ts +1750 -1840
- package/index.js +19 -2
- package/lib/crypto.js +27 -0
- package/package.json +61 -73
- package/schemas/common.js +130 -0
- package/schemas/config.js +469 -0
- package/schemas/credentials.js +78 -0
- package/schemas/environment.js +70 -0
- package/schemas/ruleset.js +545 -0
- package/services/ConfigFiles.js +119 -0
- package/services/CredentialResolver.js +40 -0
- package/services/GitHubClient.js +875 -0
- package/services/OnePasswordClient.js +30 -0
- package/services/SyncEngine.js +580 -0
- package/services/SyncLogger.js +106 -0
- package/tsdoc-metadata.json +11 -11
- package/500.js +0 -3354
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { taplo, tombi } from "xdg-effect";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/schemas/ruleset.ts
|
|
5
|
+
const ResolvedRefSchema = Schema.Struct({ resolved: Schema.String.annotations({
|
|
6
|
+
title: "Credential label",
|
|
7
|
+
description: "Reference to a named value in the active credential profile's resolve section"
|
|
8
|
+
}) }).annotations({
|
|
9
|
+
identifier: "ResolvedRef",
|
|
10
|
+
title: "Resolved reference",
|
|
11
|
+
description: "A reference to a credential-resolved value"
|
|
12
|
+
});
|
|
13
|
+
const ActorTypeSchema = Schema.Literal("Integration", "OrganizationAdmin", "RepositoryRole", "Team", "DeployKey").annotations({
|
|
14
|
+
title: "Actor type",
|
|
15
|
+
description: "The type of actor that can bypass a ruleset"
|
|
16
|
+
});
|
|
17
|
+
const BypassModeSchema = Schema.Literal("always", "pull_request", "exempt").annotations({
|
|
18
|
+
title: "Bypass mode",
|
|
19
|
+
description: "When the specified actor can bypass the ruleset"
|
|
20
|
+
});
|
|
21
|
+
const BypassActorSchema = Schema.Struct({
|
|
22
|
+
actor_id: Schema.optional(Schema.Union(Schema.Int, ResolvedRefSchema).annotations({
|
|
23
|
+
title: "Actor ID",
|
|
24
|
+
description: "The ID of the actor, or a { resolved } reference to a credential label."
|
|
25
|
+
})),
|
|
26
|
+
actor_type: ActorTypeSchema,
|
|
27
|
+
bypass_mode: Schema.optionalWith(BypassModeSchema, { default: () => "always" })
|
|
28
|
+
}).annotations({
|
|
29
|
+
identifier: "BypassActor",
|
|
30
|
+
title: "Bypass actor",
|
|
31
|
+
description: "An actor that can bypass rules in a ruleset"
|
|
32
|
+
});
|
|
33
|
+
const RefNameConditionSchema = Schema.Struct({
|
|
34
|
+
include: Schema.optionalWith(Schema.Array(Schema.String).annotations({
|
|
35
|
+
title: "Include patterns",
|
|
36
|
+
description: "Ref name patterns to include. Accepts ~DEFAULT_BRANCH, ~ALL, or glob patterns.",
|
|
37
|
+
examples: [["~DEFAULT_BRANCH"]]
|
|
38
|
+
}), { default: () => [] }),
|
|
39
|
+
exclude: Schema.optionalWith(Schema.Array(Schema.String).annotations({
|
|
40
|
+
title: "Exclude patterns",
|
|
41
|
+
description: "Ref name patterns to exclude"
|
|
42
|
+
}), { default: () => [] })
|
|
43
|
+
}).annotations({
|
|
44
|
+
identifier: "RefNameCondition",
|
|
45
|
+
title: "Ref name condition",
|
|
46
|
+
description: "Conditions for matching ref names (branches or tags)"
|
|
47
|
+
});
|
|
48
|
+
const RulesetConditionsSchema = Schema.Struct({ ref_name: Schema.optional(RefNameConditionSchema) }).annotations({
|
|
49
|
+
identifier: "RulesetConditions",
|
|
50
|
+
title: "Ruleset conditions",
|
|
51
|
+
description: "Conditions that determine when the ruleset applies"
|
|
52
|
+
});
|
|
53
|
+
const RequiredReviewerSchema = Schema.Struct({
|
|
54
|
+
file_patterns: Schema.Array(Schema.String).annotations({
|
|
55
|
+
title: "File patterns",
|
|
56
|
+
description: "File patterns this reviewer must approve (fnmatch syntax)"
|
|
57
|
+
}),
|
|
58
|
+
minimum_approvals: Schema.Int.annotations({
|
|
59
|
+
title: "Minimum approvals",
|
|
60
|
+
description: "Minimum approvals required from this team (0 = optional)"
|
|
61
|
+
}),
|
|
62
|
+
reviewer: Schema.Struct({
|
|
63
|
+
id: Schema.Int.annotations({
|
|
64
|
+
title: "Team ID",
|
|
65
|
+
description: "Team ID"
|
|
66
|
+
}),
|
|
67
|
+
type: Schema.Literal("Team")
|
|
68
|
+
}).annotations({ title: "Reviewer team" })
|
|
69
|
+
});
|
|
70
|
+
const StatusCheckSchema = Schema.Struct({
|
|
71
|
+
context: Schema.String.annotations({
|
|
72
|
+
title: "Context",
|
|
73
|
+
description: "The status check context name that must be present on the commit"
|
|
74
|
+
}),
|
|
75
|
+
integration_id: Schema.optional(Schema.Union(Schema.Int, ResolvedRefSchema).annotations({
|
|
76
|
+
title: "Integration ID",
|
|
77
|
+
description: "The integration ID, or a { resolved } reference to a credential label"
|
|
78
|
+
}))
|
|
79
|
+
});
|
|
80
|
+
const WorkflowFileSchema = Schema.Struct({
|
|
81
|
+
path: Schema.String.annotations({
|
|
82
|
+
title: "Workflow path",
|
|
83
|
+
description: "Path to the workflow file"
|
|
84
|
+
}),
|
|
85
|
+
ref: Schema.optional(Schema.String.annotations({
|
|
86
|
+
title: "Ref",
|
|
87
|
+
description: "Branch or tag of the workflow file"
|
|
88
|
+
})),
|
|
89
|
+
repository_id: Schema.Union(Schema.Int, ResolvedRefSchema).annotations({
|
|
90
|
+
title: "Repository ID",
|
|
91
|
+
description: "Repository ID, or a { resolved } reference to a credential label"
|
|
92
|
+
}),
|
|
93
|
+
sha: Schema.optional(Schema.String.annotations({
|
|
94
|
+
title: "SHA",
|
|
95
|
+
description: "Commit SHA of the workflow file"
|
|
96
|
+
}))
|
|
97
|
+
});
|
|
98
|
+
const TargetPatternSchema = Schema.Union(Schema.Struct({ include: Schema.String.annotations({
|
|
99
|
+
title: "Include pattern",
|
|
100
|
+
description: "Glob pattern to include"
|
|
101
|
+
}) }), Schema.Struct({ exclude: Schema.String.annotations({
|
|
102
|
+
title: "Exclude pattern",
|
|
103
|
+
description: "Glob pattern to exclude"
|
|
104
|
+
}) })).annotations({
|
|
105
|
+
identifier: "TargetPattern",
|
|
106
|
+
title: "Target pattern",
|
|
107
|
+
description: "An include or exclude pattern for ref matching"
|
|
108
|
+
});
|
|
109
|
+
const TargetsSchema = Schema.Union(Schema.Literal("default", "all").annotations({
|
|
110
|
+
title: "Target preset",
|
|
111
|
+
description: "'default' targets the default branch; 'all' targets all branches/tags"
|
|
112
|
+
}), Schema.Array(TargetPatternSchema).annotations({
|
|
113
|
+
title: "Custom target patterns",
|
|
114
|
+
description: "Array of include/exclude patterns for fine-grained ref targeting"
|
|
115
|
+
})).annotations({
|
|
116
|
+
identifier: "Targets",
|
|
117
|
+
title: "Targets shorthand",
|
|
118
|
+
description: "Shorthand for specifying ref_name conditions: 'default', 'all', or custom patterns"
|
|
119
|
+
});
|
|
120
|
+
const PullRequestsShorthandSchema = Schema.Struct({
|
|
121
|
+
approvals: Schema.optionalWith(Schema.Int.pipe(Schema.between(0, 10)).annotations({
|
|
122
|
+
title: "Required approvals",
|
|
123
|
+
description: "Number of approving reviews required (0-10)"
|
|
124
|
+
}), { default: () => 0 }),
|
|
125
|
+
dismiss_stale_reviews: Schema.optionalWith(Schema.Boolean.annotations({
|
|
126
|
+
title: "Dismiss stale reviews",
|
|
127
|
+
description: "Dismiss previous approvals when new commits are pushed"
|
|
128
|
+
}), { default: () => false }),
|
|
129
|
+
code_owner_review: Schema.optionalWith(Schema.Boolean.annotations({
|
|
130
|
+
title: "Code owner review",
|
|
131
|
+
description: "Require review from code owners for files they own"
|
|
132
|
+
}), { default: () => false }),
|
|
133
|
+
last_push_approval: Schema.optionalWith(Schema.Boolean.annotations({
|
|
134
|
+
title: "Last push approval",
|
|
135
|
+
description: "Most recent push must be approved by someone other than the pusher"
|
|
136
|
+
}), { default: () => false }),
|
|
137
|
+
resolve_threads: Schema.optionalWith(Schema.Boolean.annotations({
|
|
138
|
+
title: "Resolve threads",
|
|
139
|
+
description: "All review conversations must be resolved before merging"
|
|
140
|
+
}), { default: () => false }),
|
|
141
|
+
merge_methods: Schema.optional(Schema.Array(Schema.Literal("merge", "squash", "rebase")).annotations({
|
|
142
|
+
title: "Merge methods",
|
|
143
|
+
description: "Allowed merge methods. At least one must be enabled."
|
|
144
|
+
})),
|
|
145
|
+
reviewers: Schema.optional(Schema.Array(RequiredReviewerSchema).annotations({
|
|
146
|
+
title: "Required reviewers",
|
|
147
|
+
description: "Teams that must approve specific file patterns"
|
|
148
|
+
}))
|
|
149
|
+
}).annotations({
|
|
150
|
+
identifier: "PullRequestsShorthand",
|
|
151
|
+
title: "Pull requests shorthand",
|
|
152
|
+
description: "Simplified pull request configuration (branch rulesets only)"
|
|
153
|
+
});
|
|
154
|
+
const StatusChecksShorthandSchema = Schema.Struct({
|
|
155
|
+
update_branch: Schema.optional(Schema.Boolean.annotations({
|
|
156
|
+
title: "Strict status checks",
|
|
157
|
+
description: "PRs must be tested with the latest code"
|
|
158
|
+
})),
|
|
159
|
+
on_creation: Schema.optional(Schema.Boolean.annotations({
|
|
160
|
+
title: "Enforce on create",
|
|
161
|
+
description: "When false, allows branch creation even if checks would prohibit it"
|
|
162
|
+
})),
|
|
163
|
+
default_integration_id: Schema.optional(Schema.Union(Schema.Int, ResolvedRefSchema).annotations({
|
|
164
|
+
title: "Default integration ID",
|
|
165
|
+
description: "Default integration ID applied to all checks that do not specify one"
|
|
166
|
+
})),
|
|
167
|
+
required: Schema.Array(StatusCheckSchema).annotations({
|
|
168
|
+
title: "Required checks",
|
|
169
|
+
description: "Status checks that must pass"
|
|
170
|
+
})
|
|
171
|
+
}).annotations({
|
|
172
|
+
identifier: "StatusChecksShorthand",
|
|
173
|
+
title: "Status checks shorthand",
|
|
174
|
+
description: "Simplified status checks configuration"
|
|
175
|
+
});
|
|
176
|
+
const EnforcementSchema = Schema.Literal("disabled", "active", "evaluate").annotations({
|
|
177
|
+
title: "Enforcement level",
|
|
178
|
+
description: "disabled = off, active = enforced, evaluate = test mode (GitHub Enterprise only)"
|
|
179
|
+
});
|
|
180
|
+
const PatternEntrySchema = Schema.Struct({
|
|
181
|
+
operator: Schema.Literal("starts_with", "ends_with", "contains", "regex").annotations({
|
|
182
|
+
title: "Operator",
|
|
183
|
+
description: "The operator to use for matching"
|
|
184
|
+
}),
|
|
185
|
+
pattern: Schema.String.annotations({
|
|
186
|
+
title: "Pattern",
|
|
187
|
+
description: "The pattern to match"
|
|
188
|
+
}),
|
|
189
|
+
name: Schema.optional(Schema.String.annotations({
|
|
190
|
+
title: "Rule name",
|
|
191
|
+
description: "Display name for this pattern rule"
|
|
192
|
+
})),
|
|
193
|
+
negate: Schema.optional(Schema.Boolean.annotations({
|
|
194
|
+
title: "Negate",
|
|
195
|
+
description: "If true, the rule fails when the pattern matches"
|
|
196
|
+
}))
|
|
197
|
+
}).annotations({
|
|
198
|
+
identifier: "PatternEntry",
|
|
199
|
+
title: "Pattern entry",
|
|
200
|
+
description: "A pattern matching rule with operator, pattern, and optional name/negate"
|
|
201
|
+
});
|
|
202
|
+
const MergeQueueShorthandSchema = Schema.Struct({
|
|
203
|
+
check_timeout: Schema.Int.pipe(Schema.between(1, 360)).annotations({
|
|
204
|
+
title: "Check timeout (minutes)",
|
|
205
|
+
description: "Max time for status checks to report"
|
|
206
|
+
}),
|
|
207
|
+
grouping: Schema.Literal("ALLGREEN", "HEADGREEN").annotations({
|
|
208
|
+
title: "Grouping strategy",
|
|
209
|
+
description: "Whether all commits or only the head commit must pass checks"
|
|
210
|
+
}),
|
|
211
|
+
max_build: Schema.Int.pipe(Schema.between(0, 100)).annotations({
|
|
212
|
+
title: "Max entries to build",
|
|
213
|
+
description: "Max queued PRs requesting checks simultaneously"
|
|
214
|
+
}),
|
|
215
|
+
max_merge: Schema.Int.pipe(Schema.between(0, 100)).annotations({
|
|
216
|
+
title: "Max entries to merge",
|
|
217
|
+
description: "Max PRs merged together in a group"
|
|
218
|
+
}),
|
|
219
|
+
merge_method: Schema.Literal("MERGE", "SQUASH", "REBASE").annotations({
|
|
220
|
+
title: "Merge method",
|
|
221
|
+
description: "Merge method for queued PRs"
|
|
222
|
+
}),
|
|
223
|
+
min_merge: Schema.Int.pipe(Schema.between(0, 100)).annotations({
|
|
224
|
+
title: "Min entries to merge",
|
|
225
|
+
description: "Min PRs merged together in a group"
|
|
226
|
+
}),
|
|
227
|
+
min_wait: Schema.Int.pipe(Schema.between(0, 360)).annotations({
|
|
228
|
+
title: "Min wait time (minutes)",
|
|
229
|
+
description: "Wait time for min group size after first PR is added"
|
|
230
|
+
})
|
|
231
|
+
}).annotations({
|
|
232
|
+
identifier: "MergeQueueShorthand",
|
|
233
|
+
title: "Merge queue",
|
|
234
|
+
description: "Merge queue configuration"
|
|
235
|
+
});
|
|
236
|
+
const CopilotReviewShorthandSchema = Schema.Struct({
|
|
237
|
+
draft_prs: Schema.optional(Schema.Boolean.annotations({
|
|
238
|
+
title: "Review draft PRs",
|
|
239
|
+
description: "Review draft PRs before they are marked ready"
|
|
240
|
+
})),
|
|
241
|
+
on_push: Schema.optional(Schema.Boolean.annotations({
|
|
242
|
+
title: "Review on push",
|
|
243
|
+
description: "Review each new push to the PR"
|
|
244
|
+
}))
|
|
245
|
+
}).annotations({
|
|
246
|
+
identifier: "CopilotReviewShorthand",
|
|
247
|
+
title: "Copilot review",
|
|
248
|
+
description: "Copilot code review configuration"
|
|
249
|
+
});
|
|
250
|
+
const CodeScanningEntrySchema = Schema.Struct({
|
|
251
|
+
tool: Schema.String.annotations({
|
|
252
|
+
title: "Tool name",
|
|
253
|
+
description: "Name of the code scanning tool"
|
|
254
|
+
}),
|
|
255
|
+
alerts: Schema.Literal("none", "errors", "errors_and_warnings", "all").annotations({
|
|
256
|
+
title: "Alerts threshold",
|
|
257
|
+
description: "Severity level at which alerts block updates"
|
|
258
|
+
}),
|
|
259
|
+
security_alerts: Schema.Literal("none", "critical", "high_or_higher", "medium_or_higher", "all").annotations({
|
|
260
|
+
title: "Security alerts threshold",
|
|
261
|
+
description: "Severity level at which security alerts block updates"
|
|
262
|
+
})
|
|
263
|
+
}).annotations({
|
|
264
|
+
identifier: "CodeScanningEntry",
|
|
265
|
+
title: "Code scanning tool",
|
|
266
|
+
description: "A code scanning tool with alert thresholds"
|
|
267
|
+
});
|
|
268
|
+
const WorkflowsShorthandSchema = Schema.Struct({
|
|
269
|
+
on_creation: Schema.optional(Schema.Boolean.annotations({
|
|
270
|
+
title: "Enforce on creation",
|
|
271
|
+
description: "Enforce workflows when a branch is created (false = skip on creation)"
|
|
272
|
+
})),
|
|
273
|
+
required: Schema.Array(WorkflowFileSchema).annotations({
|
|
274
|
+
title: "Required workflows",
|
|
275
|
+
description: "Workflows that must pass for this rule"
|
|
276
|
+
})
|
|
277
|
+
}).annotations({
|
|
278
|
+
identifier: "WorkflowsShorthand",
|
|
279
|
+
title: "Workflows",
|
|
280
|
+
description: "Required workflow configuration"
|
|
281
|
+
});
|
|
282
|
+
const sharedRulesetFields = {
|
|
283
|
+
name: Schema.String.annotations({
|
|
284
|
+
title: "Ruleset name",
|
|
285
|
+
description: "The name of the ruleset (used for matching when creating or updating)"
|
|
286
|
+
}),
|
|
287
|
+
enforcement: EnforcementSchema,
|
|
288
|
+
conditions: Schema.optional(RulesetConditionsSchema),
|
|
289
|
+
bypass_actors: Schema.optional(Schema.Array(BypassActorSchema)),
|
|
290
|
+
creation: Schema.optional(Schema.Boolean.annotations({
|
|
291
|
+
title: "Restrict creation",
|
|
292
|
+
description: "When true, adds a creation rule"
|
|
293
|
+
})),
|
|
294
|
+
update: Schema.optional(Schema.Boolean.annotations({
|
|
295
|
+
title: "Restrict updates",
|
|
296
|
+
description: "When true, adds an update rule with update_allows_fetch_and_merge: true"
|
|
297
|
+
})),
|
|
298
|
+
deletion: Schema.optional(Schema.Boolean.annotations({
|
|
299
|
+
title: "Restrict deletion",
|
|
300
|
+
description: "When true, adds a deletion rule"
|
|
301
|
+
})),
|
|
302
|
+
required_linear_history: Schema.optional(Schema.Boolean.annotations({
|
|
303
|
+
title: "Require linear history",
|
|
304
|
+
description: "When true, adds a required_linear_history rule"
|
|
305
|
+
})),
|
|
306
|
+
required_signatures: Schema.optional(Schema.Boolean.annotations({
|
|
307
|
+
title: "Require signatures",
|
|
308
|
+
description: "When true, adds a required_signatures rule"
|
|
309
|
+
})),
|
|
310
|
+
non_fast_forward: Schema.optional(Schema.Boolean.annotations({
|
|
311
|
+
title: "Prevent non-fast-forward",
|
|
312
|
+
description: "When true, adds a non_fast_forward rule"
|
|
313
|
+
})),
|
|
314
|
+
deployments: Schema.optional(Schema.Array(Schema.String).annotations({
|
|
315
|
+
title: "Required deployments",
|
|
316
|
+
description: "Deployment environments that must succeed; converts to required_deployments rule"
|
|
317
|
+
})),
|
|
318
|
+
targets: Schema.optional(TargetsSchema),
|
|
319
|
+
status_checks: Schema.optional(StatusChecksShorthandSchema),
|
|
320
|
+
commit_message: Schema.optional(Schema.Array(PatternEntrySchema).annotations({
|
|
321
|
+
title: "Commit message patterns",
|
|
322
|
+
description: "Commit message pattern rules"
|
|
323
|
+
})),
|
|
324
|
+
commit_author_email: Schema.optional(Schema.Array(PatternEntrySchema).annotations({
|
|
325
|
+
title: "Commit author email patterns",
|
|
326
|
+
description: "Commit author email pattern rules"
|
|
327
|
+
})),
|
|
328
|
+
committer_email: Schema.optional(Schema.Array(PatternEntrySchema).annotations({
|
|
329
|
+
title: "Committer email patterns",
|
|
330
|
+
description: "Committer email pattern rules"
|
|
331
|
+
}))
|
|
332
|
+
};
|
|
333
|
+
const BranchRulesetSchema = Schema.Struct({
|
|
334
|
+
...sharedRulesetFields,
|
|
335
|
+
type: Schema.Literal("branch").annotations({
|
|
336
|
+
title: "Ruleset type",
|
|
337
|
+
description: "This ruleset applies to branches"
|
|
338
|
+
}),
|
|
339
|
+
pull_requests: Schema.optional(PullRequestsShorthandSchema),
|
|
340
|
+
merge_queue: Schema.optional(MergeQueueShorthandSchema),
|
|
341
|
+
copilot_review: Schema.optional(CopilotReviewShorthandSchema),
|
|
342
|
+
code_scanning: Schema.optional(Schema.Array(CodeScanningEntrySchema).annotations({
|
|
343
|
+
title: "Code scanning tools",
|
|
344
|
+
description: "Code scanning tool requirements"
|
|
345
|
+
})),
|
|
346
|
+
workflows: Schema.optional(WorkflowsShorthandSchema),
|
|
347
|
+
branch_name: Schema.optional(Schema.Array(PatternEntrySchema).annotations({
|
|
348
|
+
title: "Branch name patterns",
|
|
349
|
+
description: "Branch name pattern rules"
|
|
350
|
+
}))
|
|
351
|
+
}).annotations({
|
|
352
|
+
identifier: "BranchRuleset",
|
|
353
|
+
title: "Branch ruleset",
|
|
354
|
+
description: "A ruleset that applies to branches",
|
|
355
|
+
jsonSchema: {
|
|
356
|
+
...tombi({ tableKeysOrder: "schema" }),
|
|
357
|
+
...taplo({
|
|
358
|
+
initKeys: [
|
|
359
|
+
"name",
|
|
360
|
+
"type",
|
|
361
|
+
"enforcement",
|
|
362
|
+
"targets"
|
|
363
|
+
],
|
|
364
|
+
links: { key: "https://github.com/spencerbeggs/reposets/blob/main/docs/rulesets.md" }
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
const TagRulesetSchema = Schema.Struct({
|
|
369
|
+
...sharedRulesetFields,
|
|
370
|
+
type: Schema.Literal("tag").annotations({
|
|
371
|
+
title: "Ruleset type",
|
|
372
|
+
description: "This ruleset applies to tags"
|
|
373
|
+
}),
|
|
374
|
+
tag_name: Schema.optional(Schema.Array(PatternEntrySchema).annotations({
|
|
375
|
+
title: "Tag name patterns",
|
|
376
|
+
description: "Tag name pattern rules"
|
|
377
|
+
}))
|
|
378
|
+
}).annotations({
|
|
379
|
+
identifier: "TagRuleset",
|
|
380
|
+
title: "Tag ruleset",
|
|
381
|
+
description: "A ruleset that applies to tags",
|
|
382
|
+
jsonSchema: {
|
|
383
|
+
...tombi({ tableKeysOrder: "schema" }),
|
|
384
|
+
...taplo({
|
|
385
|
+
initKeys: [
|
|
386
|
+
"name",
|
|
387
|
+
"type",
|
|
388
|
+
"enforcement",
|
|
389
|
+
"targets"
|
|
390
|
+
],
|
|
391
|
+
links: { key: "https://github.com/spencerbeggs/reposets/blob/main/docs/rulesets.md" }
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
const RulesetSchema = Schema.Union(BranchRulesetSchema, TagRulesetSchema).annotations({
|
|
396
|
+
identifier: "Ruleset",
|
|
397
|
+
title: "Repository ruleset",
|
|
398
|
+
description: "A set of rules to apply when specified conditions are met",
|
|
399
|
+
jsonSchema: taplo({ links: { key: "https://github.com/spencerbeggs/reposets/blob/main/docs/rulesets.md" } })
|
|
400
|
+
});
|
|
401
|
+
/**
|
|
402
|
+
* Builds an API-compatible ruleset payload from the config-level Ruleset.
|
|
403
|
+
* Converts all shorthand fields into the GitHub API rules array format.
|
|
404
|
+
*/
|
|
405
|
+
function buildRulesetPayload(ruleset) {
|
|
406
|
+
const rules = [];
|
|
407
|
+
if (ruleset.creation === true) rules.push({ type: "creation" });
|
|
408
|
+
if (ruleset.update === true) rules.push({
|
|
409
|
+
type: "update",
|
|
410
|
+
parameters: { update_allows_fetch_and_merge: true }
|
|
411
|
+
});
|
|
412
|
+
if (ruleset.deletion === true) rules.push({ type: "deletion" });
|
|
413
|
+
if (ruleset.required_linear_history === true) rules.push({ type: "required_linear_history" });
|
|
414
|
+
if (ruleset.required_signatures === true) rules.push({ type: "required_signatures" });
|
|
415
|
+
if (ruleset.non_fast_forward === true) rules.push({ type: "non_fast_forward" });
|
|
416
|
+
if (ruleset.deployments !== void 0 && ruleset.deployments.length > 0) rules.push({
|
|
417
|
+
type: "required_deployments",
|
|
418
|
+
parameters: { required_deployment_environments: ruleset.deployments }
|
|
419
|
+
});
|
|
420
|
+
if (ruleset.type === "branch" && ruleset.pull_requests !== void 0) {
|
|
421
|
+
const pr = ruleset.pull_requests;
|
|
422
|
+
rules.push({
|
|
423
|
+
type: "pull_request",
|
|
424
|
+
parameters: {
|
|
425
|
+
required_approving_review_count: pr.approvals,
|
|
426
|
+
dismiss_stale_reviews_on_push: pr.dismiss_stale_reviews,
|
|
427
|
+
require_code_owner_review: pr.code_owner_review,
|
|
428
|
+
require_last_push_approval: pr.last_push_approval,
|
|
429
|
+
required_review_thread_resolution: pr.resolve_threads,
|
|
430
|
+
...pr.merge_methods !== void 0 ? { allowed_merge_methods: pr.merge_methods } : {},
|
|
431
|
+
...pr.reviewers !== void 0 ? { required_reviewers: pr.reviewers } : {}
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
if (ruleset.status_checks !== void 0) {
|
|
436
|
+
const sc = ruleset.status_checks;
|
|
437
|
+
const checks = sc.required.map((check) => {
|
|
438
|
+
if (check.integration_id === void 0 && sc.default_integration_id !== void 0) return {
|
|
439
|
+
...check,
|
|
440
|
+
integration_id: sc.default_integration_id
|
|
441
|
+
};
|
|
442
|
+
return check;
|
|
443
|
+
});
|
|
444
|
+
rules.push({
|
|
445
|
+
type: "required_status_checks",
|
|
446
|
+
parameters: {
|
|
447
|
+
strict_required_status_checks_policy: sc.update_branch ?? true,
|
|
448
|
+
...sc.on_creation === false ? { do_not_enforce_on_create: true } : {},
|
|
449
|
+
required_status_checks: checks
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
if (ruleset.type === "branch" && ruleset.merge_queue !== void 0) {
|
|
454
|
+
const mq = ruleset.merge_queue;
|
|
455
|
+
rules.push({
|
|
456
|
+
type: "merge_queue",
|
|
457
|
+
parameters: {
|
|
458
|
+
check_response_timeout_minutes: mq.check_timeout,
|
|
459
|
+
grouping_strategy: mq.grouping,
|
|
460
|
+
max_entries_to_build: mq.max_build,
|
|
461
|
+
max_entries_to_merge: mq.max_merge,
|
|
462
|
+
merge_method: mq.merge_method,
|
|
463
|
+
min_entries_to_merge: mq.min_merge,
|
|
464
|
+
min_entries_to_merge_wait_minutes: mq.min_wait
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
if (ruleset.type === "branch" && ruleset.copilot_review !== void 0) {
|
|
469
|
+
const cr = ruleset.copilot_review;
|
|
470
|
+
rules.push({
|
|
471
|
+
type: "copilot_code_review",
|
|
472
|
+
parameters: {
|
|
473
|
+
...cr.draft_prs !== void 0 ? { review_draft_pull_requests: cr.draft_prs } : {},
|
|
474
|
+
...cr.on_push !== void 0 ? { review_on_push: cr.on_push } : {}
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
if (ruleset.type === "branch" && ruleset.code_scanning !== void 0) rules.push({
|
|
479
|
+
type: "code_scanning",
|
|
480
|
+
parameters: { code_scanning_tools: ruleset.code_scanning.map((entry) => ({
|
|
481
|
+
tool: entry.tool,
|
|
482
|
+
alerts_threshold: entry.alerts,
|
|
483
|
+
security_alerts_threshold: entry.security_alerts
|
|
484
|
+
})) }
|
|
485
|
+
});
|
|
486
|
+
if (ruleset.type === "branch" && ruleset.workflows !== void 0) {
|
|
487
|
+
const wf = ruleset.workflows;
|
|
488
|
+
rules.push({
|
|
489
|
+
type: "workflows",
|
|
490
|
+
parameters: {
|
|
491
|
+
...wf.on_creation === false ? { do_not_enforce_on_create: true } : {},
|
|
492
|
+
workflows: wf.required
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
for (const [field, ruleType] of [
|
|
497
|
+
["commit_message", "commit_message_pattern"],
|
|
498
|
+
["commit_author_email", "commit_author_email_pattern"],
|
|
499
|
+
["committer_email", "committer_email_pattern"]
|
|
500
|
+
]) {
|
|
501
|
+
const patterns = ruleset[field];
|
|
502
|
+
if (patterns !== void 0) for (const entry of patterns) rules.push({
|
|
503
|
+
type: ruleType,
|
|
504
|
+
parameters: entry
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
if (ruleset.type === "branch" && ruleset.branch_name !== void 0) for (const entry of ruleset.branch_name) rules.push({
|
|
508
|
+
type: "branch_name_pattern",
|
|
509
|
+
parameters: entry
|
|
510
|
+
});
|
|
511
|
+
if (ruleset.type === "tag" && ruleset.tag_name !== void 0) for (const entry of ruleset.tag_name) rules.push({
|
|
512
|
+
type: "tag_name_pattern",
|
|
513
|
+
parameters: entry
|
|
514
|
+
});
|
|
515
|
+
let conditions = ruleset.conditions;
|
|
516
|
+
if (ruleset.targets !== void 0) if (ruleset.targets === "default") conditions = { ref_name: {
|
|
517
|
+
include: ["~DEFAULT_BRANCH"],
|
|
518
|
+
exclude: []
|
|
519
|
+
} };
|
|
520
|
+
else if (ruleset.targets === "all") conditions = { ref_name: {
|
|
521
|
+
include: ["~ALL"],
|
|
522
|
+
exclude: []
|
|
523
|
+
} };
|
|
524
|
+
else {
|
|
525
|
+
const include = [];
|
|
526
|
+
const exclude = [];
|
|
527
|
+
for (const pattern of ruleset.targets) if ("include" in pattern) include.push(pattern.include);
|
|
528
|
+
else exclude.push(pattern.exclude);
|
|
529
|
+
conditions = { ref_name: {
|
|
530
|
+
include,
|
|
531
|
+
exclude
|
|
532
|
+
} };
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
name: ruleset.name,
|
|
536
|
+
target: ruleset.type,
|
|
537
|
+
enforcement: ruleset.enforcement,
|
|
538
|
+
...conditions !== void 0 ? { conditions } : {},
|
|
539
|
+
...ruleset.bypass_actors !== void 0 ? { bypass_actors: ruleset.bypass_actors } : {},
|
|
540
|
+
...rules.length > 0 ? { rules } : {}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
//#endregion
|
|
545
|
+
export { BypassActorSchema, ResolvedRefSchema, RulesetSchema, buildRulesetPayload };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { ConfigSchema } from "../schemas/config.js";
|
|
2
|
+
import { CredentialsSchema } from "../schemas/credentials.js";
|
|
3
|
+
import { AppDirsConfig, ConfigError, ConfigFile, ExplicitPath, FirstMatch, StaticDir, TomlCodec, UpwardWalk, XdgConfigLive, XdgConfigResolver, XdgSavePath } from "xdg-effect";
|
|
4
|
+
import { Effect, Option } from "effect";
|
|
5
|
+
import { existsSync, statSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
//#region src/services/ConfigFiles.ts
|
|
8
|
+
const CONFIG_FILENAME = "reposets.config.toml";
|
|
9
|
+
const CREDENTIALS_FILENAME = "reposets.credentials.toml";
|
|
10
|
+
const ReposetsConfigFile = ConfigFile.Tag("reposets/Config");
|
|
11
|
+
const ReposetsCredentialsFile = ConfigFile.Tag("reposets/Credentials");
|
|
12
|
+
/**
|
|
13
|
+
* Validates all internal cross-references in a parsed config. Checks that
|
|
14
|
+
* every group's settings, secrets, variables, rulesets, and environments
|
|
15
|
+
* references point to defined top-level sections, and that environment-scoped
|
|
16
|
+
* secret/variable groups reference defined environments. Collects ALL errors
|
|
17
|
+
* into a single ConfigError.
|
|
18
|
+
*/
|
|
19
|
+
function validateConfigRefs(config) {
|
|
20
|
+
const errors = [];
|
|
21
|
+
const definedSettings = new Set(Object.keys(config.settings));
|
|
22
|
+
const definedSecrets = new Set(Object.keys(config.secrets));
|
|
23
|
+
const definedVariables = new Set(Object.keys(config.variables));
|
|
24
|
+
const definedRulesets = new Set(Object.keys(config.rulesets));
|
|
25
|
+
const definedEnvironments = new Set(Object.keys(config.environments));
|
|
26
|
+
const definedSecurity = new Set(Object.keys(config.security));
|
|
27
|
+
const definedCodeScanning = new Set(Object.keys(config.code_scanning));
|
|
28
|
+
for (const [groupName, group] of Object.entries(config.groups)) {
|
|
29
|
+
if (group.settings) {
|
|
30
|
+
for (const ref of group.settings) if (!definedSettings.has(ref)) errors.push(`group '${groupName}': unknown settings group '${ref}'`);
|
|
31
|
+
}
|
|
32
|
+
if (group.rulesets) {
|
|
33
|
+
for (const ref of group.rulesets) if (!definedRulesets.has(ref)) errors.push(`group '${groupName}': unknown ruleset '${ref}'`);
|
|
34
|
+
}
|
|
35
|
+
if (group.environments) {
|
|
36
|
+
for (const ref of group.environments) if (!definedEnvironments.has(ref)) errors.push(`group '${groupName}': unknown environment '${ref}'`);
|
|
37
|
+
}
|
|
38
|
+
if (group.security) {
|
|
39
|
+
for (const ref of group.security) if (!definedSecurity.has(ref)) errors.push(`group '${groupName}': unknown security group '${ref}'`);
|
|
40
|
+
}
|
|
41
|
+
if (group.code_scanning) {
|
|
42
|
+
for (const ref of group.code_scanning) if (!definedCodeScanning.has(ref)) errors.push(`group '${groupName}': unknown code_scanning group '${ref}'`);
|
|
43
|
+
}
|
|
44
|
+
if (group.secrets) {
|
|
45
|
+
if (group.secrets.actions) {
|
|
46
|
+
for (const ref of group.secrets.actions) if (!definedSecrets.has(ref)) errors.push(`group '${groupName}': unknown secrets group '${ref}'`);
|
|
47
|
+
}
|
|
48
|
+
if (group.secrets.dependabot) {
|
|
49
|
+
for (const ref of group.secrets.dependabot) if (!definedSecrets.has(ref)) errors.push(`group '${groupName}': unknown secrets group '${ref}'`);
|
|
50
|
+
}
|
|
51
|
+
if (group.secrets.codespaces) {
|
|
52
|
+
for (const ref of group.secrets.codespaces) if (!definedSecrets.has(ref)) errors.push(`group '${groupName}': unknown secrets group '${ref}'`);
|
|
53
|
+
}
|
|
54
|
+
if (group.secrets.environments) for (const [envName, secretGroups] of Object.entries(group.secrets.environments)) {
|
|
55
|
+
if (!definedEnvironments.has(envName)) errors.push(`group '${groupName}': unknown environment '${envName}' in secrets.environments`);
|
|
56
|
+
for (const ref of secretGroups) if (!definedSecrets.has(ref)) errors.push(`group '${groupName}': in secrets.environments.'${envName}': unknown secrets group '${ref}'`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (group.variables) {
|
|
60
|
+
if (group.variables.actions) {
|
|
61
|
+
for (const ref of group.variables.actions) if (!definedVariables.has(ref)) errors.push(`group '${groupName}': unknown variables group '${ref}'`);
|
|
62
|
+
}
|
|
63
|
+
if (group.variables.environments) for (const [envName, varGroups] of Object.entries(group.variables.environments)) {
|
|
64
|
+
if (!definedEnvironments.has(envName)) errors.push(`group '${groupName}': unknown environment '${envName}' in variables.environments`);
|
|
65
|
+
for (const ref of varGroups) if (!definedVariables.has(ref)) errors.push(`group '${groupName}': in variables.environments.'${envName}': unknown variables group '${ref}'`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (errors.length > 0) return Effect.fail(new ConfigError({
|
|
70
|
+
operation: "validate",
|
|
71
|
+
reason: errors.join("\n")
|
|
72
|
+
}));
|
|
73
|
+
return Effect.succeed(config);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Creates a live Layer providing both config and credentials file services.
|
|
77
|
+
* When configFlag is Some and points to a directory, prepends StaticDir resolver.
|
|
78
|
+
* When configFlag is Some and points to a file, prepends ExplicitPath resolver.
|
|
79
|
+
* Always includes UpwardWalk + XdgConfigResolver as fallback resolvers.
|
|
80
|
+
* Passes validateConfigRefs as the validate callback on the config spec.
|
|
81
|
+
*/
|
|
82
|
+
function makeConfigFilesLive(configFlag) {
|
|
83
|
+
const configResolvers = [];
|
|
84
|
+
if (Option.isSome(configFlag)) {
|
|
85
|
+
const flag = configFlag.value;
|
|
86
|
+
if (existsSync(flag) && statSync(flag).isDirectory()) configResolvers.push(StaticDir({
|
|
87
|
+
dir: flag,
|
|
88
|
+
filename: CONFIG_FILENAME
|
|
89
|
+
}));
|
|
90
|
+
else configResolvers.push(ExplicitPath(flag));
|
|
91
|
+
}
|
|
92
|
+
configResolvers.push(UpwardWalk({ filename: CONFIG_FILENAME }), XdgConfigResolver({ filename: CONFIG_FILENAME }));
|
|
93
|
+
return XdgConfigLive.multi({
|
|
94
|
+
app: new AppDirsConfig({ namespace: "reposets" }),
|
|
95
|
+
configs: [{
|
|
96
|
+
tag: ReposetsConfigFile,
|
|
97
|
+
schema: ConfigSchema,
|
|
98
|
+
codec: TomlCodec,
|
|
99
|
+
strategy: FirstMatch,
|
|
100
|
+
resolvers: configResolvers,
|
|
101
|
+
validate: validateConfigRefs
|
|
102
|
+
}, {
|
|
103
|
+
tag: ReposetsCredentialsFile,
|
|
104
|
+
schema: CredentialsSchema,
|
|
105
|
+
codec: TomlCodec,
|
|
106
|
+
strategy: FirstMatch,
|
|
107
|
+
resolvers: [UpwardWalk({ filename: CREDENTIALS_FILENAME }), XdgConfigResolver({ filename: CREDENTIALS_FILENAME })],
|
|
108
|
+
defaultPath: XdgSavePath(CREDENTIALS_FILENAME)
|
|
109
|
+
}]
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Default ConfigFiles layer with no --config flag override.
|
|
114
|
+
* Used by CLI entrypoint when no flag is provided.
|
|
115
|
+
*/
|
|
116
|
+
const ConfigFilesLive = makeConfigFilesLive(Option.none());
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
export { CONFIG_FILENAME, CREDENTIALS_FILENAME, ConfigFilesLive, ReposetsConfigFile, ReposetsCredentialsFile, makeConfigFilesLive, validateConfigRefs };
|