@homeflare/alchemy 0.25.1 → 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/dist/launchd/index.js +51 -47
- package/dist/launchd/index.js.map +5 -4
- package/dist/launchd/sudo-guard-mode.d.ts +62 -0
- package/dist/launchd/sudo-guard-mode.d.ts.map +1 -0
- package/dist/launchd/sudo-guard.d.ts +1 -38
- package/dist/launchd/sudo-guard.d.ts.map +1 -1
- package/dist/linux/fake-sudo-root.d.ts +26 -0
- package/dist/linux/fake-sudo-root.d.ts.map +1 -0
- package/dist/linux/fake-sudo.d.ts +56 -0
- package/dist/linux/fake-sudo.d.ts.map +1 -0
- package/dist/linux/index.d.ts +2 -0
- package/dist/linux/index.d.ts.map +1 -1
- package/dist/linux/index.js +1134 -2
- package/dist/linux/index.js.map +22 -5
- package/dist/linux/sudo-allowlist-chown.d.ts +12 -0
- package/dist/linux/sudo-allowlist-chown.d.ts.map +1 -0
- package/dist/linux/sudo-allowlist-dir.d.ts +6 -0
- package/dist/linux/sudo-allowlist-dir.d.ts.map +1 -0
- package/dist/linux/sudo-allowlist.d.ts +40 -0
- package/dist/linux/sudo-allowlist.d.ts.map +1 -0
- package/dist/linux/sudo-exec.d.ts +31 -0
- package/dist/linux/sudo-exec.d.ts.map +1 -0
- package/dist/linux/sudo-guard.d.ts +36 -0
- package/dist/linux/sudo-guard.d.ts.map +1 -0
- package/dist/linux/sudo-runner.d.ts +37 -0
- package/dist/linux/sudo-runner.d.ts.map +1 -0
- package/dist/linux/sudo-stage.d.ts +22 -0
- package/dist/linux/sudo-stage.d.ts.map +1 -0
- package/dist/linux/sudo-write.d.ts +25 -0
- package/dist/linux/sudo-write.d.ts.map +1 -0
- package/dist/linux/unit-form.d.ts +2 -0
- package/dist/linux/unit-form.d.ts.map +1 -1
- package/dist/litellm/fake-litellm.d.ts.map +1 -1
- package/dist/proxmox/fake-pve-lxc.d.ts.map +1 -1
- package/docs/launchd-sudo.md +12 -3
- package/docs/linux-host.md +3 -3
- package/docs/linux-sudo.md +187 -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/src/launchd/sudo-guard-mode.ts +111 -0
- package/src/launchd/sudo-guard.ts +3 -79
- package/src/launchd/sudo-modes.test.ts +38 -1
- package/src/linux/fake-sudo-root.ts +157 -0
- package/src/linux/fake-sudo.ts +142 -0
- package/src/linux/index.ts +2 -0
- package/src/linux/sudo-allowlist-chown.ts +71 -0
- package/src/linux/sudo-allowlist-dir.ts +103 -0
- package/src/linux/sudo-allowlist.test.ts +252 -0
- package/src/linux/sudo-allowlist.ts +223 -0
- package/src/linux/sudo-exec.ts +118 -0
- package/src/linux/sudo-guard.test.ts +136 -0
- package/src/linux/sudo-guard.ts +142 -0
- package/src/linux/sudo-lifecycle.test.ts +151 -0
- package/src/linux/sudo-runner.test.ts +206 -0
- package/src/linux/sudo-runner.ts +164 -0
- package/src/linux/sudo-stage.ts +50 -0
- package/src/linux/sudo-write.ts +167 -0
- package/src/linux/unit-form.ts +2 -1
- package/src/litellm/fake-litellm.ts +2 -0
- package/src/proxmox/client.test.ts +17 -3
- package/src/proxmox/fake-pve-lxc.ts +10 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-form constraints for `GitHub.RepositoryRuleset`, read off GitHub's own schema rather
|
|
3
|
+
* than invented (vendor-schema's rule: never infer a bound the schema does not state).
|
|
4
|
+
*
|
|
5
|
+
* Provenance: `github/rest-api-description` commit `4377b4f4845b` on `main`
|
|
6
|
+
* (measured 2026-09-23; 12,964,430 bytes; sha256 `b8ca0376…` (prefix as given upstream by the
|
|
7
|
+
* planner — this file does not extend it), `info.version` `1.1.4`).
|
|
8
|
+
* That commit's `pull_request` rule gives `allowed_merge_methods` as the enum
|
|
9
|
+
* `{merge, squash, rebase}` and `required_approving_review_count` as `0–10` — both match
|
|
10
|
+
* what Octokit's own installed types carry (below), so the two agree.
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ MEASURED 2026-09-23, CORRECTING THE PLAN'S OWN CITATION: `@octokit/rest@22.0.1` pulls
|
|
13
|
+
* `@octokit/openapi-types@29.0.1` in directly, but the REST method PARAMETER TYPES Octokit
|
|
14
|
+
* actually exposes — `Parameters<Octokit['rest']['repos']['createRepoRuleset']>[0]`, which
|
|
15
|
+
* `RepositoryRuleType` below is derived from — resolve through a DIFFERENT chain:
|
|
16
|
+
* `@octokit/plugin-rest-endpoint-methods@17.0.0` → `@octokit/types@16.0.0` →
|
|
17
|
+
* `@octokit/openapi-types@27.0.0` (`bun install`'s own resolution in this checkout,
|
|
18
|
+
* confirmed by reading each intermediate `package.json`). 27.0.0's `repository-rule` union
|
|
19
|
+
* has 21 members, one fewer than 29.0.1's 22 — it has no `license_compliance_scanning`.
|
|
20
|
+
* `RULE_TYPE_COVERAGE`'s `satisfies` below is written against the 27.0.0 union, because
|
|
21
|
+
* that is the one `tsc` (and so `bun run check`) actually enforces; a bump that moves
|
|
22
|
+
* `plugin-rest-endpoint-methods` onto a newer `@octokit/types` will fail this file to
|
|
23
|
+
* compile on the very day the union changes (S1 test iv), which is the point.
|
|
24
|
+
*
|
|
25
|
+
* ⛔ `require_extra_approval_for_unattributed_changes` appears in NEITHER schema (0 occurrences
|
|
26
|
+
* in the commit above, 0 in either installed `@octokit/openapi-types`) — see
|
|
27
|
+
* repository-ruleset.ts for why it is still modeled (H15) and repository-ruleset-wire.test.ts
|
|
28
|
+
* for whether it survives Octokit's request builder.
|
|
29
|
+
*/
|
|
30
|
+
import type { Octokit } from '@octokit/rest';
|
|
31
|
+
|
|
32
|
+
/** The exact vendor/toolchain versions this file's constraints were read against (S38). A
|
|
33
|
+
* re-walk that finds a different version updates this comment and the header above together. */
|
|
34
|
+
export const RULESET_SCHEMA_PROVENANCE = {
|
|
35
|
+
alchemy: '2.0.0-beta.79',
|
|
36
|
+
octokitRest: '22.0.1',
|
|
37
|
+
/** The version `Octokit['rest']['repos']['createRepoRuleset']`'s own PARAMETER TYPES
|
|
38
|
+
* actually resolve through (`plugin-rest-endpoint-methods@17.0.0` → `@octokit/types@16.0.0`
|
|
39
|
+
* → this) — not the 29.0.1 `@octokit/rest@22.0.1` pulls in directly. See the file header. */
|
|
40
|
+
octokitOpenapiTypesForRestMethods: '27.0.0',
|
|
41
|
+
restApiDescriptionCommit: '4377b4f4845b',
|
|
42
|
+
restApiDescriptionSha256Prefix: 'b8ca0376',
|
|
43
|
+
restApiDescriptionInfoVersion: '1.1.4',
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
type CreateRulesetParams = NonNullable<
|
|
47
|
+
Parameters<Octokit['rest']['repos']['createRepoRuleset']>[0]
|
|
48
|
+
>;
|
|
49
|
+
/** The request body Octokit's OWN types accept, minus the two URL-template params — the shape
|
|
50
|
+
* repository-ruleset-form.ts builds `rules`/`conditions`/`bypass_actors` against.
|
|
51
|
+
* ⚠️ `Pick` with explicit keys, NOT `Omit<CreateRulesetParams, 'owner' | 'repo'>` — measured:
|
|
52
|
+
* `CreateRulesetParams` is an intersection with Octokit's own `RequestParameters`, which
|
|
53
|
+
* carries a string index signature; `Omit`'s `keyof`-based exclusion collapses through that
|
|
54
|
+
* signature and produces a type whose named properties no longer survive an object spread
|
|
55
|
+
* (every spread of the `Omit`'d type type-checked as contributing only `{}`). `Pick` with
|
|
56
|
+
* literal keys never computes `keyof` over the intersection, so it does not collapse. */
|
|
57
|
+
export type CreateRulesetBody = Pick<
|
|
58
|
+
CreateRulesetParams,
|
|
59
|
+
'name' | 'target' | 'enforcement' | 'bypass_actors' | 'conditions' | 'rules'
|
|
60
|
+
>;
|
|
61
|
+
export type WireRule = NonNullable<CreateRulesetParams['rules']>[number];
|
|
62
|
+
export type WirePullRequestRule = Extract<WireRule, { type: 'pull_request' }>;
|
|
63
|
+
export type WireStatusChecksRule = Extract<WireRule, { type: 'required_status_checks' }>;
|
|
64
|
+
|
|
65
|
+
/** Every rule `type` Octokit's OWN installed types know about — derived, not hand-listed, so a
|
|
66
|
+
* vendor upgrade that adds a rule type fails `RULE_TYPE_COVERAGE`'s `satisfies` below at
|
|
67
|
+
* compile time (S1 test iv) rather than silently reaching `reconcile` unrecognized. */
|
|
68
|
+
export type RepositoryRuleType = WireRule['type'];
|
|
69
|
+
|
|
70
|
+
/** `'modeled'` rules this resource can declare and diff; `'refused'` rules it recognizes on a
|
|
71
|
+
* live ruleset but will never send — a live rule of a refused type fails the plan by name
|
|
72
|
+
* (`UndeclaredLiveRule`) instead of being silently dropped by the wholesale `rules` PUT. */
|
|
73
|
+
export const RULE_TYPE_COVERAGE = {
|
|
74
|
+
creation: 'modeled',
|
|
75
|
+
update: 'modeled',
|
|
76
|
+
deletion: 'modeled',
|
|
77
|
+
required_linear_history: 'modeled',
|
|
78
|
+
required_signatures: 'modeled',
|
|
79
|
+
pull_request: 'modeled',
|
|
80
|
+
required_status_checks: 'modeled',
|
|
81
|
+
non_fast_forward: 'modeled',
|
|
82
|
+
merge_queue: 'refused',
|
|
83
|
+
required_deployments: 'refused',
|
|
84
|
+
commit_message_pattern: 'refused',
|
|
85
|
+
commit_author_email_pattern: 'refused',
|
|
86
|
+
committer_email_pattern: 'refused',
|
|
87
|
+
branch_name_pattern: 'refused',
|
|
88
|
+
tag_name_pattern: 'refused',
|
|
89
|
+
workflows: 'refused',
|
|
90
|
+
code_scanning: 'refused',
|
|
91
|
+
copilot_code_review: 'refused',
|
|
92
|
+
file_path_restriction: 'refused',
|
|
93
|
+
max_file_path_length: 'refused',
|
|
94
|
+
file_extension_restriction: 'refused',
|
|
95
|
+
max_file_size: 'refused',
|
|
96
|
+
} as const satisfies Record<RepositoryRuleType, 'modeled' | 'refused'>;
|
|
97
|
+
|
|
98
|
+
export const MODELED_RULE_TYPES: readonly RepositoryRuleType[] = (
|
|
99
|
+
Object.keys(RULE_TYPE_COVERAGE) as RepositoryRuleType[]
|
|
100
|
+
).filter((type) => RULE_TYPE_COVERAGE[type] === 'modeled');
|
|
101
|
+
|
|
102
|
+
export const ALLOWED_MERGE_METHODS = ['merge', 'squash', 'rebase'] as const;
|
|
103
|
+
export type AllowedMergeMethod = (typeof ALLOWED_MERGE_METHODS)[number];
|
|
104
|
+
|
|
105
|
+
export const MIN_REQUIRED_APPROVALS = 0;
|
|
106
|
+
export const MAX_REQUIRED_APPROVALS = 10;
|
|
107
|
+
|
|
108
|
+
/** `undefined` when the value is within the schema's `0–10` bound for
|
|
109
|
+
* `required_approving_review_count`; otherwise the refusal reason. Plan-time, not wire-time —
|
|
110
|
+
* schema-codegen's rule: check the form, before the "no previous state" early return. */
|
|
111
|
+
export function approvalCountRefusal(count: number): string | undefined {
|
|
112
|
+
if (
|
|
113
|
+
!Number.isInteger(count) ||
|
|
114
|
+
count < MIN_REQUIRED_APPROVALS ||
|
|
115
|
+
count > MAX_REQUIRED_APPROVALS
|
|
116
|
+
) {
|
|
117
|
+
return (
|
|
118
|
+
`requiredApprovingReviewCount must be a whole number from ${String(MIN_REQUIRED_APPROVALS)} ` +
|
|
119
|
+
`to ${String(MAX_REQUIRED_APPROVALS)} (GitHub's own bound); got ${String(count)}.`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** `undefined` when every entry is one of the schema's enum and the list is non-empty ("at
|
|
126
|
+
* least one option must be enabled" — the schema's own words); otherwise the refusal reason. */
|
|
127
|
+
export function allowedMergeMethodsRefusal(methods: readonly string[]): string | undefined {
|
|
128
|
+
if (methods.length === 0) {
|
|
129
|
+
return 'allowedMergeMethods must name at least one method — GitHub refuses an empty list.';
|
|
130
|
+
}
|
|
131
|
+
const bad = methods.filter((m) => !(ALLOWED_MERGE_METHODS as readonly string[]).includes(m));
|
|
132
|
+
if (bad.length > 0) {
|
|
133
|
+
return `allowedMergeMethods has values GitHub does not accept: ${bad.join(', ')}.`;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (iv) `RULE_TYPE_COVERAGE` is a compile-time exhaustiveness check by construction: it is typed
|
|
3
|
+
* `as const satisfies Record<RepositoryRuleType, 'modeled' | 'refused'>`, and `RepositoryRuleType`
|
|
4
|
+
* is derived from Octokit's OWN installed rule-type union (repository-ruleset-constraints.ts),
|
|
5
|
+
* not hand-listed. When a future `@octokit/rest`/`@octokit/types` upgrade adds a rule type,
|
|
6
|
+
* `bun run check`'s `tsc` step fails on repository-ruleset-constraints.ts — the object literal
|
|
7
|
+
* is missing a required key — before `reconcile` can ever see a rule it does not recognize.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ THIS FILE CANNOT DEMONSTRATE THAT FAILURE AT RUNTIME. `bun:test` does not type-check; a
|
|
10
|
+
* `// @ts-expect-error` fixture proving the object literal rejects an incomplete map would
|
|
11
|
+
* need its own file kept permanently out of `tsc`'s project (or it would break the build it
|
|
12
|
+
* is trying to test), which is more machinery than the guarantee is worth. What this file
|
|
13
|
+
* DOES check at runtime: the count and shape of the union `tsc` is enforcing right now, as a
|
|
14
|
+
* change detector — a value this test did not expect means the installed Octokit version
|
|
15
|
+
* moved and `RULE_TYPE_COVERAGE`'s completeness (already enforced separately, by `tsc`) has a
|
|
16
|
+
* new member to classify as `'modeled'` or `'refused'` on purpose, not by accident.
|
|
17
|
+
*/
|
|
18
|
+
import { describe, expect, test } from 'bun:test';
|
|
19
|
+
import {
|
|
20
|
+
MODELED_RULE_TYPES,
|
|
21
|
+
RULESET_SCHEMA_PROVENANCE,
|
|
22
|
+
RULE_TYPE_COVERAGE,
|
|
23
|
+
} from './repository-ruleset-constraints.ts';
|
|
24
|
+
|
|
25
|
+
const EXPECTED_TYPES = [
|
|
26
|
+
'creation',
|
|
27
|
+
'update',
|
|
28
|
+
'deletion',
|
|
29
|
+
'required_linear_history',
|
|
30
|
+
'required_signatures',
|
|
31
|
+
'pull_request',
|
|
32
|
+
'required_status_checks',
|
|
33
|
+
'non_fast_forward',
|
|
34
|
+
'merge_queue',
|
|
35
|
+
'required_deployments',
|
|
36
|
+
'commit_message_pattern',
|
|
37
|
+
'commit_author_email_pattern',
|
|
38
|
+
'committer_email_pattern',
|
|
39
|
+
'branch_name_pattern',
|
|
40
|
+
'tag_name_pattern',
|
|
41
|
+
'workflows',
|
|
42
|
+
'code_scanning',
|
|
43
|
+
'copilot_code_review',
|
|
44
|
+
'file_path_restriction',
|
|
45
|
+
'max_file_path_length',
|
|
46
|
+
'file_extension_restriction',
|
|
47
|
+
'max_file_size',
|
|
48
|
+
] as const;
|
|
49
|
+
|
|
50
|
+
describe('RULE_TYPE_COVERAGE tracks Octokit-installed rule types exactly', () => {
|
|
51
|
+
test("classifies every one of the 21 rule types this checkout's Octokit knows", () => {
|
|
52
|
+
expect(Object.keys(RULE_TYPE_COVERAGE).sort()).toEqual([...EXPECTED_TYPES].sort());
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('exactly the 8 upstream `GitHub.Ruleset` models are "modeled", the rest "refused"', () => {
|
|
56
|
+
const modeled = new Set(MODELED_RULE_TYPES);
|
|
57
|
+
expect(modeled).toEqual(
|
|
58
|
+
new Set([
|
|
59
|
+
'creation',
|
|
60
|
+
'update',
|
|
61
|
+
'deletion',
|
|
62
|
+
'required_linear_history',
|
|
63
|
+
'required_signatures',
|
|
64
|
+
'pull_request',
|
|
65
|
+
'required_status_checks',
|
|
66
|
+
'non_fast_forward',
|
|
67
|
+
]),
|
|
68
|
+
);
|
|
69
|
+
for (const type of EXPECTED_TYPES) {
|
|
70
|
+
if (!modeled.has(type)) expect(RULE_TYPE_COVERAGE[type]).toBe('refused');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('the provenance record names the version tcs actually checked this file against', () => {
|
|
75
|
+
// The 27.0.0 vs 29.0.1 split is the point (see repository-ruleset-constraints.ts's header):
|
|
76
|
+
// `@octokit/rest` pulls 29.0.1 directly, but the REST method parameter types this family
|
|
77
|
+
// derives `RepositoryRuleType` from resolve through a different, older chain.
|
|
78
|
+
expect(RULESET_SCHEMA_PROVENANCE.octokitOpenapiTypesForRestMethods).toBe('27.0.0');
|
|
79
|
+
expect(RULESET_SCHEMA_PROVENANCE.octokitRest).toBe('22.0.1');
|
|
80
|
+
expect(RULESET_SCHEMA_PROVENANCE.alchemy).toBe('2.0.0-beta.79');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GitHub.RepositoryRuleset`'s failures as typed tags (S21). None of these sniffs a status
|
|
3
|
+
* code or matches a message from Octokit — every one is raised from a fact this family's own
|
|
4
|
+
* code already checked (a paginated list, a live GET, a declared prop), so `catchTag` covers
|
|
5
|
+
* every refusal without a driver-string coincidence hiding a different failure underneath it.
|
|
6
|
+
*/
|
|
7
|
+
import * as Data from 'effect/Data';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* More than one ruleset shares a name (and target) on the same repository. `probeByName`
|
|
11
|
+
* fails BEFORE reading either one — see repository-ruleset-probe.ts — because there is no
|
|
12
|
+
* way to know which one is "ours" and guessing risks converging the wrong ruleset.
|
|
13
|
+
*/
|
|
14
|
+
export class DuplicateRuleset extends Data.TaggedError('DuplicateRuleset')<{
|
|
15
|
+
readonly owner: string;
|
|
16
|
+
readonly repository: string;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly ids: readonly number[];
|
|
19
|
+
}> {
|
|
20
|
+
override get message(): string {
|
|
21
|
+
return (
|
|
22
|
+
`GitHub.RepositoryRuleset: ${this.owner}/${this.repository} has ${String(this.ids.length)} ` +
|
|
23
|
+
`rulesets named "${this.name}" (ids ${this.ids.join(', ')}). Delete the extra one or rename ` +
|
|
24
|
+
'it by hand before this resource can adopt or manage it — nothing was read or written.'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A declared `bypassActors` entry names an actor the live ruleset does not have. Widening
|
|
30
|
+
* bypass is a handoff, never a deploy — see repository-ruleset-guards.ts. */
|
|
31
|
+
export class BypassActorWidened extends Data.TaggedError('BypassActorWidened')<{
|
|
32
|
+
readonly owner: string;
|
|
33
|
+
readonly repository: string;
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly widened: readonly string[];
|
|
36
|
+
}> {
|
|
37
|
+
override get message(): string {
|
|
38
|
+
return (
|
|
39
|
+
`GitHub.RepositoryRuleset "${this.name}" on ${this.owner}/${this.repository}: the ` +
|
|
40
|
+
`declaration adds a bypass actor the live ruleset does not have (${this.widened.join(', ')}). ` +
|
|
41
|
+
'Widening bypass is refused here; grant it by hand and adopt the change, or use handoff.'
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A live rule's `type` is neither modeled nor explicitly refused by RULE_TYPE_COVERAGE, or is
|
|
47
|
+
* refused and present live anyway. The PUT replaces `rules` wholesale, so silently omitting a
|
|
48
|
+
* live rule type would drop it — this fails the plan by name instead. */
|
|
49
|
+
export class UndeclaredLiveRule extends Data.TaggedError('UndeclaredLiveRule')<{
|
|
50
|
+
readonly owner: string;
|
|
51
|
+
readonly repository: string;
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly ruleType: string;
|
|
54
|
+
}> {
|
|
55
|
+
override get message(): string {
|
|
56
|
+
return (
|
|
57
|
+
`GitHub.RepositoryRuleset "${this.name}" on ${this.owner}/${this.repository}: the live ` +
|
|
58
|
+
`ruleset has a "${this.ruleType}" rule this resource does not model. Declare it (if it is ` +
|
|
59
|
+
'now modeled) or an explicit `false` to remove it — the PUT replaces `rules` wholesale, so ' +
|
|
60
|
+
'nothing may be dropped silently.'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The declaration omits `requiredStatusChecks` while adopting or updating a live ruleset that
|
|
66
|
+
* HAS one. Declare the contexts, or declare `requiredStatusChecks: false` to remove them. */
|
|
67
|
+
export class RequiredChecksOmitted extends Data.TaggedError('RequiredChecksOmitted')<{
|
|
68
|
+
readonly owner: string;
|
|
69
|
+
readonly repository: string;
|
|
70
|
+
readonly name: string;
|
|
71
|
+
readonly liveContexts: readonly string[];
|
|
72
|
+
}> {
|
|
73
|
+
override get message(): string {
|
|
74
|
+
return (
|
|
75
|
+
`GitHub.RepositoryRuleset "${this.name}" on ${this.owner}/${this.repository}: the live ` +
|
|
76
|
+
`ruleset requires ${this.liveContexts.join(', ')}, and the declaration says nothing about ` +
|
|
77
|
+
'requiredStatusChecks. Declare the same contexts, or `rules.requiredStatusChecks: false` to ' +
|
|
78
|
+
'remove them on purpose.'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A context being ADDED to `requiredStatusChecks` has never reported success on the default
|
|
84
|
+
* branch. Requiring it would leave every future pull request pending forever. */
|
|
85
|
+
export class NeverReportedContext extends Data.TaggedError('NeverReportedContext')<{
|
|
86
|
+
readonly owner: string;
|
|
87
|
+
readonly repository: string;
|
|
88
|
+
readonly context: string;
|
|
89
|
+
}> {
|
|
90
|
+
override get message(): string {
|
|
91
|
+
return (
|
|
92
|
+
`GitHub.RepositoryRuleset: "${this.context}" has never reported success on ` +
|
|
93
|
+
`${this.owner}/${this.repository}'s default branch. Requiring it now would leave every ` +
|
|
94
|
+
'pull request waiting on a check that has never once reported — refused before any write.'
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A prop value the wire form or GitHub's own schema refuses — an approval count outside 0–10,
|
|
100
|
+
* an empty `allowedMergeMethods`, or an unmodeled rule `type` in the declaration itself. */
|
|
101
|
+
export class RulesetConstraintRefused extends Data.TaggedError('RulesetConstraintRefused')<{
|
|
102
|
+
readonly field: string;
|
|
103
|
+
readonly reason: string;
|
|
104
|
+
}> {
|
|
105
|
+
override get message(): string {
|
|
106
|
+
return `GitHub.RepositoryRuleset: ${this.field} — ${this.reason}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The independent readback GET after a write does not match what was sent, for one of the
|
|
111
|
+
* fields this family refuses to leave unverified (S10: the write's own report is never proof). */
|
|
112
|
+
export class ReadbackMismatch extends Data.TaggedError('ReadbackMismatch')<{
|
|
113
|
+
readonly owner: string;
|
|
114
|
+
readonly repository: string;
|
|
115
|
+
readonly name: string;
|
|
116
|
+
readonly field: string;
|
|
117
|
+
readonly sent: unknown;
|
|
118
|
+
readonly read: unknown;
|
|
119
|
+
}> {
|
|
120
|
+
override get message(): string {
|
|
121
|
+
return (
|
|
122
|
+
`GitHub.RepositoryRuleset "${this.name}" on ${this.owner}/${this.repository}: after the ` +
|
|
123
|
+
`write, an independent GET of ${this.field} read back ${JSON.stringify(this.read)}, not the ` +
|
|
124
|
+
`${JSON.stringify(this.sent)} that was sent. The write did not persist as declared.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type RepositoryRulesetError =
|
|
130
|
+
| DuplicateRuleset
|
|
131
|
+
| BypassActorWidened
|
|
132
|
+
| UndeclaredLiveRule
|
|
133
|
+
| RequiredChecksOmitted
|
|
134
|
+
| NeverReportedContext
|
|
135
|
+
| RulesetConstraintRefused
|
|
136
|
+
| ReadbackMismatch;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two `RulesetOctokit` fakes for the test files — no HTTP, no Octokit instance, exactly the
|
|
3
|
+
* seam repository-ruleset-probe.ts documents. Mirrors `postgres/fake-sql.ts`'s split: a
|
|
4
|
+
* GET-only fake that refuses to be written to (proves a plan touches nothing when it should
|
|
5
|
+
* not), and a write-recording fake that behaves like a tiny in-memory GitHub, so the SAME
|
|
6
|
+
* `reconcileRuleset` code under test runs against both.
|
|
7
|
+
*/
|
|
8
|
+
import * as Effect from 'effect/Effect';
|
|
9
|
+
import type { CreateRulesetBody } from './repository-ruleset-constraints.ts';
|
|
10
|
+
import type { RulesetOctokit, RulesetRecord, RulesetSummary } from './repository-ruleset-probe.ts';
|
|
11
|
+
|
|
12
|
+
const refuseWrite = (op: string) => () =>
|
|
13
|
+
Effect.fail(new Error(`GET-only fake: ${op} must never be called`));
|
|
14
|
+
|
|
15
|
+
/** Serves a fixed `{repo: RulesetRecord}` map and fails any write — for tests that must prove
|
|
16
|
+
* a plan is a pure read (a noop, or a refusal caught before any write). */
|
|
17
|
+
export function makeGetOnlyFake(byRepo: Readonly<Record<string, RulesetRecord>>): RulesetOctokit {
|
|
18
|
+
return {
|
|
19
|
+
list: ({ repo }) =>
|
|
20
|
+
Effect.succeed(
|
|
21
|
+
byRepo[repo] === undefined
|
|
22
|
+
? []
|
|
23
|
+
: ([
|
|
24
|
+
{
|
|
25
|
+
id: byRepo[repo].id,
|
|
26
|
+
name: byRepo[repo].name,
|
|
27
|
+
target: byRepo[repo].target,
|
|
28
|
+
source_type: byRepo[repo].source_type,
|
|
29
|
+
},
|
|
30
|
+
] satisfies RulesetSummary[]),
|
|
31
|
+
),
|
|
32
|
+
get: ({ repo, rulesetId }) =>
|
|
33
|
+
Effect.succeed(byRepo[repo]?.id === rulesetId ? byRepo[repo] : undefined),
|
|
34
|
+
create: refuseWrite('create'),
|
|
35
|
+
update: refuseWrite('update'),
|
|
36
|
+
delete: refuseWrite('delete'),
|
|
37
|
+
hasContextReportedSuccess: () => Effect.succeed(true),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RecordedWrite {
|
|
42
|
+
readonly op: 'create' | 'update' | 'delete';
|
|
43
|
+
readonly repo: string;
|
|
44
|
+
readonly body?: CreateRulesetBody;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RecordingFake {
|
|
48
|
+
readonly octokit: RulesetOctokit;
|
|
49
|
+
readonly writes: RecordedWrite[];
|
|
50
|
+
/** Whether `hasContextReportedSuccess` answers `true` — defaults to `true` (every context
|
|
51
|
+
* has reported); set per-context to test `NeverReportedContext`. */
|
|
52
|
+
readonly reportedContexts: Set<string>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let nextId = 90000;
|
|
56
|
+
|
|
57
|
+
/** An in-memory GitHub: `create` assigns an id and stores the record verbatim (merging the
|
|
58
|
+
* REQUEST body, not re-deriving it, so a test can inspect exactly what reconcile sent);
|
|
59
|
+
* `update` replaces the stored rules/enforcement/etc from the body; every write is also
|
|
60
|
+
* appended to `writes` for direct assertions ("N changed values give N non-noop results" is
|
|
61
|
+
* checked by counting entries here, not by re-reading state). */
|
|
62
|
+
export function makeRecordingFake(initial: Readonly<Record<string, RulesetRecord>>): RecordingFake {
|
|
63
|
+
const state = new Map<string, RulesetRecord>(Object.entries(initial));
|
|
64
|
+
const writes: RecordedWrite[] = [];
|
|
65
|
+
const reportedContexts = new Set<string>();
|
|
66
|
+
|
|
67
|
+
const octokit: RulesetOctokit = {
|
|
68
|
+
list: ({ repo }) => {
|
|
69
|
+
const record = state.get(repo);
|
|
70
|
+
return Effect.succeed(
|
|
71
|
+
record === undefined
|
|
72
|
+
? []
|
|
73
|
+
: ([
|
|
74
|
+
{
|
|
75
|
+
id: record.id,
|
|
76
|
+
name: record.name,
|
|
77
|
+
target: record.target,
|
|
78
|
+
source_type: record.source_type,
|
|
79
|
+
},
|
|
80
|
+
] satisfies RulesetSummary[]),
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
get: ({ repo, rulesetId }) => {
|
|
84
|
+
const record = state.get(repo);
|
|
85
|
+
return Effect.succeed(record?.id === rulesetId ? record : undefined);
|
|
86
|
+
},
|
|
87
|
+
create: ({ repo, body }) =>
|
|
88
|
+
Effect.sync(() => {
|
|
89
|
+
const id = nextId++;
|
|
90
|
+
const record: RulesetRecord = {
|
|
91
|
+
id,
|
|
92
|
+
name: body.name,
|
|
93
|
+
target: body.target ?? 'branch',
|
|
94
|
+
source_type: 'Repository',
|
|
95
|
+
enforcement: body.enforcement,
|
|
96
|
+
bypass_actors: body.bypass_actors,
|
|
97
|
+
conditions: body.conditions,
|
|
98
|
+
rules: body.rules as unknown as Record<string, unknown>[],
|
|
99
|
+
};
|
|
100
|
+
state.set(repo, record);
|
|
101
|
+
writes.push({ op: 'create', repo, body });
|
|
102
|
+
return record;
|
|
103
|
+
}),
|
|
104
|
+
update: ({ repo, rulesetId, body }) =>
|
|
105
|
+
Effect.sync(() => {
|
|
106
|
+
const existing = state.get(repo);
|
|
107
|
+
const record: RulesetRecord = {
|
|
108
|
+
id: rulesetId,
|
|
109
|
+
name: body.name,
|
|
110
|
+
target: body.target ?? 'branch',
|
|
111
|
+
source_type: 'Repository',
|
|
112
|
+
enforcement: body.enforcement,
|
|
113
|
+
bypass_actors: body.bypass_actors,
|
|
114
|
+
conditions: body.conditions,
|
|
115
|
+
rules: body.rules as unknown as Record<string, unknown>[],
|
|
116
|
+
created_at: existing?.created_at,
|
|
117
|
+
};
|
|
118
|
+
state.set(repo, record);
|
|
119
|
+
writes.push({ op: 'update', repo, body });
|
|
120
|
+
return record;
|
|
121
|
+
}),
|
|
122
|
+
delete: ({ repo, rulesetId }) =>
|
|
123
|
+
Effect.sync(() => {
|
|
124
|
+
if (state.get(repo)?.id === rulesetId) state.delete(repo);
|
|
125
|
+
writes.push({ op: 'delete', repo });
|
|
126
|
+
}),
|
|
127
|
+
hasContextReportedSuccess: ({ context }) => Effect.succeed(reportedContexts.has(context)),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return { octokit, writes, reportedContexts };
|
|
131
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 16 live GitHub rulesets this family was walked against — captured 2026-09-23 by the
|
|
3
|
+
* planner's read-only `gh api` survey (owner `taslabs-net`; no secrets in any of these rows,
|
|
4
|
+
* only ruleset configuration GitHub already shows to any collaborator). Used by
|
|
5
|
+
* repository-ruleset-lifecycle.test.ts as a GET-only loopback fake, so a real baseline diff is
|
|
6
|
+
* checked against real shapes rather than a hand-picked "easy" fixture.
|
|
7
|
+
*
|
|
8
|
+
* Two simplifications from the raw survey, both noted here rather than silently taken:
|
|
9
|
+
* - `aimto`'s `secret scan (any app)` context is recorded as plain `secret scan` — the survey
|
|
10
|
+
* did not say the wire context string differs, only that the workflow's check-suite scope
|
|
11
|
+
* does; nothing in this family reads that scope, so it would not change any comparison.
|
|
12
|
+
* - `updated_at` is a placeholder ISO date derived from the survey's two-digit day, not the
|
|
13
|
+
* real timestamp (which the survey did not capture to the second).
|
|
14
|
+
*/
|
|
15
|
+
import type { RulesetRecord } from './repository-ruleset-probe.ts';
|
|
16
|
+
|
|
17
|
+
const conditions = { ref_name: { include: ['~DEFAULT_BRANCH'], exclude: [] } };
|
|
18
|
+
|
|
19
|
+
const pullRequestRule = (extraApprovalUnattributed: boolean) => ({
|
|
20
|
+
type: 'pull_request',
|
|
21
|
+
parameters: {
|
|
22
|
+
required_approving_review_count: 0,
|
|
23
|
+
dismiss_stale_reviews_on_push: true,
|
|
24
|
+
require_code_owner_review: false,
|
|
25
|
+
require_last_push_approval: false,
|
|
26
|
+
required_review_thread_resolution: false,
|
|
27
|
+
required_reviewers: [],
|
|
28
|
+
allowed_merge_methods: ['merge', 'squash', 'rebase'],
|
|
29
|
+
require_extra_approval_for_unattributed_changes: extraApprovalUnattributed,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const statusChecksRule = (checks: readonly string[]) => ({
|
|
34
|
+
type: 'required_status_checks',
|
|
35
|
+
parameters: {
|
|
36
|
+
required_status_checks: checks.map((context) => ({ context, integration_id: null })),
|
|
37
|
+
strict_required_status_checks_policy: false,
|
|
38
|
+
do_not_enforce_on_create: false,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const STANDARD_CHECKS = ['ci', 'secret scan'] as const;
|
|
43
|
+
|
|
44
|
+
interface FixtureInput {
|
|
45
|
+
readonly repo: string;
|
|
46
|
+
readonly id: number;
|
|
47
|
+
readonly day: string;
|
|
48
|
+
readonly enforcement?: 'active' | 'disabled';
|
|
49
|
+
readonly checks?: readonly string[];
|
|
50
|
+
readonly extraApprovalUnattributed: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fixture(input: FixtureInput): RulesetRecord {
|
|
54
|
+
return {
|
|
55
|
+
id: input.id,
|
|
56
|
+
name: 'main',
|
|
57
|
+
target: 'branch',
|
|
58
|
+
source_type: 'Repository',
|
|
59
|
+
enforcement: input.enforcement ?? 'active',
|
|
60
|
+
bypass_actors: [],
|
|
61
|
+
conditions,
|
|
62
|
+
rules: [
|
|
63
|
+
{ type: 'deletion' },
|
|
64
|
+
{ type: 'non_fast_forward' },
|
|
65
|
+
pullRequestRule(input.extraApprovalUnattributed),
|
|
66
|
+
...(input.checks === undefined ? [] : [statusChecksRule(input.checks)]),
|
|
67
|
+
],
|
|
68
|
+
created_at: `2026-09-${input.day}T00:00:00Z`,
|
|
69
|
+
updated_at: `2026-09-${input.day}T00:00:00Z`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Keyed by repo name — every row from the LIVE SURVEY's per-repo ruleset table. */
|
|
74
|
+
export const LIVE_RULESET_FIXTURES: Readonly<Record<string, RulesetRecord>> = {
|
|
75
|
+
homeflare: fixture({
|
|
76
|
+
repo: 'homeflare',
|
|
77
|
+
id: 23613456,
|
|
78
|
+
day: '17',
|
|
79
|
+
extraApprovalUnattributed: true,
|
|
80
|
+
}),
|
|
81
|
+
aimto: fixture({
|
|
82
|
+
repo: 'aimto',
|
|
83
|
+
id: 23616354,
|
|
84
|
+
day: '17',
|
|
85
|
+
checks: STANDARD_CHECKS,
|
|
86
|
+
extraApprovalUnattributed: false,
|
|
87
|
+
}),
|
|
88
|
+
alerts: fixture({
|
|
89
|
+
repo: 'alerts',
|
|
90
|
+
id: 23552096,
|
|
91
|
+
day: '17',
|
|
92
|
+
checks: STANDARD_CHECKS,
|
|
93
|
+
extraApprovalUnattributed: false,
|
|
94
|
+
}),
|
|
95
|
+
anyauth: fixture({
|
|
96
|
+
repo: 'anyauth',
|
|
97
|
+
id: 23478011,
|
|
98
|
+
day: '15',
|
|
99
|
+
enforcement: 'disabled',
|
|
100
|
+
extraApprovalUnattributed: true,
|
|
101
|
+
}),
|
|
102
|
+
blog: fixture({
|
|
103
|
+
repo: 'blog',
|
|
104
|
+
id: 23598797,
|
|
105
|
+
day: '17',
|
|
106
|
+
checks: STANDARD_CHECKS,
|
|
107
|
+
extraApprovalUnattributed: false,
|
|
108
|
+
}),
|
|
109
|
+
desktop: fixture({ repo: 'desktop', id: 23601674, day: '17', extraApprovalUnattributed: true }),
|
|
110
|
+
kit: fixture({
|
|
111
|
+
repo: 'kit',
|
|
112
|
+
id: 23471358,
|
|
113
|
+
day: '15',
|
|
114
|
+
checks: STANDARD_CHECKS,
|
|
115
|
+
extraApprovalUnattributed: false,
|
|
116
|
+
}),
|
|
117
|
+
landscape: fixture({
|
|
118
|
+
repo: 'landscape',
|
|
119
|
+
id: 23555162,
|
|
120
|
+
day: '17',
|
|
121
|
+
checks: STANDARD_CHECKS,
|
|
122
|
+
extraApprovalUnattributed: false,
|
|
123
|
+
}),
|
|
124
|
+
memory: fixture({
|
|
125
|
+
repo: 'memory',
|
|
126
|
+
id: 23616353,
|
|
127
|
+
day: '17',
|
|
128
|
+
checks: STANDARD_CHECKS,
|
|
129
|
+
extraApprovalUnattributed: false,
|
|
130
|
+
}),
|
|
131
|
+
mini: fixture({
|
|
132
|
+
repo: 'mini',
|
|
133
|
+
id: 23837649,
|
|
134
|
+
day: '22',
|
|
135
|
+
checks: STANDARD_CHECKS,
|
|
136
|
+
extraApprovalUnattributed: false,
|
|
137
|
+
}),
|
|
138
|
+
'openbao-plugins': fixture({
|
|
139
|
+
repo: 'openbao-plugins',
|
|
140
|
+
id: 23657714,
|
|
141
|
+
day: '18',
|
|
142
|
+
checks: STANDARD_CHECKS,
|
|
143
|
+
extraApprovalUnattributed: false,
|
|
144
|
+
}),
|
|
145
|
+
openbao: fixture({
|
|
146
|
+
repo: 'openbao',
|
|
147
|
+
id: 23580452,
|
|
148
|
+
day: '17',
|
|
149
|
+
checks: STANDARD_CHECKS,
|
|
150
|
+
extraApprovalUnattributed: false,
|
|
151
|
+
}),
|
|
152
|
+
proxmox: fixture({
|
|
153
|
+
repo: 'proxmox',
|
|
154
|
+
id: 23837620,
|
|
155
|
+
day: '22',
|
|
156
|
+
checks: STANDARD_CHECKS,
|
|
157
|
+
extraApprovalUnattributed: false,
|
|
158
|
+
}),
|
|
159
|
+
secrets: fixture({
|
|
160
|
+
repo: 'secrets',
|
|
161
|
+
id: 23616342,
|
|
162
|
+
day: '17',
|
|
163
|
+
checks: STANDARD_CHECKS,
|
|
164
|
+
extraApprovalUnattributed: false,
|
|
165
|
+
}),
|
|
166
|
+
'subnet-calc': fixture({
|
|
167
|
+
repo: 'subnet-calc',
|
|
168
|
+
id: 23580453,
|
|
169
|
+
day: '17',
|
|
170
|
+
checks: STANDARD_CHECKS,
|
|
171
|
+
extraApprovalUnattributed: false,
|
|
172
|
+
}),
|
|
173
|
+
wiki: fixture({
|
|
174
|
+
repo: 'wiki',
|
|
175
|
+
id: 23580451,
|
|
176
|
+
day: '17',
|
|
177
|
+
checks: STANDARD_CHECKS,
|
|
178
|
+
extraApprovalUnattributed: false,
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** repos with NO live ruleset at all (measured: 0 rulesets), per the survey's RULESETS section. */
|
|
183
|
+
export const REPOS_WITH_NO_RULESET: readonly string[] = ['builds', 'memos'];
|