@homeflare/alchemy 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/github/declare-repo-baseline.d.ts +11 -0
- package/dist/github/declare-repo-baseline.d.ts.map +1 -0
- package/dist/github/index.d.ts +20 -11
- package/dist/github/index.d.ts.map +1 -1
- package/dist/github/index.js +715 -4
- package/dist/github/index.js.map +14 -4
- package/dist/github/repo-baseline-data.d.ts +75 -0
- package/dist/github/repo-baseline-data.d.ts.map +1 -0
- package/dist/github/repo-policy.d.ts +4 -4
- package/dist/github/repo-policy.d.ts.map +1 -1
- package/dist/github/repository-ruleset-constraints.d.ts +105 -0
- package/dist/github/repository-ruleset-constraints.d.ts.map +1 -0
- package/dist/github/repository-ruleset-errors.d.ts +97 -0
- package/dist/github/repository-ruleset-errors.d.ts.map +1 -0
- package/dist/github/repository-ruleset-fake-octokit.d.ts +24 -0
- package/dist/github/repository-ruleset-fake-octokit.d.ts.map +1 -0
- package/dist/github/repository-ruleset-fixtures.d.ts +20 -0
- package/dist/github/repository-ruleset-fixtures.d.ts.map +1 -0
- package/dist/github/repository-ruleset-form.d.ts +30 -0
- package/dist/github/repository-ruleset-form.d.ts.map +1 -0
- package/dist/github/repository-ruleset-guards.d.ts +70 -0
- package/dist/github/repository-ruleset-guards.d.ts.map +1 -0
- package/dist/github/repository-ruleset-octokit.d.ts +5 -0
- package/dist/github/repository-ruleset-octokit.d.ts.map +1 -0
- package/dist/github/repository-ruleset-probe.d.ts +87 -0
- package/dist/github/repository-ruleset-probe.d.ts.map +1 -0
- package/dist/github/repository-ruleset-reconcile.d.ts +24 -0
- package/dist/github/repository-ruleset-reconcile.d.ts.map +1 -0
- package/dist/github/repository-ruleset-test-helpers.d.ts +32 -0
- package/dist/github/repository-ruleset-test-helpers.d.ts.map +1 -0
- package/dist/github/repository-ruleset.d.ts +89 -0
- package/dist/github/repository-ruleset.d.ts.map +1 -0
- package/docs/repo-policy-ruleset-hazards.md +8 -0
- package/docs/repo-policy.md +27 -12
- package/docs/repository-ruleset.md +114 -0
- package/docs/upstream-conformance.md +12 -1
- package/docs/upstream-pr-ruleset-fixes.md +77 -0
- package/package.json +1 -1
- package/src/github/declare-repo-baseline.ts +42 -0
- package/src/github/index.ts +43 -11
- package/src/github/repo-baseline-data.test.ts +76 -0
- package/src/github/repo-baseline-data.ts +86 -0
- package/src/github/repo-policy.ts +17 -17
- package/src/github/repository-ruleset-constraints.ts +136 -0
- package/src/github/repository-ruleset-coverage.test.ts +82 -0
- package/src/github/repository-ruleset-errors.ts +136 -0
- package/src/github/repository-ruleset-fake-octokit.ts +131 -0
- package/src/github/repository-ruleset-fixtures.ts +183 -0
- package/src/github/repository-ruleset-form.ts +212 -0
- package/src/github/repository-ruleset-guards.ts +194 -0
- package/src/github/repository-ruleset-lifecycle.test.ts +172 -0
- package/src/github/repository-ruleset-octokit.ts +140 -0
- package/src/github/repository-ruleset-probe.ts +116 -0
- package/src/github/repository-ruleset-reconcile.ts +176 -0
- package/src/github/repository-ruleset-test-helpers.ts +102 -0
- package/src/github/repository-ruleset-wire.test.ts +66 -0
- package/src/github/repository-ruleset-write-refusals.test.ts +199 -0
- package/src/github/repository-ruleset-write.test.ts +83 -0
- package/src/github/repository-ruleset.ts +219 -0
package/dist/github/index.js
CHANGED
|
@@ -138,9 +138,643 @@ function repoPolicy(options) {
|
|
|
138
138
|
import { adopt } from "alchemy/AdoptPolicy";
|
|
139
139
|
import * as GitHub from "alchemy/GitHub";
|
|
140
140
|
import * as RemovalPolicy from "alchemy/RemovalPolicy";
|
|
141
|
+
import * as Effect6 from "effect/Effect";
|
|
142
|
+
|
|
143
|
+
// src/github/repository-ruleset.ts
|
|
144
|
+
import { Resource } from "alchemy";
|
|
145
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
146
|
+
import { isResolved } from "alchemy/Diff";
|
|
147
|
+
import * as Provider from "alchemy/Provider";
|
|
148
|
+
import * as Effect5 from "effect/Effect";
|
|
149
|
+
|
|
150
|
+
// src/github/repository-ruleset-constraints.ts
|
|
151
|
+
var RULE_TYPE_COVERAGE = {
|
|
152
|
+
creation: "modeled",
|
|
153
|
+
update: "modeled",
|
|
154
|
+
deletion: "modeled",
|
|
155
|
+
required_linear_history: "modeled",
|
|
156
|
+
required_signatures: "modeled",
|
|
157
|
+
pull_request: "modeled",
|
|
158
|
+
required_status_checks: "modeled",
|
|
159
|
+
non_fast_forward: "modeled",
|
|
160
|
+
merge_queue: "refused",
|
|
161
|
+
required_deployments: "refused",
|
|
162
|
+
commit_message_pattern: "refused",
|
|
163
|
+
commit_author_email_pattern: "refused",
|
|
164
|
+
committer_email_pattern: "refused",
|
|
165
|
+
branch_name_pattern: "refused",
|
|
166
|
+
tag_name_pattern: "refused",
|
|
167
|
+
workflows: "refused",
|
|
168
|
+
code_scanning: "refused",
|
|
169
|
+
copilot_code_review: "refused",
|
|
170
|
+
file_path_restriction: "refused",
|
|
171
|
+
max_file_path_length: "refused",
|
|
172
|
+
file_extension_restriction: "refused",
|
|
173
|
+
max_file_size: "refused"
|
|
174
|
+
};
|
|
175
|
+
var MODELED_RULE_TYPES = Object.keys(RULE_TYPE_COVERAGE).filter((type) => RULE_TYPE_COVERAGE[type] === "modeled");
|
|
176
|
+
var ALLOWED_MERGE_METHODS = ["merge", "squash", "rebase"];
|
|
177
|
+
var MIN_REQUIRED_APPROVALS = 0;
|
|
178
|
+
var MAX_REQUIRED_APPROVALS = 10;
|
|
179
|
+
function approvalCountRefusal(count) {
|
|
180
|
+
if (!Number.isInteger(count) || count < MIN_REQUIRED_APPROVALS || count > MAX_REQUIRED_APPROVALS) {
|
|
181
|
+
return `requiredApprovingReviewCount must be a whole number from ${String(MIN_REQUIRED_APPROVALS)} ` + `to ${String(MAX_REQUIRED_APPROVALS)} (GitHub's own bound); got ${String(count)}.`;
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
function allowedMergeMethodsRefusal(methods) {
|
|
186
|
+
if (methods.length === 0) {
|
|
187
|
+
return "allowedMergeMethods must name at least one method — GitHub refuses an empty list.";
|
|
188
|
+
}
|
|
189
|
+
const bad = methods.filter((m) => !ALLOWED_MERGE_METHODS.includes(m));
|
|
190
|
+
if (bad.length > 0) {
|
|
191
|
+
return `allowedMergeMethods has values GitHub does not accept: ${bad.join(", ")}.`;
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/github/repository-ruleset-errors.ts
|
|
197
|
+
import * as Data from "effect/Data";
|
|
198
|
+
|
|
199
|
+
class DuplicateRuleset extends Data.TaggedError("DuplicateRuleset") {
|
|
200
|
+
get message() {
|
|
201
|
+
return `GitHub.RepositoryRuleset: ${this.owner}/${this.repository} has ${String(this.ids.length)} ` + `rulesets named "${this.name}" (ids ${this.ids.join(", ")}). Delete the extra one or rename ` + "it by hand before this resource can adopt or manage it — nothing was read or written.";
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
class BypassActorWidened extends Data.TaggedError("BypassActorWidened") {
|
|
206
|
+
get message() {
|
|
207
|
+
return `GitHub.RepositoryRuleset "${this.name}" on ${this.owner}/${this.repository}: the ` + `declaration adds a bypass actor the live ruleset does not have (${this.widened.join(", ")}). ` + "Widening bypass is refused here; grant it by hand and adopt the change, or use handoff.";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
class UndeclaredLiveRule extends Data.TaggedError("UndeclaredLiveRule") {
|
|
212
|
+
get message() {
|
|
213
|
+
return `GitHub.RepositoryRuleset "${this.name}" on ${this.owner}/${this.repository}: the live ` + `ruleset has a "${this.ruleType}" rule this resource does not model. Declare it (if it is ` + "now modeled) or an explicit `false` to remove it — the PUT replaces `rules` wholesale, so " + "nothing may be dropped silently.";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
class RequiredChecksOmitted extends Data.TaggedError("RequiredChecksOmitted") {
|
|
218
|
+
get message() {
|
|
219
|
+
return `GitHub.RepositoryRuleset "${this.name}" on ${this.owner}/${this.repository}: the live ` + `ruleset requires ${this.liveContexts.join(", ")}, and the declaration says nothing about ` + "requiredStatusChecks. Declare the same contexts, or `rules.requiredStatusChecks: false` to " + "remove them on purpose.";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
class NeverReportedContext extends Data.TaggedError("NeverReportedContext") {
|
|
224
|
+
get message() {
|
|
225
|
+
return `GitHub.RepositoryRuleset: "${this.context}" has never reported success on ` + `${this.owner}/${this.repository}'s default branch. Requiring it now would leave every ` + "pull request waiting on a check that has never once reported — refused before any write.";
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
class RulesetConstraintRefused extends Data.TaggedError("RulesetConstraintRefused") {
|
|
230
|
+
get message() {
|
|
231
|
+
return `GitHub.RepositoryRuleset: ${this.field} — ${this.reason}`;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
class ReadbackMismatch extends Data.TaggedError("ReadbackMismatch") {
|
|
236
|
+
get message() {
|
|
237
|
+
return `GitHub.RepositoryRuleset "${this.name}" on ${this.owner}/${this.repository}: after the ` + `write, an independent GET of ${this.field} read back ${JSON.stringify(this.read)}, not the ` + `${JSON.stringify(this.sent)} that was sent. The write did not persist as declared.`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/github/repository-ruleset-octokit.ts
|
|
242
|
+
import { GitHubCredentials } from "alchemy/GitHub";
|
|
141
243
|
import * as Effect from "effect/Effect";
|
|
142
|
-
var
|
|
143
|
-
|
|
244
|
+
var asError = (e) => e instanceof Error ? e : new Error(String(e));
|
|
245
|
+
var octokitFor = (baseUrl) => Effect.gen(function* () {
|
|
246
|
+
const creds = yield* yield* GitHubCredentials;
|
|
247
|
+
return baseUrl === undefined ? creds.octokit() : creds.octokit({ baseUrl });
|
|
248
|
+
}).pipe(Effect.mapError(asError));
|
|
249
|
+
var is404 = (e) => e?.status === 404;
|
|
250
|
+
var makeRulesetOctokit = (baseUrl) => ({
|
|
251
|
+
list: ({ owner, repo }) => Effect.gen(function* () {
|
|
252
|
+
const octokit = yield* octokitFor(baseUrl);
|
|
253
|
+
const rulesets = yield* Effect.tryPromise({
|
|
254
|
+
try: () => octokit.paginate(octokit.rest.repos.getRepoRulesets, {
|
|
255
|
+
owner,
|
|
256
|
+
repo,
|
|
257
|
+
includes_parents: false,
|
|
258
|
+
per_page: 100
|
|
259
|
+
}),
|
|
260
|
+
catch: asError
|
|
261
|
+
});
|
|
262
|
+
return rulesets;
|
|
263
|
+
}),
|
|
264
|
+
get: ({ owner, repo, rulesetId }) => Effect.gen(function* () {
|
|
265
|
+
const octokit = yield* octokitFor(baseUrl);
|
|
266
|
+
return yield* Effect.tryPromise({
|
|
267
|
+
try: () => octokit.rest.repos.getRepoRuleset({ owner, repo, ruleset_id: rulesetId }),
|
|
268
|
+
catch: (e) => e
|
|
269
|
+
}).pipe(Effect.map(({ data }) => data), Effect.catchIf(is404, () => Effect.succeed(undefined)), Effect.mapError(asError));
|
|
270
|
+
}),
|
|
271
|
+
create: (input) => Effect.gen(function* () {
|
|
272
|
+
const octokit = yield* octokitFor(baseUrl);
|
|
273
|
+
const { data } = yield* Effect.tryPromise({
|
|
274
|
+
try: () => octokit.rest.repos.createRepoRuleset({
|
|
275
|
+
owner: input.owner,
|
|
276
|
+
repo: input.repo,
|
|
277
|
+
...input.body
|
|
278
|
+
}),
|
|
279
|
+
catch: asError
|
|
280
|
+
});
|
|
281
|
+
return data;
|
|
282
|
+
}),
|
|
283
|
+
update: (input) => Effect.gen(function* () {
|
|
284
|
+
const octokit = yield* octokitFor(baseUrl);
|
|
285
|
+
const { data } = yield* Effect.tryPromise({
|
|
286
|
+
try: () => octokit.rest.repos.updateRepoRuleset({
|
|
287
|
+
owner: input.owner,
|
|
288
|
+
repo: input.repo,
|
|
289
|
+
ruleset_id: input.rulesetId,
|
|
290
|
+
...input.body
|
|
291
|
+
}),
|
|
292
|
+
catch: asError
|
|
293
|
+
});
|
|
294
|
+
return data;
|
|
295
|
+
}),
|
|
296
|
+
delete: ({ owner, repo, rulesetId }) => Effect.gen(function* () {
|
|
297
|
+
const octokit = yield* octokitFor(baseUrl);
|
|
298
|
+
yield* Effect.tryPromise({
|
|
299
|
+
try: () => octokit.rest.repos.deleteRepoRuleset({ owner, repo, ruleset_id: rulesetId }),
|
|
300
|
+
catch: (e) => e
|
|
301
|
+
}).pipe(Effect.asVoid, Effect.catchIf(is404, () => Effect.void), Effect.mapError(asError));
|
|
302
|
+
}),
|
|
303
|
+
hasContextReportedSuccess: ({ owner, repo, context }) => Effect.gen(function* () {
|
|
304
|
+
const octokit = yield* octokitFor(baseUrl);
|
|
305
|
+
const repository = yield* Effect.tryPromise({
|
|
306
|
+
try: () => octokit.rest.repos.get({ owner, repo }),
|
|
307
|
+
catch: asError
|
|
308
|
+
});
|
|
309
|
+
const ref = repository.data.default_branch;
|
|
310
|
+
const [checkRuns, statuses] = yield* Effect.all([
|
|
311
|
+
Effect.tryPromise({
|
|
312
|
+
try: () => octokit.rest.checks.listForRef({ owner, repo, ref, check_name: context }),
|
|
313
|
+
catch: asError
|
|
314
|
+
}),
|
|
315
|
+
Effect.tryPromise({
|
|
316
|
+
try: () => octokit.rest.repos.listCommitStatusesForRef({ owner, repo, ref }),
|
|
317
|
+
catch: asError
|
|
318
|
+
})
|
|
319
|
+
]);
|
|
320
|
+
const checkReported = checkRuns.data.check_runs.some((run) => run.name === context && run.conclusion === "success");
|
|
321
|
+
const statusReported = statuses.data.some((status) => status.context === context && status.state === "success");
|
|
322
|
+
return checkReported || statusReported;
|
|
323
|
+
})
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// src/github/repository-ruleset-probe.ts
|
|
327
|
+
import * as Effect2 from "effect/Effect";
|
|
328
|
+
var probeByName = (octokit, input) => Effect2.gen(function* () {
|
|
329
|
+
const all2 = yield* octokit.list({ owner: input.owner, repo: input.repo });
|
|
330
|
+
const matches = all2.filter((r) => r.name === input.name && (r.target ?? "branch") === input.target);
|
|
331
|
+
const [only, ...rest] = matches;
|
|
332
|
+
if (only === undefined)
|
|
333
|
+
return;
|
|
334
|
+
if (rest.length > 0) {
|
|
335
|
+
return yield* Effect2.fail(new DuplicateRuleset({
|
|
336
|
+
owner: input.owner,
|
|
337
|
+
repository: input.repo,
|
|
338
|
+
name: input.name,
|
|
339
|
+
ids: matches.map((m) => m.id)
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
return yield* octokit.get({ owner: input.owner, repo: input.repo, rulesetId: only.id });
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// src/github/repository-ruleset-reconcile.ts
|
|
346
|
+
import * as Effect4 from "effect/Effect";
|
|
347
|
+
|
|
348
|
+
// src/github/repository-ruleset-form.ts
|
|
349
|
+
var RULE_BUILD_ORDER = [
|
|
350
|
+
"creation",
|
|
351
|
+
"update",
|
|
352
|
+
"deletion",
|
|
353
|
+
"required_linear_history",
|
|
354
|
+
"required_signatures",
|
|
355
|
+
"non_fast_forward",
|
|
356
|
+
"pull_request",
|
|
357
|
+
"required_status_checks"
|
|
358
|
+
];
|
|
359
|
+
function buildPullRequestRule(rule) {
|
|
360
|
+
return {
|
|
361
|
+
type: "pull_request",
|
|
362
|
+
parameters: {
|
|
363
|
+
required_approving_review_count: rule.requiredApprovingReviewCount ?? 0,
|
|
364
|
+
dismiss_stale_reviews_on_push: rule.dismissStaleReviewsOnPush ?? false,
|
|
365
|
+
require_code_owner_review: rule.requireCodeOwnerReview ?? false,
|
|
366
|
+
require_last_push_approval: rule.requireLastPushApproval ?? false,
|
|
367
|
+
required_review_thread_resolution: rule.requiredReviewThreadResolution ?? false,
|
|
368
|
+
...rule.allowedMergeMethods === undefined ? {} : { allowed_merge_methods: [...rule.allowedMergeMethods] },
|
|
369
|
+
required_reviewers: (rule.requiredReviewers ?? []).map((r) => ({
|
|
370
|
+
file_patterns: [...r.filePatterns],
|
|
371
|
+
minimum_approvals: r.minimumApprovals,
|
|
372
|
+
reviewer: { id: r.reviewer.actorId, type: r.reviewer.actorType }
|
|
373
|
+
})),
|
|
374
|
+
...rule.extraApprovalForUnattributedChanges === undefined ? {} : { require_extra_approval_for_unattributed_changes: false }
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function buildStatusChecksRule(rule) {
|
|
379
|
+
return {
|
|
380
|
+
type: "required_status_checks",
|
|
381
|
+
parameters: {
|
|
382
|
+
required_status_checks: rule.checks.map((c) => ({
|
|
383
|
+
context: c.context,
|
|
384
|
+
...c.integrationId === undefined ? {} : { integration_id: c.integrationId }
|
|
385
|
+
})),
|
|
386
|
+
strict_required_status_checks_policy: rule.strictRequiredStatusChecksPolicy ?? false,
|
|
387
|
+
do_not_enforce_on_create: rule.doNotEnforceOnCreate ?? false
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function buildWireRules(rules) {
|
|
392
|
+
const wire = [];
|
|
393
|
+
for (const type of RULE_BUILD_ORDER) {
|
|
394
|
+
switch (type) {
|
|
395
|
+
case "creation":
|
|
396
|
+
if (rules?.creation)
|
|
397
|
+
wire.push({ type: "creation" });
|
|
398
|
+
break;
|
|
399
|
+
case "update":
|
|
400
|
+
if (rules?.update) {
|
|
401
|
+
wire.push({ type: "update", parameters: { update_allows_fetch_and_merge: false } });
|
|
402
|
+
}
|
|
403
|
+
break;
|
|
404
|
+
case "deletion":
|
|
405
|
+
if (rules?.deletion)
|
|
406
|
+
wire.push({ type: "deletion" });
|
|
407
|
+
break;
|
|
408
|
+
case "required_linear_history":
|
|
409
|
+
if (rules?.requiredLinearHistory)
|
|
410
|
+
wire.push({ type: "required_linear_history" });
|
|
411
|
+
break;
|
|
412
|
+
case "required_signatures":
|
|
413
|
+
if (rules?.requiredSignatures)
|
|
414
|
+
wire.push({ type: "required_signatures" });
|
|
415
|
+
break;
|
|
416
|
+
case "non_fast_forward":
|
|
417
|
+
if (rules?.nonFastForward)
|
|
418
|
+
wire.push({ type: "non_fast_forward" });
|
|
419
|
+
break;
|
|
420
|
+
case "pull_request":
|
|
421
|
+
if (rules?.pullRequest)
|
|
422
|
+
wire.push(buildPullRequestRule(rules.pullRequest));
|
|
423
|
+
break;
|
|
424
|
+
case "required_status_checks":
|
|
425
|
+
if (rules?.requiredStatusChecks) {
|
|
426
|
+
wire.push(buildStatusChecksRule(rules.requiredStatusChecks));
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return wire;
|
|
432
|
+
}
|
|
433
|
+
function desiredWireRuleset(props) {
|
|
434
|
+
return {
|
|
435
|
+
name: props.name,
|
|
436
|
+
target: props.target ?? "branch",
|
|
437
|
+
enforcement: props.enforcement ?? "active",
|
|
438
|
+
bypass_actors: (props.bypassActors ?? []).map((a) => ({
|
|
439
|
+
actor_type: a.actorType,
|
|
440
|
+
...a.actorId === undefined ? {} : { actor_id: a.actorId },
|
|
441
|
+
bypass_mode: a.bypassMode ?? "always"
|
|
442
|
+
})),
|
|
443
|
+
conditions: {
|
|
444
|
+
ref_name: {
|
|
445
|
+
include: [...props.conditions?.include ?? ["~ALL"]].sort(),
|
|
446
|
+
exclude: [...props.conditions?.exclude ?? []].sort()
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
rules: buildWireRules(props.rules)
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
var sortByJson = (items) => [...items].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
453
|
+
function stripNullish(value) {
|
|
454
|
+
if (Array.isArray(value))
|
|
455
|
+
return value.map(stripNullish);
|
|
456
|
+
if (value !== null && typeof value === "object") {
|
|
457
|
+
const out = {};
|
|
458
|
+
for (const k of Object.keys(value).sort()) {
|
|
459
|
+
const v = value[k];
|
|
460
|
+
if (v !== null && v !== undefined)
|
|
461
|
+
out[k] = stripNullish(v);
|
|
462
|
+
}
|
|
463
|
+
return out;
|
|
464
|
+
}
|
|
465
|
+
return value;
|
|
466
|
+
}
|
|
467
|
+
function canonicalizeRule(rule) {
|
|
468
|
+
const stripped = stripNullish(rule);
|
|
469
|
+
const params = stripped.parameters;
|
|
470
|
+
if (params === undefined)
|
|
471
|
+
return stripped;
|
|
472
|
+
const next = { ...params };
|
|
473
|
+
if (Array.isArray(next.allowed_merge_methods)) {
|
|
474
|
+
next.allowed_merge_methods = [...next.allowed_merge_methods].sort();
|
|
475
|
+
}
|
|
476
|
+
if (Array.isArray(next.required_status_checks)) {
|
|
477
|
+
next.required_status_checks = sortByJson(next.required_status_checks);
|
|
478
|
+
}
|
|
479
|
+
return { ...stripped, parameters: next };
|
|
480
|
+
}
|
|
481
|
+
function canonicalizeWireRuleset(input) {
|
|
482
|
+
const r = input ?? {};
|
|
483
|
+
return stripNullish({
|
|
484
|
+
name: r.name,
|
|
485
|
+
target: r.target ?? "branch",
|
|
486
|
+
enforcement: r.enforcement,
|
|
487
|
+
bypass_actors: sortByJson(r.bypass_actors ?? []),
|
|
488
|
+
conditions: r.conditions,
|
|
489
|
+
rules: sortByJson((r.rules ?? []).map(canonicalizeRule))
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/github/repository-ruleset-guards.ts
|
|
494
|
+
import * as Effect3 from "effect/Effect";
|
|
495
|
+
var bypassKey = (a) => `${String(a.actor_type)}:${String(a.actor_id ?? "")}:${String(a.bypass_mode ?? "always")}`;
|
|
496
|
+
function bypassWideningRefusal(input) {
|
|
497
|
+
if (input.live === undefined)
|
|
498
|
+
return;
|
|
499
|
+
const liveActors = input.live.bypass_actors ?? [];
|
|
500
|
+
const liveKeys = new Set(liveActors.map(bypassKey));
|
|
501
|
+
const widened = (input.declared ?? []).map((a) => bypassKey({
|
|
502
|
+
actor_type: a.actorType,
|
|
503
|
+
actor_id: a.actorId,
|
|
504
|
+
bypass_mode: a.bypassMode ?? "always"
|
|
505
|
+
})).filter((k) => !liveKeys.has(k));
|
|
506
|
+
if (widened.length === 0)
|
|
507
|
+
return;
|
|
508
|
+
return new BypassActorWidened({
|
|
509
|
+
owner: input.owner,
|
|
510
|
+
repository: input.repository,
|
|
511
|
+
name: input.name,
|
|
512
|
+
widened
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
var liveRuleTypes = (live) => new Set((live?.rules ?? []).map((r) => String(r.type)));
|
|
516
|
+
function undeclaredLiveRuleRefusal(input) {
|
|
517
|
+
for (const type of liveRuleTypes(input.live)) {
|
|
518
|
+
const coverage = RULE_TYPE_COVERAGE[type];
|
|
519
|
+
if (coverage === "refused") {
|
|
520
|
+
return new UndeclaredLiveRule({
|
|
521
|
+
owner: input.owner,
|
|
522
|
+
repository: input.repository,
|
|
523
|
+
name: input.name,
|
|
524
|
+
ruleType: type
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
if (coverage === "modeled" && type !== "required_status_checks" && !input.declaredTypes.has(type)) {
|
|
528
|
+
return new UndeclaredLiveRule({
|
|
529
|
+
owner: input.owner,
|
|
530
|
+
repository: input.repository,
|
|
531
|
+
name: input.name,
|
|
532
|
+
ruleType: type
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
function declaredRuleTypes(rules) {
|
|
539
|
+
if (rules === undefined)
|
|
540
|
+
return new Set;
|
|
541
|
+
const BOOLEAN_KEY_TO_TYPE = {
|
|
542
|
+
creation: "creation",
|
|
543
|
+
update: "update",
|
|
544
|
+
deletion: "deletion",
|
|
545
|
+
requiredLinearHistory: "required_linear_history",
|
|
546
|
+
requiredSignatures: "required_signatures",
|
|
547
|
+
nonFastForward: "non_fast_forward"
|
|
548
|
+
};
|
|
549
|
+
const types = new Set;
|
|
550
|
+
for (const [key, type] of Object.entries(BOOLEAN_KEY_TO_TYPE)) {
|
|
551
|
+
if (Object.hasOwn(rules, key))
|
|
552
|
+
types.add(type);
|
|
553
|
+
}
|
|
554
|
+
if (Object.hasOwn(rules, "pullRequest"))
|
|
555
|
+
types.add("pull_request");
|
|
556
|
+
return types;
|
|
557
|
+
}
|
|
558
|
+
function requiredChecksOmissionRefusal(input) {
|
|
559
|
+
if (input.declared?.requiredStatusChecks !== undefined)
|
|
560
|
+
return;
|
|
561
|
+
const liveRule = (input.live?.rules ?? []).find((r) => r.type === "required_status_checks");
|
|
562
|
+
if (liveRule === undefined)
|
|
563
|
+
return;
|
|
564
|
+
const params = liveRule.parameters;
|
|
565
|
+
const liveContexts = (params?.required_status_checks ?? []).map((c) => c.context);
|
|
566
|
+
return new RequiredChecksOmitted({
|
|
567
|
+
owner: input.owner,
|
|
568
|
+
repository: input.repository,
|
|
569
|
+
name: input.name,
|
|
570
|
+
liveContexts
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
var newlyRequiredContexts = (declared, live) => {
|
|
574
|
+
if (declared?.requiredStatusChecks === false || declared?.requiredStatusChecks === undefined)
|
|
575
|
+
return [];
|
|
576
|
+
const liveRule = (live?.rules ?? []).find((r) => r.type === "required_status_checks");
|
|
577
|
+
const liveParams = liveRule?.parameters;
|
|
578
|
+
const liveContexts = new Set((liveParams?.required_status_checks ?? []).map((c) => c.context));
|
|
579
|
+
return declared.requiredStatusChecks.checks.map((c) => c.context).filter((c) => !liveContexts.has(c));
|
|
580
|
+
};
|
|
581
|
+
var refuseUnreportedContexts = (octokit, owner, repo, contexts) => Effect3.gen(function* () {
|
|
582
|
+
for (const context of contexts) {
|
|
583
|
+
const reported = yield* octokit.hasContextReportedSuccess({ owner, repo, context });
|
|
584
|
+
if (!reported) {
|
|
585
|
+
return yield* Effect3.fail(new NeverReportedContext({ owner, repository: repo, context }));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// src/github/repository-ruleset-reconcile.ts
|
|
591
|
+
var guardedPullRequestFields = (rule) => {
|
|
592
|
+
const params = rule?.parameters ?? {};
|
|
593
|
+
return {
|
|
594
|
+
required_approving_review_count: params.required_approving_review_count,
|
|
595
|
+
allowed_merge_methods: Array.isArray(params.allowed_merge_methods) ? [...params.allowed_merge_methods].sort() : undefined,
|
|
596
|
+
require_extra_approval_for_unattributed_changes: params.require_extra_approval_for_unattributed_changes
|
|
597
|
+
};
|
|
598
|
+
};
|
|
599
|
+
function readbackRefusal(owner, repository, name, sentRules, independent) {
|
|
600
|
+
const sentPr = sentRules.find((r) => r.type === "pull_request");
|
|
601
|
+
if (sentPr === undefined)
|
|
602
|
+
return;
|
|
603
|
+
const readPr = (independent.rules ?? []).find((r) => r.type === "pull_request");
|
|
604
|
+
const sent = guardedPullRequestFields(sentPr);
|
|
605
|
+
const read = guardedPullRequestFields(readPr);
|
|
606
|
+
for (const field of Object.keys(sent)) {
|
|
607
|
+
if (sent[field] === undefined)
|
|
608
|
+
continue;
|
|
609
|
+
if (JSON.stringify(sent[field]) !== JSON.stringify(read[field])) {
|
|
610
|
+
return new ReadbackMismatch({
|
|
611
|
+
owner,
|
|
612
|
+
repository,
|
|
613
|
+
name,
|
|
614
|
+
field: `rules.pull_request.parameters.${field}`,
|
|
615
|
+
sent: sent[field],
|
|
616
|
+
read: read[field]
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
var reconcileRuleset = (input) => Effect4.gen(function* () {
|
|
623
|
+
const { news, output, constraintRefusal, attrsOf, octokit } = input;
|
|
624
|
+
const refusal = constraintRefusal(news);
|
|
625
|
+
if (refusal !== undefined)
|
|
626
|
+
return yield* Effect4.fail(refusal);
|
|
627
|
+
const owner = news.owner;
|
|
628
|
+
const repo = news.repository;
|
|
629
|
+
const target = news.target ?? "branch";
|
|
630
|
+
let observed = output === undefined ? undefined : yield* octokit.get({ owner, repo, rulesetId: output.rulesetId });
|
|
631
|
+
if (observed === undefined) {
|
|
632
|
+
observed = yield* probeByName(octokit, { owner, repo, name: news.name, target });
|
|
633
|
+
}
|
|
634
|
+
const bypassRefusal = news.bypassActors === undefined ? undefined : bypassWideningRefusal({
|
|
635
|
+
owner,
|
|
636
|
+
repository: repo,
|
|
637
|
+
name: news.name,
|
|
638
|
+
declared: news.bypassActors,
|
|
639
|
+
live: observed
|
|
640
|
+
});
|
|
641
|
+
if (bypassRefusal !== undefined)
|
|
642
|
+
return yield* Effect4.fail(bypassRefusal);
|
|
643
|
+
const ruleRefusal = undeclaredLiveRuleRefusal({
|
|
644
|
+
owner,
|
|
645
|
+
repository: repo,
|
|
646
|
+
name: news.name,
|
|
647
|
+
live: observed,
|
|
648
|
+
declaredTypes: declaredRuleTypes(news.rules)
|
|
649
|
+
});
|
|
650
|
+
if (ruleRefusal !== undefined)
|
|
651
|
+
return yield* Effect4.fail(ruleRefusal);
|
|
652
|
+
const checksRefusal = requiredChecksOmissionRefusal({
|
|
653
|
+
owner,
|
|
654
|
+
repository: repo,
|
|
655
|
+
name: news.name,
|
|
656
|
+
declared: news.rules,
|
|
657
|
+
live: observed
|
|
658
|
+
});
|
|
659
|
+
if (checksRefusal !== undefined)
|
|
660
|
+
return yield* Effect4.fail(checksRefusal);
|
|
661
|
+
const newContexts = newlyRequiredContexts(news.rules, observed);
|
|
662
|
+
yield* refuseUnreportedContexts(octokit, owner, repo, newContexts);
|
|
663
|
+
const desired = desiredWireRuleset(news);
|
|
664
|
+
const body = news.bypassActors === undefined && observed !== undefined ? { ...desired, bypass_actors: observed.bypass_actors ?? [] } : desired;
|
|
665
|
+
let record;
|
|
666
|
+
let wrote = false;
|
|
667
|
+
if (observed === undefined) {
|
|
668
|
+
record = yield* octokit.create({ owner, repo, body });
|
|
669
|
+
wrote = true;
|
|
670
|
+
} else {
|
|
671
|
+
const same = JSON.stringify(canonicalizeWireRuleset(body)) === JSON.stringify(canonicalizeWireRuleset(observed));
|
|
672
|
+
if (same) {
|
|
673
|
+
record = observed;
|
|
674
|
+
} else {
|
|
675
|
+
record = yield* octokit.update({ owner, repo, rulesetId: observed.id, body });
|
|
676
|
+
wrote = true;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (wrote) {
|
|
680
|
+
const independent = yield* octokit.get({ owner, repo, rulesetId: record.id });
|
|
681
|
+
if (independent !== undefined) {
|
|
682
|
+
const sentRules = body.rules ?? [];
|
|
683
|
+
const mismatch = readbackRefusal(owner, repo, news.name, sentRules, independent);
|
|
684
|
+
if (mismatch !== undefined)
|
|
685
|
+
return yield* Effect4.fail(mismatch);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return attrsOf(record);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// src/github/repository-ruleset.ts
|
|
692
|
+
var RepositoryRuleset = Resource("GitHub.RepositoryRuleset", {
|
|
693
|
+
defaultRemovalPolicy: "retain"
|
|
694
|
+
});
|
|
695
|
+
var attrsOf = (r) => ({
|
|
696
|
+
rulesetId: r.id,
|
|
697
|
+
nodeId: r.node_id,
|
|
698
|
+
name: r.name,
|
|
699
|
+
createdAt: r.created_at,
|
|
700
|
+
updatedAt: r.updated_at
|
|
701
|
+
});
|
|
702
|
+
function constraintRefusal(props) {
|
|
703
|
+
const approvals = props.rules?.pullRequest?.requiredApprovingReviewCount;
|
|
704
|
+
if (approvals !== undefined) {
|
|
705
|
+
const reason = approvalCountRefusal(approvals);
|
|
706
|
+
if (reason !== undefined) {
|
|
707
|
+
return new RulesetConstraintRefused({
|
|
708
|
+
field: "rules.pullRequest.requiredApprovingReviewCount",
|
|
709
|
+
reason
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const methods = props.rules?.pullRequest?.allowedMergeMethods;
|
|
714
|
+
if (methods !== undefined) {
|
|
715
|
+
const reason = allowedMergeMethodsRefusal(methods);
|
|
716
|
+
if (reason !== undefined) {
|
|
717
|
+
return new RulesetConstraintRefused({
|
|
718
|
+
field: "rules.pullRequest.allowedMergeMethods",
|
|
719
|
+
reason
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
var repositoryRulesetHandlers = RepositoryRuleset.Provider.of({
|
|
726
|
+
list: () => Effect5.succeed([]),
|
|
727
|
+
nuke: { skip: true },
|
|
728
|
+
read: ({ output, olds }) => Effect5.gen(function* () {
|
|
729
|
+
const octokit = makeRulesetOctokit(olds.baseUrl);
|
|
730
|
+
if (output === undefined) {
|
|
731
|
+
const match = yield* probeByName(octokit, {
|
|
732
|
+
owner: olds.owner,
|
|
733
|
+
repo: olds.repository,
|
|
734
|
+
name: olds.name,
|
|
735
|
+
target: olds.target ?? "branch"
|
|
736
|
+
});
|
|
737
|
+
return match === undefined ? undefined : Unowned(attrsOf(match));
|
|
738
|
+
}
|
|
739
|
+
const byId = yield* octokit.get({
|
|
740
|
+
owner: olds.owner,
|
|
741
|
+
repo: olds.repository,
|
|
742
|
+
rulesetId: output.rulesetId
|
|
743
|
+
});
|
|
744
|
+
if (byId !== undefined)
|
|
745
|
+
return attrsOf(byId);
|
|
746
|
+
const byName = yield* probeByName(octokit, {
|
|
747
|
+
owner: olds.owner,
|
|
748
|
+
repo: olds.repository,
|
|
749
|
+
name: olds.name,
|
|
750
|
+
target: olds.target ?? "branch"
|
|
751
|
+
});
|
|
752
|
+
return byName === undefined ? undefined : attrsOf(byName);
|
|
753
|
+
}),
|
|
754
|
+
diff: ({ news, olds }) => Effect5.succeed(isResolved(news) && olds !== undefined && (news.owner !== olds.owner || news.repository !== olds.repository) ? { action: "replace" } : undefined),
|
|
755
|
+
reconcile: ({ news, output }) => reconcileRuleset({
|
|
756
|
+
octokit: makeRulesetOctokit(news.baseUrl),
|
|
757
|
+
news,
|
|
758
|
+
output,
|
|
759
|
+
constraintRefusal,
|
|
760
|
+
attrsOf
|
|
761
|
+
}),
|
|
762
|
+
delete: ({ olds, output }) => Effect5.gen(function* () {
|
|
763
|
+
if (output?.rulesetId === undefined)
|
|
764
|
+
return;
|
|
765
|
+
const octokit = makeRulesetOctokit(olds.baseUrl);
|
|
766
|
+
yield* octokit.delete({
|
|
767
|
+
owner: olds.owner,
|
|
768
|
+
repo: olds.repository,
|
|
769
|
+
rulesetId: output.rulesetId
|
|
770
|
+
});
|
|
771
|
+
})
|
|
772
|
+
});
|
|
773
|
+
var RepositoryRulesetProvider = () => Provider.succeed(RepositoryRuleset, repositoryRulesetHandlers);
|
|
774
|
+
|
|
775
|
+
// src/github/repo-policy.ts
|
|
776
|
+
var declareRepoPolicy = (id, options) => Effect6.gen(function* () {
|
|
777
|
+
const policy = yield* Effect6.try({
|
|
144
778
|
catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
|
|
145
779
|
try: () => repoPolicy(options)
|
|
146
780
|
});
|
|
@@ -149,13 +783,90 @@ var declareRepoPolicy = (id, options) => Effect.gen(function* () {
|
|
|
149
783
|
return options.adopt === undefined ? kept : kept.pipe(adopt(options.adopt));
|
|
150
784
|
};
|
|
151
785
|
const repository = yield* owned(GitHub.Repository(id, policy.repository));
|
|
152
|
-
const ruleset = yield* owned(
|
|
786
|
+
const ruleset = yield* owned(RepositoryRuleset(`${id}-ruleset`, policy.ruleset));
|
|
153
787
|
return { policy, repository, ruleset };
|
|
154
788
|
});
|
|
789
|
+
// src/github/repo-baseline-data.ts
|
|
790
|
+
function repoBaselineSettings(input) {
|
|
791
|
+
return {
|
|
792
|
+
owner: input.owner,
|
|
793
|
+
name: input.repository,
|
|
794
|
+
...input.description === undefined ? {} : { description: input.description },
|
|
795
|
+
visibility: input.visibility,
|
|
796
|
+
allowSquashMerge: true,
|
|
797
|
+
allowMergeCommit: false,
|
|
798
|
+
allowRebaseMerge: false,
|
|
799
|
+
allowAutoMerge: true,
|
|
800
|
+
deleteBranchOnMerge: true,
|
|
801
|
+
hasWiki: false
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function repoBaselineRuleset(input) {
|
|
805
|
+
return {
|
|
806
|
+
owner: input.owner,
|
|
807
|
+
repository: input.repository,
|
|
808
|
+
name: "main",
|
|
809
|
+
target: "branch",
|
|
810
|
+
enforcement: "active",
|
|
811
|
+
bypassActors: [],
|
|
812
|
+
conditions: { include: ["~DEFAULT_BRANCH"], exclude: [] },
|
|
813
|
+
rules: {
|
|
814
|
+
deletion: true,
|
|
815
|
+
nonFastForward: true,
|
|
816
|
+
pullRequest: {
|
|
817
|
+
requiredApprovingReviewCount: 0,
|
|
818
|
+
dismissStaleReviewsOnPush: true,
|
|
819
|
+
requireCodeOwnerReview: false,
|
|
820
|
+
requireLastPushApproval: false,
|
|
821
|
+
requiredReviewThreadResolution: false,
|
|
822
|
+
allowedMergeMethods: ["squash"],
|
|
823
|
+
extraApprovalForUnattributedChanges: false
|
|
824
|
+
},
|
|
825
|
+
...input.checks.length === 0 ? {} : {
|
|
826
|
+
requiredStatusChecks: {
|
|
827
|
+
checks: input.checks.map((context) => ({ context })),
|
|
828
|
+
strictRequiredStatusChecksPolicy: false,
|
|
829
|
+
doNotEnforceOnCreate: false
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
var PUBLIC_ONLY_UNDECLARABLE = [
|
|
836
|
+
"secret_scanning: enabled",
|
|
837
|
+
"secret_scanning_push_protection: enabled",
|
|
838
|
+
"private_vulnerability_reporting: enabled",
|
|
839
|
+
"fork-PR approval: 'first_time_contributors'"
|
|
840
|
+
];
|
|
841
|
+
// src/github/declare-repo-baseline.ts
|
|
842
|
+
import { adopt as adopt2 } from "alchemy/AdoptPolicy";
|
|
843
|
+
import * as GitHub2 from "alchemy/GitHub";
|
|
844
|
+
import * as RemovalPolicy2 from "alchemy/RemovalPolicy";
|
|
845
|
+
import * as Effect7 from "effect/Effect";
|
|
846
|
+
var declareRepoBaseline = (id, options) => Effect7.gen(function* () {
|
|
847
|
+
const owned = (resource) => resource.pipe(RemovalPolicy2.retain(), adopt2(true));
|
|
848
|
+
const repository = yield* owned(GitHub2.Repository(id, repoBaselineSettings(options)));
|
|
849
|
+
const ruleset = yield* owned(RepositoryRuleset(`${id}-ruleset`, repoBaselineRuleset(options)));
|
|
850
|
+
return { repository, ruleset };
|
|
851
|
+
});
|
|
155
852
|
export {
|
|
853
|
+
BypassActorWidened,
|
|
854
|
+
DuplicateRuleset,
|
|
855
|
+
NeverReportedContext,
|
|
856
|
+
PUBLIC_ONLY_UNDECLARABLE,
|
|
857
|
+
RULE_TYPE_COVERAGE,
|
|
858
|
+
ReadbackMismatch,
|
|
859
|
+
RepositoryRuleset,
|
|
860
|
+
RepositoryRulesetProvider,
|
|
861
|
+
RequiredChecksOmitted,
|
|
862
|
+
RulesetConstraintRefused,
|
|
863
|
+
UndeclaredLiveRule,
|
|
864
|
+
declareRepoBaseline,
|
|
156
865
|
declareRepoPolicy,
|
|
866
|
+
repoBaselineRuleset,
|
|
867
|
+
repoBaselineSettings,
|
|
157
868
|
repoPolicy
|
|
158
869
|
};
|
|
159
870
|
|
|
160
|
-
//# debugId=
|
|
871
|
+
//# debugId=45989F9C0ABF077664756E2164756E21
|
|
161
872
|
//# sourceMappingURL=index.js.map
|