reposets 0.4.2 → 1.0.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.
Files changed (54) hide show
  1. package/README.md +87 -75
  2. package/bin/reposets.js +68 -17
  3. package/cli/commands/credentials.js +170 -49
  4. package/cli/commands/doctor.js +364 -91
  5. package/cli/commands/drift.js +48 -0
  6. package/cli/commands/history.js +203 -0
  7. package/cli/commands/init.js +110 -104
  8. package/cli/commands/list.js +60 -39
  9. package/cli/commands/nuke.js +127 -0
  10. package/cli/commands/sync.js +219 -59
  11. package/cli/commands/validate.js +57 -41
  12. package/cli/flags.js +36 -0
  13. package/cli/logger.js +48 -0
  14. package/index.d.ts +471 -1499
  15. package/index.js +3 -18
  16. package/lib/config-refs.js +76 -0
  17. package/lib/credential-labels.js +0 -0
  18. package/lib/fingerprint.js +52 -0
  19. package/lib/org-only.js +61 -0
  20. package/lib/schema-issues.js +50 -0
  21. package/package.json +11 -10
  22. package/schemas/annotations.js +81 -0
  23. package/schemas/common.js +83 -48
  24. package/schemas/config.js +214 -212
  25. package/schemas/credentials.js +190 -54
  26. package/schemas/environment.js +27 -21
  27. package/schemas/ruleset.js +235 -139
  28. package/services/ConfigFiles.js +126 -104
  29. package/services/CredentialResolver.js +97 -33
  30. package/services/OnePasswordClient.js +88 -16
  31. package/services/SyncLogger.js +107 -76
  32. package/store/AppliedState.js +0 -0
  33. package/store/RepoCache.js +86 -0
  34. package/store/SyncJournal.js +92 -0
  35. package/store/migrations.js +86 -0
  36. package/sync/SyncEngine.js +156 -0
  37. package/sync/decide.js +56 -0
  38. package/sync/phase.js +55 -0
  39. package/sync/phases/cleanup.js +220 -0
  40. package/sync/phases/code-scanning.js +187 -0
  41. package/sync/phases/environments.js +106 -0
  42. package/sync/phases/index.js +39 -0
  43. package/sync/phases/resource.js +149 -0
  44. package/sync/phases/rulesets.js +186 -0
  45. package/sync/phases/secrets.js +138 -0
  46. package/sync/phases/security.js +129 -0
  47. package/sync/phases/settings.js +274 -0
  48. package/sync/phases/variables.js +132 -0
  49. package/tsdoc-metadata.json +1 -1
  50. package/bin/reposets.d.ts +0 -1
  51. package/errors.js +0 -12
  52. package/lib/crypto.js +0 -27
  53. package/services/GitHubClient.js +0 -875
  54. package/services/SyncEngine.js +0 -580
package/schemas/config.js CHANGED
@@ -1,160 +1,170 @@
1
+ import { docs, taplo, tombi } from "./annotations.js";
1
2
  import { CleanupSchema, SecretGroupSchema, VariableGroupSchema } from "./common.js";
2
3
  import { EnvironmentSchema } from "./environment.js";
3
4
  import { RulesetSchema } from "./ruleset.js";
4
- import { Jsonifiable, taplo, tombi } from "xdg-effect";
5
- import { Schema } from "effect";
5
+ import { Effect, Schema } from "effect";
6
6
 
7
7
  //#region src/schemas/config.ts
8
+ /**
9
+ * Which secret groups apply to which GitHub secret scopes.
10
+ *
11
+ * @public
12
+ */
8
13
  const SecretScopesSchema = Schema.Struct({
9
- actions: Schema.optional(Schema.Array(Schema.String).annotations({
14
+ actions: Schema.optional(Schema.Array(Schema.String).annotate({
10
15
  title: "Action secret groups",
11
16
  description: "Secret groups to sync as GitHub Actions repository secrets",
12
17
  examples: [["deploy", "app"]]
13
18
  })),
14
- dependabot: Schema.optional(Schema.Array(Schema.String).annotations({
19
+ dependabot: Schema.optional(Schema.Array(Schema.String).annotate({
15
20
  title: "Dependabot secret groups",
16
21
  description: "Secret groups to sync as Dependabot secrets",
17
22
  examples: [["deploy"]]
18
23
  })),
19
- codespaces: Schema.optional(Schema.Array(Schema.String).annotations({
24
+ codespaces: Schema.optional(Schema.Array(Schema.String).annotate({
20
25
  title: "Codespaces secret groups",
21
26
  description: "Secret groups to sync as Codespaces secrets",
22
27
  examples: [["deploy"]]
23
28
  })),
24
- environments: Schema.optional(Schema.Record({
25
- key: Schema.String,
26
- value: Schema.Array(Schema.String).annotations({
27
- title: "Environment secret groups",
28
- description: "Secret groups to sync as environment secrets"
29
- })
30
- }).annotations({
29
+ environments: Schema.optional(Schema.Record(Schema.String, Schema.Array(Schema.String).annotate({
30
+ title: "Environment secret groups",
31
+ description: "Secret groups to sync as environment secrets"
32
+ })).annotate({
33
+ ...tombi({ additionalKeyLabel: "environment_name" }),
31
34
  title: "Environment secret scopes",
32
- description: "Map of environment names to secret group references",
33
- jsonSchema: tombi({ additionalKeyLabel: "environment_name" })
35
+ description: "Map of environment names to secret group references"
34
36
  }))
35
- }).annotations({
37
+ }).annotate({
36
38
  identifier: "SecretScopes",
37
39
  title: "Secret scopes",
38
40
  description: "Assign secret groups to GitHub secret scopes (actions, dependabot, codespaces, environments)"
39
41
  });
42
+ /**
43
+ * Which variable groups apply to which GitHub variable scopes.
44
+ *
45
+ * @public
46
+ */
40
47
  const VariableScopesSchema = Schema.Struct({
41
- actions: Schema.optional(Schema.Array(Schema.String).annotations({
48
+ actions: Schema.optional(Schema.Array(Schema.String).annotate({
42
49
  title: "Action variable groups",
43
50
  description: "Variable groups to sync as GitHub Actions repository variables",
44
51
  examples: [["common"]]
45
52
  })),
46
- environments: Schema.optional(Schema.Record({
47
- key: Schema.String,
48
- value: Schema.Array(Schema.String).annotations({
49
- title: "Environment variable groups",
50
- description: "Variable groups to sync as environment variables"
51
- })
52
- }).annotations({
53
+ environments: Schema.optional(Schema.Record(Schema.String, Schema.Array(Schema.String).annotate({
54
+ title: "Environment variable groups",
55
+ description: "Variable groups to sync as environment variables"
56
+ })).annotate({
57
+ ...tombi({ additionalKeyLabel: "environment_name" }),
53
58
  title: "Environment variable scopes",
54
- description: "Map of environment names to variable group references",
55
- jsonSchema: tombi({ additionalKeyLabel: "environment_name" })
59
+ description: "Map of environment names to variable group references"
56
60
  }))
57
- }).annotations({
61
+ }).annotate({
58
62
  identifier: "VariableScopes",
59
63
  title: "Variable scopes",
60
64
  description: "Assign variable groups to GitHub variable scopes (actions, environments)"
61
65
  });
66
+ /**
67
+ * A named group of repositories and the resources assigned to them.
68
+ *
69
+ * @public
70
+ */
62
71
  const GroupSchema = Schema.Struct({
63
- owner: Schema.optional(Schema.String.annotations({
64
- title: "Owner override",
65
- description: "GitHub user or organization that owns these repos. Overrides the top-level owner.",
66
- examples: ["savvy-web"]
67
- })),
68
- repos: Schema.Array(Schema.String).annotations({
72
+ repos: Schema.Array(Schema.String).annotate({
73
+ ...tombi({ arrayValuesOrder: "ascending" }),
69
74
  title: "Repository names",
70
75
  description: "List of repository names (without owner prefix) to sync in this group",
71
76
  examples: [[
72
77
  "repo-one",
73
78
  "repo-two",
74
79
  "repo-three"
75
- ]],
76
- jsonSchema: tombi({ arrayValuesOrder: "ascending" })
80
+ ]]
77
81
  }),
78
- credentials: Schema.optional(Schema.String.annotations({
82
+ credentials: Schema.String.annotate({
79
83
  title: "Credential profile",
80
- description: "Name of the credential profile to use. If only one profile exists, it is used automatically.",
84
+ description: "Name of the credential profile this group authenticates as. Required.",
81
85
  examples: ["personal", "work"]
82
- })),
83
- settings: Schema.optional(Schema.Array(Schema.String).annotations({
86
+ }),
87
+ settings: Schema.optional(Schema.Array(Schema.String).annotate({
84
88
  title: "Settings groups",
85
89
  description: "Names of settings groups to apply to these repos",
86
90
  examples: [["oss-defaults"]]
87
91
  })),
88
- environments: Schema.optional(Schema.Array(Schema.String).annotations({
92
+ environments: Schema.optional(Schema.Array(Schema.String).annotate({
89
93
  title: "Environments",
90
94
  description: "Names of environment definitions to create/update for these repos",
91
95
  examples: [["staging", "production"]]
92
96
  })),
93
97
  secrets: Schema.optional(SecretScopesSchema),
94
98
  variables: Schema.optional(VariableScopesSchema),
95
- rulesets: Schema.optional(Schema.Array(Schema.String).annotations({
99
+ rulesets: Schema.optional(Schema.Array(Schema.String).annotate({
96
100
  title: "Rulesets",
97
101
  description: "Names of rulesets to apply to these repos",
98
102
  examples: [["workflow", "release"]]
99
103
  })),
100
- security: Schema.optional(Schema.Array(Schema.String).annotations({
104
+ security: Schema.optional(Schema.Array(Schema.String).annotate({
101
105
  title: "Security groups",
102
106
  description: "Names of security groups (vulnerability alerts, automated security fixes, private vulnerability reporting) to apply to these repos",
103
107
  examples: [["oss-defaults"]]
104
108
  })),
105
- code_scanning: Schema.optional(Schema.Array(Schema.String).annotations({
109
+ code_scanning: Schema.optional(Schema.Array(Schema.String).annotate({
106
110
  title: "Code scanning groups",
107
111
  description: "Names of code_scanning groups (CodeQL default setup) to apply to these repos",
108
112
  examples: [["oss-defaults"]]
109
113
  })),
110
114
  cleanup: Schema.optional(CleanupSchema)
111
- }).annotations({
115
+ }).annotate({
116
+ ...tombi({ tableKeysOrder: "schema" }),
117
+ ...taplo({
118
+ initKeys: ["repos"],
119
+ links: { key: docs("03-configuration.md") }
120
+ }),
112
121
  identifier: "Group",
113
122
  title: "Repository group",
114
- description: "A named group of repositories with their resource assignments",
115
- jsonSchema: {
116
- ...tombi({ tableKeysOrder: "schema" }),
117
- ...taplo({
118
- initKeys: ["repos"],
119
- links: { key: "https://github.com/spencerbeggs/reposets/blob/main/docs/configuration.md" }
120
- })
121
- }
123
+ description: "A named group of repositories with their resource assignments"
122
124
  });
123
- const SquashMergeCommitTitleSchema = Schema.Literal("PR_TITLE", "COMMIT_OR_PR_TITLE").annotations({
125
+ const SquashMergeCommitTitleSchema = Schema.Literals(["PR_TITLE", "COMMIT_OR_PR_TITLE"]).annotate({
124
126
  title: "Squash merge commit title",
125
127
  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"
126
128
  });
127
- const SquashMergeCommitMessageSchema = Schema.Literal("PR_BODY", "COMMIT_MESSAGES", "BLANK").annotations({
129
+ const SquashMergeCommitMessageSchema = Schema.Literals([
130
+ "PR_BODY",
131
+ "COMMIT_MESSAGES",
132
+ "BLANK"
133
+ ]).annotate({
128
134
  title: "Squash merge commit message",
129
135
  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"
130
136
  });
131
- const MergeCommitTitleSchema = Schema.Literal("PR_TITLE", "MERGE_MESSAGE").annotations({
137
+ const MergeCommitTitleSchema = Schema.Literals(["PR_TITLE", "MERGE_MESSAGE"]).annotate({
132
138
  title: "Merge commit title",
133
139
  description: "Default title for merge commits: PR_TITLE uses the pull request title, MERGE_MESSAGE uses the classic merge message"
134
140
  });
135
- const MergeCommitMessageSchema = Schema.Literal("PR_BODY", "PR_TITLE", "BLANK").annotations({
141
+ const MergeCommitMessageSchema = Schema.Literals([
142
+ "PR_BODY",
143
+ "PR_TITLE",
144
+ "BLANK"
145
+ ]).annotate({
136
146
  title: "Merge commit message",
137
147
  description: "Default message body for merge commits: PR_BODY uses the pull request body, PR_TITLE uses the PR title, BLANK leaves it empty"
138
148
  });
139
- const SecurityAndAnalysisStatusSchema = Schema.Literal("enabled", "disabled").annotations({
149
+ const SecurityAndAnalysisStatusSchema = Schema.Literals(["enabled", "disabled"]).annotate({
140
150
  identifier: "SecurityAndAnalysisStatus",
141
151
  title: "Security feature status",
142
152
  description: "Whether the security feature is \"enabled\" or \"disabled\""
143
153
  });
144
- const DelegatedBypassReviewerModeSchema = Schema.Literal("ALWAYS", "EXEMPT").annotations({
154
+ const DelegatedBypassReviewerModeSchema = Schema.Literals(["ALWAYS", "EXEMPT"]).annotate({
145
155
  identifier: "DelegatedBypassReviewerMode",
146
156
  title: "Delegated bypass reviewer mode",
147
157
  description: "ALWAYS: reviewer is always required to approve bypass; EXEMPT: reviewer can bypass without review"
148
158
  });
149
- const DelegatedBypassReviewerSchema = Schema.Union(Schema.Struct({
150
- team: Schema.String.annotations({
159
+ const DelegatedBypassReviewerSchema = Schema.Union([Schema.Struct({
160
+ team: Schema.String.annotate({
151
161
  title: "Team slug",
152
162
  description: "GitHub team slug (e.g., \"security-team\"); resolved to numeric reviewer_id at sync time",
153
163
  examples: ["security-team"]
154
164
  }),
155
165
  mode: Schema.optional(DelegatedBypassReviewerModeSchema)
156
166
  }), Schema.Struct({
157
- role: Schema.String.annotations({
167
+ role: Schema.String.annotate({
158
168
  title: "Organization role name",
159
169
  description: "Organization role name as defined in `GET /orgs/{org}/organization-roles` (e.g., \"all_repo_admin\", \"security_manager\"). Resolved to the numeric role ID at sync time.",
160
170
  examples: [
@@ -164,111 +174,123 @@ const DelegatedBypassReviewerSchema = Schema.Union(Schema.Struct({
164
174
  ]
165
175
  }),
166
176
  mode: Schema.optional(DelegatedBypassReviewerModeSchema)
167
- })).annotations({
177
+ })]).annotate({
168
178
  identifier: "DelegatedBypassReviewer",
169
179
  title: "Delegated bypass reviewer",
170
180
  description: "A reviewer who can approve secret-scanning push-protection bypass requests. Must specify exactly one of team or role."
171
181
  });
172
182
  const SecurityAndAnalysisSchema = Schema.Struct({
173
- advanced_security: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
183
+ advanced_security: Schema.optional(SecurityAndAnalysisStatusSchema.annotate({
174
184
  title: "GitHub Advanced Security",
175
185
  description: "(GHAS-licensed) Master toggle for GitHub Advanced Security features. Free on public repos; requires a GHAS license on private repos."
176
186
  })),
177
- code_security: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
187
+ code_security: Schema.optional(SecurityAndAnalysisStatusSchema.annotate({
178
188
  title: "GitHub Code Security",
179
189
  description: "(GHAS-licensed) Toggle GitHub Code Security functionality."
180
190
  })),
181
- secret_scanning: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
191
+ secret_scanning: Schema.optional(SecurityAndAnalysisStatusSchema.annotate({
182
192
  title: "Secret scanning",
183
193
  description: "Detect exposed credentials and sensitive data committed to the repository."
184
194
  })),
185
- secret_scanning_push_protection: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
195
+ secret_scanning_push_protection: Schema.optional(SecurityAndAnalysisStatusSchema.annotate({
186
196
  title: "Secret scanning push protection",
187
197
  description: "Block git pushes that contain detected secrets."
188
198
  })),
189
- secret_scanning_ai_detection: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
199
+ secret_scanning_ai_detection: Schema.optional(SecurityAndAnalysisStatusSchema.annotate({
190
200
  title: "Secret scanning AI detection",
191
201
  description: "(GHAS-licensed) AI-powered detection of generic secrets beyond standard provider patterns."
192
202
  })),
193
- secret_scanning_non_provider_patterns: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
203
+ secret_scanning_non_provider_patterns: Schema.optional(SecurityAndAnalysisStatusSchema.annotate({
194
204
  title: "Secret scanning non-provider patterns",
195
205
  description: "(GHAS-licensed) Detect custom secret patterns beyond the standard provider list."
196
206
  })),
197
- secret_scanning_delegated_alert_dismissal: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
207
+ secret_scanning_delegated_alert_dismissal: Schema.optional(SecurityAndAnalysisStatusSchema.annotate({
198
208
  title: "Delegated alert dismissal",
199
209
  description: "(org-only) Allow delegated dismissal of secret scanning alerts."
200
210
  })),
201
- secret_scanning_delegated_bypass: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
211
+ secret_scanning_delegated_bypass: Schema.optional(SecurityAndAnalysisStatusSchema.annotate({
202
212
  title: "Delegated push protection bypass",
203
213
  description: "(org-only) Allow delegated approval of secret scanning push protection bypass requests."
204
214
  })),
205
- delegated_bypass_reviewers: Schema.optional(Schema.Array(DelegatedBypassReviewerSchema).annotations({
215
+ delegated_bypass_reviewers: Schema.optional(Schema.Array(DelegatedBypassReviewerSchema).annotate({
206
216
  title: "Delegated bypass reviewers",
207
217
  description: "(org-only) Reviewers authorized to approve push protection bypass requests. Each entry must specify a team slug or role name."
208
218
  })),
209
- dependabot_security_updates: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
219
+ dependabot_security_updates: Schema.optional(SecurityAndAnalysisStatusSchema.annotate({
210
220
  title: "Dependabot security updates",
211
221
  description: "Automatically open pull requests to patch known dependency vulnerabilities."
212
222
  }))
213
- }).annotations({
223
+ }).annotate({
224
+ ...tombi({ tableKeysOrder: "schema" }),
225
+ ...taplo({ links: { key: docs("03-configuration.md") } }),
214
226
  identifier: "SecurityAndAnalysis",
215
227
  title: "Security and analysis",
216
- description: "GitHub repository security_and_analysis fields applied via the same PATCH /repos call as other settings. (GHAS-licensed) fields require a GHAS license on private repos; (org-only) fields are silently skipped on personal repos.",
217
- jsonSchema: {
218
- ...tombi({ tableKeysOrder: "schema" }),
219
- ...taplo({ links: { key: "https://github.com/spencerbeggs/reposets/blob/main/docs/configuration.md" } })
220
- }
228
+ description: "GitHub repository security_and_analysis fields applied via the same PATCH /repos call as other settings. (GHAS-licensed) fields require a GHAS license on private repos; (org-only) fields are silently skipped on personal repos."
221
229
  });
222
- const SettingsGroupSchema = Schema.Struct({
223
- is_template: Schema.optional(Schema.Boolean.annotations({
230
+ /**
231
+ * Repository settings to apply: known fields typed, unknown fields passed
232
+ * through to the API.
233
+ *
234
+ * @remarks
235
+ * v3 spelled the pass-through as `Schema.Struct(fields, { key, value })` — a
236
+ * second argument that v4's `Struct` no longer takes. `StructWithRest` is the
237
+ * replacement, and it composes the same way: the decoded type is the struct
238
+ * intersected with the record. GitHub adds repository settings faster than this
239
+ * schema tracks them, and an unknown key should reach the API rather than fail
240
+ * validation.
241
+ *
242
+ * @public
243
+ */
244
+ const SettingsGroupSchema = Schema.StructWithRest(Schema.Struct({
245
+ is_template: Schema.optional(Schema.Boolean.annotate({
224
246
  title: "Template repository",
225
247
  description: "Whether the repository is a template that can be used to generate new repositories"
226
248
  })),
227
- has_wiki: Schema.optional(Schema.Boolean.annotations({
249
+ has_wiki: Schema.optional(Schema.Boolean.annotate({
228
250
  title: "Wikis",
229
251
  description: "Enable the wiki feature for the repository"
230
252
  })),
231
- has_issues: Schema.optional(Schema.Boolean.annotations({
253
+ has_issues: Schema.optional(Schema.Boolean.annotate({
232
254
  title: "Issues",
233
255
  description: "Enable the issues feature for the repository"
234
256
  })),
235
- has_projects: Schema.optional(Schema.Boolean.annotations({
257
+ has_projects: Schema.optional(Schema.Boolean.annotate({
236
258
  title: "Projects",
237
259
  description: "Enable the projects feature for the repository"
238
260
  })),
239
- has_discussions: Schema.optional(Schema.Boolean.annotations({
261
+ has_discussions: Schema.optional(Schema.Boolean.annotate({
240
262
  title: "Discussions",
241
263
  description: "Enable the discussions feature for the repository"
242
264
  })),
243
- has_sponsorships: Schema.optional(Schema.Boolean.annotations({
265
+ has_sponsorships: Schema.optional(Schema.Boolean.annotate({
244
266
  title: "Sponsorships",
245
267
  description: "Display a Sponsor button for the repository (synced via GraphQL)"
246
268
  })),
247
- has_pull_requests: Schema.optional(Schema.Boolean.annotations({
269
+ has_pull_requests: Schema.optional(Schema.Boolean.annotate({
248
270
  title: "Pull requests",
249
271
  description: "Enable the pull requests feature for the repository (synced via GraphQL)"
250
272
  })),
251
- allow_forking: Schema.optional(Schema.Boolean.annotations({
273
+ allow_forking: Schema.optional(Schema.Boolean.annotate({
252
274
  title: "Allow forking",
253
275
  description: "Allow forking of the repository"
254
276
  })),
255
- allow_merge_commit: Schema.optional(Schema.Boolean.annotations({
277
+ allow_merge_commit: Schema.optional(Schema.Boolean.annotate({
256
278
  title: "Allow merge commits",
257
279
  description: "Allow merge commits when merging pull requests"
258
280
  })),
259
- allow_squash_merge: Schema.optional(Schema.Boolean.annotations({
281
+ allow_squash_merge: Schema.optional(Schema.Boolean.annotate({
260
282
  title: "Allow squash merging",
261
283
  description: "Allow squash merging when merging pull requests"
262
284
  })),
263
- allow_rebase_merge: Schema.optional(Schema.Boolean.annotations({
285
+ allow_rebase_merge: Schema.optional(Schema.Boolean.annotate({
264
286
  title: "Allow rebase merging",
265
287
  description: "Allow rebase merging when merging pull requests"
266
288
  })),
267
- allow_auto_merge: Schema.optional(Schema.Boolean.annotations({
289
+ allow_auto_merge: Schema.optional(Schema.Boolean.annotate({
268
290
  title: "Allow auto-merge",
269
291
  description: "Allow pull requests to be automatically merged once all requirements are met"
270
292
  })),
271
- allow_update_branch: Schema.optional(Schema.Boolean.annotations({
293
+ allow_update_branch: Schema.optional(Schema.Boolean.annotate({
272
294
  title: "Always suggest updating pull request branches",
273
295
  description: "Show the update branch button on pull requests"
274
296
  })),
@@ -276,80 +298,95 @@ const SettingsGroupSchema = Schema.Struct({
276
298
  squash_merge_commit_message: Schema.optional(SquashMergeCommitMessageSchema),
277
299
  merge_commit_title: Schema.optional(MergeCommitTitleSchema),
278
300
  merge_commit_message: Schema.optional(MergeCommitMessageSchema),
279
- delete_branch_on_merge: Schema.optional(Schema.Boolean.annotations({
301
+ delete_branch_on_merge: Schema.optional(Schema.Boolean.annotate({
280
302
  title: "Automatically delete head branches",
281
303
  description: "Automatically delete head branches after pull requests are merged"
282
304
  })),
283
- web_commit_signoff_required: Schema.optional(Schema.Boolean.annotations({
305
+ web_commit_signoff_required: Schema.optional(Schema.Boolean.annotate({
284
306
  title: "Require commit signoff",
285
307
  description: "Require contributors to sign off on web-based commits"
286
308
  })),
287
309
  security_and_analysis: Schema.optional(SecurityAndAnalysisSchema)
288
- }, {
289
- key: Schema.String,
290
- value: Jsonifiable
291
- }).annotations({
310
+ }), [Schema.Record(Schema.String, Schema.Json)]).annotate({
311
+ ...tombi({ tableKeysOrder: "schema" }),
312
+ ...taplo({ links: { key: docs("03-configuration.md") } }),
292
313
  identifier: "SettingsGroup",
293
314
  title: "Settings group",
294
- description: "GitHub repository settings to apply. Known fields are typed; additional fields are passed through to the API.",
295
- jsonSchema: {
296
- ...tombi({ tableKeysOrder: "schema" }),
297
- ...taplo({ links: { key: "https://github.com/spencerbeggs/reposets/blob/main/docs/configuration.md" } })
298
- }
315
+ description: "GitHub repository settings to apply. Known fields are typed; additional fields are passed through to the API."
299
316
  });
317
+ /**
318
+ * Repository security toggles that have dedicated PUT/DELETE endpoints.
319
+ *
320
+ * @public
321
+ */
300
322
  const SecurityGroupSchema = Schema.Struct({
301
- vulnerability_alerts: Schema.optional(Schema.Boolean.annotations({
323
+ vulnerability_alerts: Schema.optional(Schema.Boolean.annotate({
302
324
  title: "Vulnerability alerts",
303
325
  description: "Enable Dependabot vulnerability alerts (PUT/DELETE /repos/{o}/{r}/vulnerability-alerts)."
304
326
  })),
305
- automated_security_fixes: Schema.optional(Schema.Boolean.annotations({
327
+ automated_security_fixes: Schema.optional(Schema.Boolean.annotate({
306
328
  title: "Automated security fixes",
307
329
  description: "Enable Dependabot security pull requests (PUT/DELETE /repos/{o}/{r}/automated-security-fixes). Requires vulnerability_alerts to also be enabled."
308
330
  })),
309
- private_vulnerability_reporting: Schema.optional(Schema.Boolean.annotations({
331
+ private_vulnerability_reporting: Schema.optional(Schema.Boolean.annotate({
310
332
  title: "Private vulnerability reporting",
311
333
  description: "Enable the private vulnerability reporting inbox (PUT/DELETE /repos/{o}/{r}/private-vulnerability-reporting)."
312
334
  }))
313
- }).pipe(Schema.filter((group) => !(group.automated_security_fixes === true && group.vulnerability_alerts === false), {
314
- identifier: "SecurityGroup",
315
- message: () => "automated_security_fixes = true requires vulnerability_alerts to be enabled (or omitted to leave the existing setting in place)"
316
- })).annotations({
335
+ }).annotate({
336
+ ...tombi({ tableKeysOrder: "schema" }),
337
+ ...taplo({ links: { key: docs("03-configuration.md") } }),
317
338
  identifier: "SecurityGroup",
318
339
  title: "Security group",
319
- description: "Toggles for repository-level security features that have dedicated PUT/DELETE endpoints (vulnerability alerts, automated security fixes, private vulnerability reporting). Omitted keys are left untouched.",
320
- jsonSchema: {
321
- ...tombi({ tableKeysOrder: "schema" }),
322
- ...taplo({ links: { key: "https://github.com/spencerbeggs/reposets/blob/main/docs/configuration.md" } })
323
- }
324
- });
325
- const CodeScanningLanguageSchema = Schema.Literal("actions", "c-cpp", "csharp", "go", "java-kotlin", "javascript-typescript", "python", "ruby", "swift").annotations({
340
+ description: "Toggles for repository-level security features that have dedicated PUT/DELETE endpoints (vulnerability alerts, automated security fixes, private vulnerability reporting). Omitted keys are left untouched."
341
+ }).check(Schema.makeFilter((group) => group.automated_security_fixes === true && group.vulnerability_alerts === false ? "automated_security_fixes = true requires vulnerability_alerts to be enabled (or omitted to leave the existing setting in place)" : void 0));
342
+ /**
343
+ * A language GitHub code scanning default setup supports.
344
+ *
345
+ * @public
346
+ */
347
+ const CodeScanningLanguageSchema = Schema.Literals([
348
+ "actions",
349
+ "c-cpp",
350
+ "csharp",
351
+ "go",
352
+ "java-kotlin",
353
+ "javascript-typescript",
354
+ "python",
355
+ "ruby",
356
+ "swift"
357
+ ]).annotate({
326
358
  identifier: "CodeScanningLanguage",
327
359
  title: "CodeQL default-setup language",
328
360
  description: "Languages supported by GitHub code scanning default setup. Note: this is narrower than the CodeQL analyzer (Rust is supported by CodeQL but not by default setup)."
329
361
  });
330
- const CodeScanningStateSchema = Schema.Literal("configured", "not-configured").annotations({
362
+ const CodeScanningStateSchema = Schema.Literals(["configured", "not-configured"]).annotate({
331
363
  identifier: "CodeScanningState",
332
364
  title: "Default setup state",
333
365
  description: "\"configured\" enables CodeQL default setup; \"not-configured\" disables it."
334
366
  });
335
- const CodeScanningQuerySuiteSchema = Schema.Literal("default", "extended").annotations({
367
+ const CodeScanningQuerySuiteSchema = Schema.Literals(["default", "extended"]).annotate({
336
368
  identifier: "CodeScanningQuerySuite",
337
369
  title: "Query suite",
338
370
  description: "\"default\" runs the standard query set; \"extended\" includes additional security queries."
339
371
  });
340
- const CodeScanningThreatModelSchema = Schema.Literal("remote", "remote_and_local").annotations({
372
+ const CodeScanningThreatModelSchema = Schema.Literals(["remote", "remote_and_local"]).annotate({
341
373
  identifier: "CodeScanningThreatModel",
342
374
  title: "Threat model",
343
375
  description: "\"remote\" analyzes network sources only; \"remote_and_local\" also includes filesystem and environment access."
344
376
  });
345
- const CodeScanningRunnerTypeSchema = Schema.Literal("standard", "labeled").annotations({
377
+ const CodeScanningRunnerTypeSchema = Schema.Literals(["standard", "labeled"]).annotate({
346
378
  identifier: "CodeScanningRunnerType",
347
379
  title: "Runner type",
348
380
  description: "\"standard\" uses GitHub-hosted runners; \"labeled\" uses runners matching runner_label."
349
381
  });
382
+ /**
383
+ * CodeQL default setup configuration.
384
+ *
385
+ * @public
386
+ */
350
387
  const CodeScanningGroupSchema = Schema.Struct({
351
388
  state: Schema.optional(CodeScanningStateSchema),
352
- languages: Schema.optional(Schema.Array(CodeScanningLanguageSchema).annotations({
389
+ languages: Schema.optional(Schema.Array(CodeScanningLanguageSchema).annotate({
353
390
  title: "Languages",
354
391
  description: "CodeQL languages to analyze. Languages not detected in the repository are skipped with a warning at sync time.",
355
392
  examples: [["javascript-typescript", "python"]]
@@ -357,113 +394,78 @@ const CodeScanningGroupSchema = Schema.Struct({
357
394
  query_suite: Schema.optional(CodeScanningQuerySuiteSchema),
358
395
  threat_model: Schema.optional(CodeScanningThreatModelSchema),
359
396
  runner_type: Schema.optional(CodeScanningRunnerTypeSchema),
360
- runner_label: Schema.optional(Schema.String.annotations({
397
+ runner_label: Schema.optional(Schema.String.annotate({
361
398
  title: "Runner label",
362
399
  description: "Self-hosted runner label. Required when runner_type = \"labeled\"."
363
400
  }))
364
- }).pipe(Schema.filter((group) => group.runner_type !== "labeled" || group.runner_label !== void 0, {
365
- identifier: "CodeScanningGroup",
366
- message: () => "runner_label is required when runner_type = \"labeled\""
367
- })).annotations({
401
+ }).annotate({
402
+ ...tombi({ tableKeysOrder: "schema" }),
403
+ ...taplo({ links: { key: docs("03-configuration.md") } }),
368
404
  identifier: "CodeScanningGroup",
369
405
  title: "Code scanning group",
370
- description: "CodeQL default setup configuration applied via PATCH /repos/{o}/{r}/code-scanning/default-setup. The endpoint returns 202 Accepted and configures asynchronously; reposets sends the request and does not poll for completion.",
371
- jsonSchema: {
372
- ...tombi({ tableKeysOrder: "schema" }),
373
- ...taplo({ links: { key: "https://github.com/spencerbeggs/reposets/blob/main/docs/configuration.md" } })
374
- }
375
- });
376
- const LogLevelSchema = Schema.Literal("silent", "info", "verbose", "debug").annotations({
377
- identifier: "LogLevel",
378
- title: "Log level",
379
- description: "Controls output verbosity: silent (none), info (summaries), verbose (per-operation), debug (with sources)"
380
- });
406
+ description: "CodeQL default setup configuration applied via PATCH /repos/{o}/{r}/code-scanning/default-setup. The endpoint returns 202 Accepted and configures asynchronously; reposets sends the request and does not poll for completion."
407
+ }).check(Schema.makeFilter((group) => group.runner_type === "labeled" && group.runner_label === void 0 ? "runner_label is required when runner_type = \"labeled\"" : void 0));
408
+ /**
409
+ * The whole `reposets.config.toml` document.
410
+ *
411
+ * @remarks
412
+ * `groups` is the only required key. Every resource map defaults to empty, so a
413
+ * config that declares repositories and nothing else is valid and does nothing —
414
+ * which is the right behaviour for a tool that mutates GitHub.
415
+ *
416
+ * @public
417
+ */
381
418
  const ConfigSchema = Schema.Struct({
382
- owner: Schema.optional(Schema.String.annotations({
383
- title: "Default owner",
384
- description: "Default GitHub user or organization for all groups. Can be overridden per group.",
385
- examples: ["spencerbeggs", "savvy-web"]
386
- })),
387
- log_level: Schema.optionalWith(LogLevelSchema, { default: () => "info" }).annotations({
388
- title: "Log level",
389
- description: "Default output verbosity. Can be overridden with --log-level CLI flag."
390
- }),
391
- settings: Schema.optionalWith(Schema.Record({
392
- key: Schema.String,
393
- value: SettingsGroupSchema
394
- }).annotations({
419
+ settings: Schema.Record(Schema.String, SettingsGroupSchema).annotate({
420
+ ...tombi({ additionalKeyLabel: "setting_group" }),
395
421
  title: "Settings groups",
396
- description: "Named groups of GitHub repository settings to apply",
397
- jsonSchema: tombi({ additionalKeyLabel: "setting_group" })
398
- }), { default: () => ({}) }),
399
- secrets: Schema.optionalWith(Schema.Record({
400
- key: Schema.String,
401
- value: SecretGroupSchema
402
- }).annotations({
422
+ description: "Named groups of GitHub repository settings to apply"
423
+ }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({}))),
424
+ secrets: Schema.Record(Schema.String, SecretGroupSchema).annotate({
425
+ ...tombi({ additionalKeyLabel: "secret_group" }),
403
426
  title: "Secret groups",
404
- description: "Named groups of secrets. Each group is one kind: file, value, or resolved.",
405
- jsonSchema: tombi({ additionalKeyLabel: "secret_group" })
406
- }), { default: () => ({}) }),
407
- variables: Schema.optionalWith(Schema.Record({
408
- key: Schema.String,
409
- value: VariableGroupSchema
410
- }).annotations({
427
+ description: "Named groups of secrets. Each group is one kind: file, value, or resolved."
428
+ }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({}))),
429
+ variables: Schema.Record(Schema.String, VariableGroupSchema).annotate({
430
+ ...tombi({ additionalKeyLabel: "variable_group" }),
411
431
  title: "Variable groups",
412
- description: "Named groups of variables. Each group is one kind: file, value, or resolved.",
413
- jsonSchema: tombi({ additionalKeyLabel: "variable_group" })
414
- }), { default: () => ({}) }),
415
- rulesets: Schema.optionalWith(Schema.Record({
416
- key: Schema.String,
417
- value: RulesetSchema
418
- }).annotations({
432
+ description: "Named groups of variables. Each group is one kind: file, value, or resolved."
433
+ }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({}))),
434
+ rulesets: Schema.Record(Schema.String, RulesetSchema).annotate({
435
+ ...tombi({ additionalKeyLabel: "ruleset_name" }),
419
436
  title: "Rulesets",
420
- description: "Named rulesets defining branch and tag protection rules",
421
- jsonSchema: tombi({ additionalKeyLabel: "ruleset_name" })
422
- }), { default: () => ({}) }),
423
- environments: Schema.optionalWith(Schema.Record({
424
- key: Schema.String,
425
- value: EnvironmentSchema
426
- }).annotations({
437
+ description: "Named rulesets defining branch and tag protection rules"
438
+ }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({}))),
439
+ environments: Schema.Record(Schema.String, EnvironmentSchema).annotate({
440
+ ...tombi({ additionalKeyLabel: "environment_name" }),
427
441
  title: "Environments",
428
- description: "Named deployment environment configurations",
429
- jsonSchema: tombi({ additionalKeyLabel: "environment_name" })
430
- }), { default: () => ({}) }),
431
- security: Schema.optionalWith(Schema.Record({
432
- key: Schema.String,
433
- value: SecurityGroupSchema
434
- }).annotations({
442
+ description: "Named deployment environment configurations"
443
+ }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({}))),
444
+ security: Schema.Record(Schema.String, SecurityGroupSchema).annotate({
445
+ ...tombi({ additionalKeyLabel: "security_group" }),
435
446
  title: "Security groups",
436
- description: "Named security groups for vulnerability alerts, automated security fixes, and private vulnerability reporting",
437
- jsonSchema: tombi({ additionalKeyLabel: "security_group" })
438
- }), { default: () => ({}) }),
439
- code_scanning: Schema.optionalWith(Schema.Record({
440
- key: Schema.String,
441
- value: CodeScanningGroupSchema
442
- }).annotations({
447
+ description: "Named security groups for vulnerability alerts, automated security fixes, and private vulnerability reporting"
448
+ }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({}))),
449
+ code_scanning: Schema.Record(Schema.String, CodeScanningGroupSchema).annotate({
450
+ ...tombi({ additionalKeyLabel: "code_scanning_group" }),
443
451
  title: "Code scanning groups",
444
- description: "Named code scanning groups for CodeQL default setup configuration",
445
- jsonSchema: tombi({ additionalKeyLabel: "code_scanning_group" })
446
- }), { default: () => ({}) }),
447
- groups: Schema.Record({
448
- key: Schema.String,
449
- value: GroupSchema
450
- }).annotations({
452
+ description: "Named code scanning groups for CodeQL default setup configuration"
453
+ }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({}))),
454
+ groups: Schema.Record(Schema.String, GroupSchema).annotate({
455
+ ...tombi({ additionalKeyLabel: "group_name" }),
451
456
  title: "Groups",
452
- description: "Named groups of repositories with their settings, secrets, variables, rulesets, environments, security, and code scanning assignments",
453
- jsonSchema: tombi({ additionalKeyLabel: "group_name" })
454
- })
455
- }).annotations({
457
+ description: "Named groups of repositories with their settings, secrets, variables, rulesets, environments, security, and code scanning assignments"
458
+ }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({})))
459
+ }).annotate({
460
+ ...tombi({ tableKeysOrder: "schema" }),
461
+ ...taplo({
462
+ initKeys: ["groups"],
463
+ links: { key: docs("03-configuration.md") }
464
+ }),
456
465
  identifier: "Config",
457
466
  title: "reposets Configuration",
458
- description: "Configuration for syncing GitHub repository settings, secrets, variables, rulesets, deployment environments, advanced security toggles, and CodeQL default setup",
459
- jsonSchema: {
460
- ...tombi({ tableKeysOrder: "schema" }),
461
- ...taplo({
462
- initKeys: ["owner", "groups"],
463
- links: { key: "https://github.com/spencerbeggs/reposets/blob/main/docs/configuration.md" }
464
- })
465
- }
467
+ description: "Configuration for syncing GitHub repository settings, secrets, variables, rulesets, deployment environments, advanced security toggles, and CodeQL default setup"
466
468
  });
467
469
 
468
470
  //#endregion
469
- export { ConfigSchema, GroupSchema, LogLevelSchema };
471
+ export { CodeScanningGroupSchema, CodeScanningLanguageSchema, ConfigSchema, GroupSchema, SecretScopesSchema, SecurityGroupSchema, SettingsGroupSchema, VariableScopesSchema };