@devdogsuga/devtools 0.1.26 → 0.1.27

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 (271) hide show
  1. package/README.md +72 -11
  2. package/bin/devtools.mjs +4 -4
  3. package/dist/cli-CZKkNHK5.js +6619 -0
  4. package/dist/failure-log-BBPGejQv.js +256 -0
  5. package/dist/launch.js +267 -281
  6. package/dist/local-env-D8HC2JK_.js +351 -0
  7. package/dist/local-env-DrBCXqD8.js +2 -0
  8. package/dist/menu-CQBsQRgH.js +1684 -0
  9. package/dist/peer-redirect-hooks.js +15 -0
  10. package/dist/peers-RBplwovA.js +229 -0
  11. package/dist/root-C2OmZDwD.js +94 -0
  12. package/dist/stack-Bx-55oi_.js +2 -0
  13. package/dist/stack-BynCgRVS.js +181 -0
  14. package/dist/supabase-project-0knM0mqd.js +426 -0
  15. package/dist/telemetry-C4foD8MD.js +249 -0
  16. package/dist/telemetry.js +2 -148
  17. package/package.json +10 -29
  18. package/bin/devtools-ci-bare.mjs +0 -48
  19. package/bin/devtools-ci.mjs +0 -31
  20. package/dist/args.d.ts +0 -26
  21. package/dist/args.js +0 -78
  22. package/dist/bws/bw.d.ts +0 -18
  23. package/dist/bws/bw.js +0 -70
  24. package/dist/bws/client.d.ts +0 -43
  25. package/dist/bws/client.js +0 -363
  26. package/dist/bws/environments.d.ts +0 -96
  27. package/dist/bws/environments.js +0 -111
  28. package/dist/bws/pace.d.ts +0 -25
  29. package/dist/bws/pace.js +0 -43
  30. package/dist/bws/retry.d.ts +0 -12
  31. package/dist/bws/retry.js +0 -39
  32. package/dist/bws/store.d.ts +0 -1
  33. package/dist/bws/store.js +0 -32
  34. package/dist/bws/token.d.ts +0 -31
  35. package/dist/bws/token.js +0 -88
  36. package/dist/bws/vault.d.ts +0 -29
  37. package/dist/bws/vault.js +0 -200
  38. package/dist/catalog.d.ts +0 -57
  39. package/dist/catalog.js +0 -57
  40. package/dist/cf/build.d.ts +0 -13
  41. package/dist/cf/build.js +0 -65
  42. package/dist/cf/commands.d.ts +0 -1
  43. package/dist/cf/commands.js +0 -172
  44. package/dist/cf/local-env.d.ts +0 -58
  45. package/dist/cf/local-env.js +0 -177
  46. package/dist/ci.d.ts +0 -1
  47. package/dist/ci.js +0 -328
  48. package/dist/cli.d.ts +0 -11
  49. package/dist/cli.js +0 -1094
  50. package/dist/commands.d.ts +0 -261
  51. package/dist/commands.js +0 -1318
  52. package/dist/completions.d.ts +0 -4
  53. package/dist/completions.js +0 -110
  54. package/dist/cron/commands.d.ts +0 -47
  55. package/dist/cron/commands.js +0 -372
  56. package/dist/cron/discovery.d.ts +0 -55
  57. package/dist/cron/discovery.js +0 -85
  58. package/dist/cron/schema.d.ts +0 -45
  59. package/dist/cron/schema.js +0 -35
  60. package/dist/db/config-push.d.ts +0 -8
  61. package/dist/db/config-push.js +0 -11
  62. package/dist/db/connection.d.ts +0 -79
  63. package/dist/db/connection.js +0 -114
  64. package/dist/db/exec.d.ts +0 -1
  65. package/dist/db/exec.js +0 -11
  66. package/dist/db/generate-types.d.ts +0 -1
  67. package/dist/db/generate-types.js +0 -4
  68. package/dist/db/generated-env.d.ts +0 -69
  69. package/dist/db/generated-env.js +0 -113
  70. package/dist/db/introspect.d.ts +0 -1
  71. package/dist/db/introspect.js +0 -122
  72. package/dist/db/local-env.d.ts +0 -65
  73. package/dist/db/local-env.js +0 -23
  74. package/dist/db/new-migration.d.ts +0 -8
  75. package/dist/db/new-migration.js +0 -69
  76. package/dist/db/pooler.d.ts +0 -23
  77. package/dist/db/pooler.js +0 -55
  78. package/dist/db/run.d.ts +0 -68
  79. package/dist/db/run.js +0 -156
  80. package/dist/db/seed-buckets.d.ts +0 -2
  81. package/dist/db/seed-buckets.js +0 -4
  82. package/dist/db/seed-production.d.ts +0 -8
  83. package/dist/db/seed-production.js +0 -79
  84. package/dist/db/seed-roles.d.ts +0 -7
  85. package/dist/db/seed-roles.js +0 -33
  86. package/dist/db/session-refresh.d.ts +0 -43
  87. package/dist/db/session-refresh.js +0 -40
  88. package/dist/deploy/migrations.d.ts +0 -20
  89. package/dist/deploy/migrations.js +0 -68
  90. package/dist/deploy/orphans.d.ts +0 -20
  91. package/dist/deploy/orphans.js +0 -177
  92. package/dist/deploy/preflight.d.ts +0 -53
  93. package/dist/deploy/preflight.js +0 -189
  94. package/dist/deploy/report.d.ts +0 -26
  95. package/dist/deploy/report.js +0 -70
  96. package/dist/deploy/require-planner.d.ts +0 -2
  97. package/dist/deploy/require-planner.js +0 -60
  98. package/dist/deploy/require-token.d.ts +0 -15
  99. package/dist/deploy/require-token.js +0 -78
  100. package/dist/deploy/secrets-file.d.ts +0 -17
  101. package/dist/deploy/secrets-file.js +0 -166
  102. package/dist/deploy/write-env.d.ts +0 -40
  103. package/dist/deploy/write-env.js +0 -395
  104. package/dist/docs/index-pages.d.ts +0 -38
  105. package/dist/docs/index-pages.js +0 -186
  106. package/dist/emails/commands.d.ts +0 -15
  107. package/dist/emails/commands.js +0 -130
  108. package/dist/emails/email-types.d.ts +0 -13
  109. package/dist/emails/email-types.js +0 -1
  110. package/dist/emails/fixtures.d.ts +0 -20
  111. package/dist/emails/fixtures.js +0 -20
  112. package/dist/env/audit.d.ts +0 -206
  113. package/dist/env/audit.js +0 -436
  114. package/dist/env/cloudflare.d.ts +0 -21
  115. package/dist/env/cloudflare.js +0 -61
  116. package/dist/env/commands.d.ts +0 -99
  117. package/dist/env/commands.js +0 -613
  118. package/dist/env/discovery.d.ts +0 -15
  119. package/dist/env/discovery.js +0 -239
  120. package/dist/env/document.d.ts +0 -119
  121. package/dist/env/document.js +0 -323
  122. package/dist/env/example.d.ts +0 -68
  123. package/dist/env/example.js +0 -605
  124. package/dist/env/selection.d.ts +0 -105
  125. package/dist/env/selection.js +0 -228
  126. package/dist/env-entry.d.ts +0 -49
  127. package/dist/env-entry.js +0 -204
  128. package/dist/environment-doctor.d.ts +0 -45
  129. package/dist/environment-doctor.js +0 -429
  130. package/dist/environment.d.ts +0 -92
  131. package/dist/environment.js +0 -185
  132. package/dist/fingerprint.d.ts +0 -10
  133. package/dist/fingerprint.js +0 -18
  134. package/dist/gen/campus-map.d.ts +0 -1
  135. package/dist/gen/campus-map.js +0 -473
  136. package/dist/gen/commands.d.ts +0 -1
  137. package/dist/gen/commands.js +0 -25
  138. package/dist/gen/hypno.d.ts +0 -1
  139. package/dist/gen/hypno.js +0 -134
  140. package/dist/gh/client.d.ts +0 -80
  141. package/dist/gh/client.js +0 -259
  142. package/dist/gh/environments.d.ts +0 -83
  143. package/dist/gh/environments.js +0 -178
  144. package/dist/gh/rulesets/actors.d.ts +0 -14
  145. package/dist/gh/rulesets/actors.js +0 -69
  146. package/dist/gh/rulesets/api.d.ts +0 -15
  147. package/dist/gh/rulesets/api.js +0 -104
  148. package/dist/gh/rulesets/commands.d.ts +0 -3
  149. package/dist/gh/rulesets/commands.js +0 -203
  150. package/dist/gh/rulesets/desired.d.ts +0 -112
  151. package/dist/gh/rulesets/desired.js +0 -271
  152. package/dist/gh/rulesets/diff.d.ts +0 -65
  153. package/dist/gh/rulesets/diff.js +0 -170
  154. package/dist/gh/rulesets/types.d.ts +0 -76
  155. package/dist/gh/rulesets/types.js +0 -13
  156. package/dist/gh/settings/api.d.ts +0 -40
  157. package/dist/gh/settings/api.js +0 -180
  158. package/dist/gh/settings/commands.d.ts +0 -1
  159. package/dist/gh/settings/commands.js +0 -254
  160. package/dist/gh/settings/desired.d.ts +0 -53
  161. package/dist/gh/settings/desired.js +0 -43
  162. package/dist/gh/settings/diff.d.ts +0 -57
  163. package/dist/gh/settings/diff.js +0 -161
  164. package/dist/gh/settings/types.d.ts +0 -92
  165. package/dist/gh/settings/types.js +0 -11
  166. package/dist/gh/settings/workflows.d.ts +0 -41
  167. package/dist/gh/settings/workflows.js +0 -128
  168. package/dist/grantRoot.d.ts +0 -42
  169. package/dist/grantRoot.js +0 -89
  170. package/dist/help.d.ts +0 -18
  171. package/dist/help.js +0 -169
  172. package/dist/images/commands.d.ts +0 -32
  173. package/dist/images/commands.js +0 -231
  174. package/dist/images/events.d.ts +0 -47
  175. package/dist/images/events.js +0 -116
  176. package/dist/images/graphics.d.ts +0 -70
  177. package/dist/images/graphics.js +0 -234
  178. package/dist/images/open-graph-types.d.ts +0 -75
  179. package/dist/images/open-graph-types.js +0 -1
  180. package/dist/images/prompts.d.ts +0 -34
  181. package/dist/images/prompts.js +0 -100
  182. package/dist/images/render.d.ts +0 -21
  183. package/dist/images/render.js +0 -61
  184. package/dist/images/select.d.ts +0 -64
  185. package/dist/images/select.js +0 -96
  186. package/dist/instance.d.ts +0 -69
  187. package/dist/instance.js +0 -102
  188. package/dist/invocation.d.ts +0 -49
  189. package/dist/invocation.js +0 -78
  190. package/dist/launch-ci.d.ts +0 -1
  191. package/dist/launch-ci.js +0 -90
  192. package/dist/launch.d.ts +0 -18
  193. package/dist/menu.d.ts +0 -43
  194. package/dist/menu.js +0 -300
  195. package/dist/moderation.d.ts +0 -70
  196. package/dist/moderation.js +0 -128
  197. package/dist/oauth/browser.d.ts +0 -1
  198. package/dist/oauth/browser.js +0 -13
  199. package/dist/oauth/config.d.ts +0 -24
  200. package/dist/oauth/config.js +0 -24
  201. package/dist/oauth/connect-url.d.ts +0 -17
  202. package/dist/oauth/connect-url.js +0 -23
  203. package/dist/oauth/db.d.ts +0 -37
  204. package/dist/oauth/db.js +0 -108
  205. package/dist/oauth/device.d.ts +0 -93
  206. package/dist/oauth/device.js +0 -198
  207. package/dist/oauth/discovery.d.ts +0 -28
  208. package/dist/oauth/discovery.js +0 -54
  209. package/dist/oauth/env-file.d.ts +0 -6
  210. package/dist/oauth/env-file.js +0 -35
  211. package/dist/oauth/exchange.d.ts +0 -43
  212. package/dist/oauth/exchange.js +0 -93
  213. package/dist/oauth/label.d.ts +0 -1
  214. package/dist/oauth/label.js +0 -25
  215. package/dist/oauth/loopback.d.ts +0 -52
  216. package/dist/oauth/loopback.js +0 -168
  217. package/dist/oauth/pkce.d.ts +0 -6
  218. package/dist/oauth/pkce.js +0 -34
  219. package/dist/oauth/target.d.ts +0 -54
  220. package/dist/oauth/target.js +0 -102
  221. package/dist/oauth/transport.d.ts +0 -41
  222. package/dist/oauth/transport.js +0 -32
  223. package/dist/oauth/wizard.d.ts +0 -40
  224. package/dist/oauth/wizard.js +0 -578
  225. package/dist/persona.d.ts +0 -18
  226. package/dist/persona.js +0 -221
  227. package/dist/pick.d.ts +0 -6
  228. package/dist/pick.js +0 -64
  229. package/dist/pipes.d.ts +0 -1
  230. package/dist/pipes.js +0 -25
  231. package/dist/planner/checks.d.ts +0 -26
  232. package/dist/planner/checks.js +0 -104
  233. package/dist/planner/commands.d.ts +0 -10
  234. package/dist/planner/commands.js +0 -367
  235. package/dist/planner/db.d.ts +0 -15
  236. package/dist/planner/db.js +0 -33
  237. package/dist/planner/role.d.ts +0 -33
  238. package/dist/planner/role.js +0 -68
  239. package/dist/repo/peer-redirect-hooks.d.ts +0 -16
  240. package/dist/repo/peer-redirect-hooks.js +0 -10
  241. package/dist/repo/peer-redirect.d.ts +0 -4
  242. package/dist/repo/peer-redirect.js +0 -26
  243. package/dist/repo/peers.d.ts +0 -39
  244. package/dist/repo/peers.js +0 -139
  245. package/dist/repo/resolve.d.ts +0 -41
  246. package/dist/repo/resolve.js +0 -152
  247. package/dist/repo/root.d.ts +0 -23
  248. package/dist/repo/root.js +0 -91
  249. package/dist/repo/source.d.ts +0 -14
  250. package/dist/repo/source.js +0 -54
  251. package/dist/repo/supabase-project.d.ts +0 -60
  252. package/dist/repo/supabase-project.js +0 -124
  253. package/dist/repo/tsx-loader.d.ts +0 -6
  254. package/dist/repo/tsx-loader.js +0 -41
  255. package/dist/run/pick.d.ts +0 -90
  256. package/dist/run/pick.js +0 -664
  257. package/dist/setup.d.ts +0 -1
  258. package/dist/setup.js +0 -280
  259. package/dist/stack.d.ts +0 -48
  260. package/dist/stack.js +0 -403
  261. package/dist/telemetry.d.ts +0 -59
  262. package/dist/tier.d.ts +0 -40
  263. package/dist/tier.js +0 -96
  264. package/dist/ui.d.ts +0 -40
  265. package/dist/ui.js +0 -71
  266. package/dist/version.d.ts +0 -7
  267. package/dist/version.js +0 -17
  268. package/dist/workers.d.ts +0 -5
  269. package/dist/workers.js +0 -33
  270. package/dist/workflows/commands.d.ts +0 -54
  271. package/dist/workflows/commands.js +0 -763
@@ -1,112 +0,0 @@
1
- /**
2
- * The five fixed rulesets this repository must carry, as code.
3
- *
4
- * `apps/platform/src/server/github/rulesets.ts` (DevDogsUGA) has described
5
- * these in prose since it was written — its module doc counts them toward
6
- * the 75-ruleset cap — but nothing ever created them. TASK-322 is that gap:
7
- * `main-protection` blocked only deletion and force-push (a push landed on
8
- * `main` directly), `production` had no ruleset at all (a push there
9
- * deploys), and a stale `comp-branches` ruleset survived a naming change.
10
- * This module is the missing desired-state builder; `diff.ts` compares it
11
- * against what `gh api .../rulesets` actually reports; `commands.ts` is the
12
- * `devtools github rulesets` command that prints or applies the difference.
13
- *
14
- * ## Why the fixed `team/**` ruleset is safe alongside per-team ones
15
- *
16
- * `rulesets.ts`'s constraint 2 warns against ever running a broad `team/**`
17
- * ruleset alongside the per-team ones `teamSync.ts` creates: rules AGGREGATE
18
- * across rulesets sharing a ref, but bypass does NOT, so a broad ruleset
19
- * restricting `update`/`push` would leave every team blocked by the broad
20
- * one while bypassing only its own — readable, unpushable, silently.
21
- *
22
- * That warning is about a ruleset carrying `update` on `team/**`. This one
23
- * carries `creation`, `deletion` and `non_fast_forward` — deliberately NOT
24
- * `update`. No per-team ruleset (`teamRulesetPayload` in DevDogsUGA) has
25
- * ever granted a `creation` rule — `rulesets.ts`'s own doc says why: the
26
- * branch is cut by `cutTeamBranch` with the App's token before the per-team
27
- * ruleset exists, so a `creation` rule there would be inert on the first
28
- * branch and would block re-provisioning after a deletion. Two rulesets
29
- * governing disjoint rule TYPES on the same ref pattern don't aggregate into
30
- * a conflict for `creation` — there is nothing for them to disagree about.
31
- * What that rule closes is a real hole `rulesets.ts` never covered: every
32
- * team member holds a repository-wide `push` grant (GitHub team permissions
33
- * have no branch dimension), so *before* the platform provisions a team, any
34
- * member could `git push origin HEAD:refs/heads/team/<slug>` and front-run
35
- * the App — creating the ref with content of their choosing, or squatting a
36
- * slug that never gets legitimately provisioned.
37
- *
38
- * `deletion` and `non_fast_forward` DO aggregate against the per-team
39
- * ruleset's own `deletion` rule (and its deliberate absence of
40
- * `non_fast_forward`) — deliberately. A team is a bypass actor on its OWN
41
- * per-team ruleset, not on this one, so the aggregate result is that a team
42
- * can no longer delete or force-push its own branch, even though its
43
- * per-team ruleset alone would let it (bypass is ruleset-scoped, constraint
44
- * 1) — TASK-324's access-control checklist requires exactly that refusal.
45
- * `update` is excluded from this ruleset for the opposite reason: it is the
46
- * one rule type the per-team ruleset itself carries and bypasses its own
47
- * team for, so adding a second `update` rule here — which only the App and
48
- * devops bypass — would block every team's ordinary pushes to its own
49
- * branch despite its per-team ruleset correctly bypassing them. That is
50
- * exactly the failure constraint 2 describes; leaving `update` out is what
51
- * keeps this ruleset on the safe side of it.
52
- *
53
- * ## Fixed cost is now 5, not 4
54
- *
55
- * `rulesets.ts`'s constraint 3 said "Fixed cost is 4: `main`, `production`,
56
- * `~ALL` and the tag ruleset, leaving 71 for teams." This adds `team/**` as
57
- * a fifth fixed ruleset, so the constant there needs updating to 5 fixed /
58
- * 70 for teams — done alongside this file, in the same DevDogsUGA commit
59
- * that would eventually consume this reconciler's output.
60
- */
61
- import type { DesiredRuleset } from "./types.js";
62
- /** Resolved GitHub ids this module needs before it can build a plan. */
63
- export interface RulesetActors {
64
- /** The `devops` team's numeric id (`GET /orgs/{org}/teams/devops`). */
65
- devopsTeamId: number;
66
- /** The `admins` team's numeric id — bypasses the tag ruleset, as it does live today. */
67
- adminsTeamId: number;
68
- /** The DevDogs Platform GitHub App's id (`app_id`, not the installation id). */
69
- appId: number;
70
- /**
71
- * The Renovate GitHub App's id, or `undefined` when it cannot be resolved
72
- * yet (TASK-299 — Sloan has not installed it at the time this module was
73
- * written). `undefined` means "omit it from `~ALL`'s bypass list", not "fail
74
- * the plan" — `commands.ts` resolves this separately from the other three
75
- * (all required) and prints a warning rather than refusing, so re-running
76
- * after the App is installed picks it up with no code change here.
77
- */
78
- renovateAppId?: number;
79
- }
80
- /**
81
- * Canonical fixed-ruleset name -> earlier name(s) this reconciler recognizes
82
- * as the SAME slot, so the first run renames rather than creating a
83
- * duplicate that leaves the old one live and unmanaged.
84
- *
85
- * Only `main` has a legacy alias today (`main-protection`, live on GitHub as
86
- * of 2026-09-24). `production`, `~ALL` and `team/**` have no live ruleset to
87
- * rename — they are created fresh. The tag ruleset keeps its live name,
88
- * `tag-protection`, unchanged; nothing about its shape is wrong today, so
89
- * there is no rename to make.
90
- */
91
- export declare const LEGACY_NAME_ALIASES: Readonly<Record<string, readonly string[]>>;
92
- /** Ruleset names this reconciler deletes outright — nothing in the desired set replaces them. */
93
- export declare const DELETE_RULESET_NAMES: readonly string[];
94
- /**
95
- * A live ruleset name this reconciler must NEVER touch: one `teamSync.ts`
96
- * created and owns (`team/<slug>`, an exact-ref ruleset), as opposed to the
97
- * `team/**` wildcard-creation ruleset this module itself manages.
98
- *
99
- * `teamRulesetName()` in DevDogsUGA's `rulesets.ts` builds these as
100
- * `team/${slug}`, and slugs never contain `/`, so a live name is a per-team
101
- * ruleset iff it starts with `team/` and is not the literal `team/**` this
102
- * reconciler owns.
103
- */
104
- export declare function isPerTeamRulesetName(name: string): boolean;
105
- /**
106
- * The five fixed rulesets, built from resolved actor ids.
107
- *
108
- * Pure — no network, no `gh` — so `desired.test.ts` can assert the shape
109
- * without mocking anything, and `commands.ts` calls this only after
110
- * resolving `RulesetActors` from the live org.
111
- */
112
- export declare function buildDesiredRulesets(actors: RulesetActors): DesiredRuleset[];
@@ -1,271 +0,0 @@
1
- /**
2
- * Canonical fixed-ruleset name -> earlier name(s) this reconciler recognizes
3
- * as the SAME slot, so the first run renames rather than creating a
4
- * duplicate that leaves the old one live and unmanaged.
5
- *
6
- * Only `main` has a legacy alias today (`main-protection`, live on GitHub as
7
- * of 2026-09-24). `production`, `~ALL` and `team/**` have no live ruleset to
8
- * rename — they are created fresh. The tag ruleset keeps its live name,
9
- * `tag-protection`, unchanged; nothing about its shape is wrong today, so
10
- * there is no rename to make.
11
- */
12
- export const LEGACY_NAME_ALIASES = {
13
- main: ["main-protection"],
14
- };
15
- /** Ruleset names this reconciler deletes outright — nothing in the desired set replaces them. */
16
- export const DELETE_RULESET_NAMES = ["comp-branches"];
17
- /**
18
- * A live ruleset name this reconciler must NEVER touch: one `teamSync.ts`
19
- * created and owns (`team/<slug>`, an exact-ref ruleset), as opposed to the
20
- * `team/**` wildcard-creation ruleset this module itself manages.
21
- *
22
- * `teamRulesetName()` in DevDogsUGA's `rulesets.ts` builds these as
23
- * `team/${slug}`, and slugs never contain `/`, so a live name is a per-team
24
- * ruleset iff it starts with `team/` and is not the literal `team/**` this
25
- * reconciler owns.
26
- */
27
- export function isPerTeamRulesetName(name) {
28
- return name.startsWith("team/") && name !== "team/**";
29
- }
30
- /**
31
- * The five fixed rulesets, built from resolved actor ids.
32
- *
33
- * Pure — no network, no `gh` — so `desired.test.ts` can assert the shape
34
- * without mocking anything, and `commands.ts` calls this only after
35
- * resolving `RulesetActors` from the live org.
36
- */
37
- export function buildDesiredRulesets(actors) {
38
- return [
39
- {
40
- name: "main",
41
- target: "branch",
42
- enforcement: "active",
43
- conditions: {
44
- ref_name: { include: ["refs/heads/main"], exclude: [] },
45
- },
46
- // `devops` bypasses everything on `main`, including a direct push in
47
- // an emergency — unlike `production`, there is no deploy on the other
48
- // side of a `main` push, so `always` (not `pull_request`-only) is the
49
- // right bypass mode here.
50
- bypass_actors: [
51
- {
52
- actor_id: actors.devopsTeamId,
53
- actor_type: "Team",
54
- bypass_mode: "always",
55
- },
56
- ],
57
- rules: [
58
- { type: "deletion" },
59
- { type: "non_fast_forward" },
60
- // The bug this closes: `main-protection` carried `deletion` and
61
- // `non_fast_forward` only, so a plain `git push origin main` landed
62
- // directly. `update` restricts that to bypass actors.
63
- {
64
- type: "update",
65
- parameters: { update_allows_fetch_and_merge: false },
66
- },
67
- // Competition entries merge into `main` by squash — one commit per
68
- // entry, not the team branch's whole history.
69
- {
70
- type: "pull_request",
71
- parameters: {
72
- allowed_merge_methods: ["squash"],
73
- dismiss_stale_reviews_on_push: false,
74
- require_code_owner_review: false,
75
- require_last_push_approval: false,
76
- required_approving_review_count: 0,
77
- required_review_thread_resolution: false,
78
- },
79
- },
80
- ],
81
- },
82
- {
83
- name: "production",
84
- target: "branch",
85
- enforcement: "active",
86
- conditions: {
87
- ref_name: { include: ["refs/heads/production"], exclude: [] },
88
- },
89
- // A push to `production` deploys (see `GITHUB_COMPETITION_REPO`'s doc
90
- // in DevDogsUGA's env.ts: "the production branch, not a second repo,
91
- // is the deploy boundary"). `devops` bypasses on PULL REQUESTS only —
92
- // `bypass_mode: "pull_request"` — so a Promote PR still needs the
93
- // code-owner review below even from devops; only a direct push
94
- // outside a PR bypasses freely, which devops should not have either
95
- // for the deploy boundary. Kept at `pull_request` rather than
96
- // dropping the bypass entirely because devops still needs to be able
97
- // to merge a Promote PR without waiting on a second reviewer when
98
- // nobody else is around.
99
- bypass_actors: [
100
- {
101
- actor_id: actors.devopsTeamId,
102
- actor_type: "Team",
103
- bypass_mode: "pull_request",
104
- },
105
- ],
106
- rules: [
107
- { type: "deletion" },
108
- { type: "non_fast_forward" },
109
- {
110
- type: "update",
111
- parameters: { update_allows_fetch_and_merge: false },
112
- },
113
- // Promote PRs merge as a single merge commit — `production`'s
114
- // history should read as one commit per promotion, traceable back
115
- // to the `main` state it promoted.
116
- {
117
- type: "pull_request",
118
- parameters: {
119
- allowed_merge_methods: ["merge"],
120
- dismiss_stale_reviews_on_push: false,
121
- require_code_owner_review: true,
122
- require_last_push_approval: false,
123
- required_approving_review_count: 1,
124
- required_review_thread_resolution: false,
125
- },
126
- },
127
- ],
128
- },
129
- {
130
- // `~ALL` is GitHub's own wildcard for "every branch in the
131
- // repository" (`target: "branch"` scopes it to `refs/heads/*`, never
132
- // tags). It exists to close the gap `teamSync.ts`'s header names
133
- // directly: every team member holds a repository-wide `push` grant,
134
- // because GitHub team permissions have no branch dimension, so
135
- // WITHOUT this ruleset any team member can push straight to any
136
- // branch that is not their own team's — including `main` and
137
- // `production`, if either of THOSE rulesets were ever misconfigured,
138
- // and including any other stray branch nothing else names. This is
139
- // deliberately the blunt, redundant backstop.
140
- //
141
- // TASK-324's access-control checklist requires refusing a non-admin
142
- // member who tries to CREATE an arbitrary new branch too, not only
143
- // push to an existing one — `creation`, `deletion` and
144
- // `non_fast_forward` join `update` here for that. `grep`-verified
145
- // (2026-09-24) that no platform code path (`teamSync.ts`, the rest of
146
- // `server/github/`) ever creates, deletes or force-pushes a ref
147
- // outside `refs/heads/team/**` — `cutTeamBranch`'s `createRef` is the
148
- // only ref-creation call in the app, and it targets a team ref, which
149
- // `~ALL` already excludes. So the platform's own App does NOT bypass
150
- // `~ALL`; nothing here needs it to.
151
- name: "~ALL",
152
- target: "branch",
153
- enforcement: "active",
154
- conditions: {
155
- ref_name: {
156
- include: ["~ALL"],
157
- // MUST exclude `team/**`: rules aggregate across rulesets, and a
158
- // per-team ruleset's bypass (its own team, `always`) does not
159
- // exempt that team from a DIFFERENT ruleset's `update` rule
160
- // matching the same ref (`rulesets.ts`'s constraint 2). Without
161
- // this exclude, every team would be correctly bypassing its own
162
- // per-team ruleset and STILL blocked here — readable, unpushable,
163
- // and the cause invisible from either ruleset read alone.
164
- exclude: ["refs/heads/team/**"],
165
- },
166
- },
167
- bypass_actors: [
168
- {
169
- actor_id: actors.devopsTeamId,
170
- actor_type: "Team",
171
- bypass_mode: "always",
172
- },
173
- // Renovate (TASK-299) creates and updates `renovate/*` branches for
174
- // its dependency-update PRs — an ordinary bot workflow, not a hole,
175
- // so it bypasses the same way the platform App bypasses `team/**`.
176
- // Conditional: the App may not be installed yet, in which case
177
- // `commands.ts` never resolves an id for it and this entry is
178
- // omitted rather than the whole plan failing — see
179
- // `RulesetActors.renovateAppId`'s doc.
180
- ...(actors.renovateAppId !== undefined
181
- ? [
182
- {
183
- actor_id: actors.renovateAppId,
184
- actor_type: "Integration",
185
- bypass_mode: "always",
186
- },
187
- ]
188
- : []),
189
- ],
190
- rules: [
191
- {
192
- type: "update",
193
- parameters: { update_allows_fetch_and_merge: false },
194
- },
195
- { type: "creation" },
196
- { type: "deletion" },
197
- { type: "non_fast_forward" },
198
- ],
199
- },
200
- {
201
- // `creation`, `deletion` and `non_fast_forward` — deliberately NOT
202
- // `update`. Pushes to a team's own branch stay governed entirely by
203
- // that team's per-team ruleset (`teamRulesetPayload`, which grants
204
- // exactly that team `update`+`deletion` bypass on its own ref); this
205
- // ruleset must not add a SECOND `update` rule here, because rules
206
- // aggregate and only the App/devops bypass this one — an `update` rule
207
- // here would block every team's own pushes despite their per-team
208
- // ruleset correctly bypassing (`rulesets.ts`'s constraint 2, the exact
209
- // failure this file's header already warns about).
210
- //
211
- // `deletion` and `non_fast_forward`, by contrast, are SAFE to add here
212
- // precisely because they aggregate: a team bypasses its own per-team
213
- // ruleset's `deletion` rule, but is not a bypass actor on THIS one, so
214
- // the aggregate result is that nobody but the App or devops can delete
215
- // or force-push any `team/**` branch — including a team acting on its
216
- // own. See `rulesets.ts`'s `teamRulesetPayload` doc, updated alongside
217
- // this file, for what that changes about the per-team ruleset's own
218
- // "right trade" reasoning.
219
- name: "team/**",
220
- target: "branch",
221
- enforcement: "active",
222
- conditions: {
223
- ref_name: { include: ["refs/heads/team/**"], exclude: [] },
224
- },
225
- bypass_actors: [
226
- {
227
- actor_id: actors.appId,
228
- actor_type: "Integration",
229
- bypass_mode: "always",
230
- },
231
- {
232
- actor_id: actors.devopsTeamId,
233
- actor_type: "Team",
234
- bypass_mode: "always",
235
- },
236
- ],
237
- rules: [
238
- { type: "creation" },
239
- { type: "deletion" },
240
- { type: "non_fast_forward" },
241
- ],
242
- },
243
- {
244
- // The tag ruleset `rulesets.ts`'s constraint 3 counts toward the fixed
245
- // cost but never named a shape for. Kept at its live name,
246
- // `tag-protection` — nothing about its current shape is the bug this
247
- // task fixes, so there is no rename to make, only a shape to hold
248
- // steady (a plan against the live fixture is a no-op here; see
249
- // `diff.test.ts`'s idempotence case).
250
- name: "tag-protection",
251
- target: "tag",
252
- enforcement: "active",
253
- conditions: {
254
- ref_name: { include: ["refs/tags/**"], exclude: [] },
255
- },
256
- bypass_actors: [
257
- {
258
- actor_id: actors.adminsTeamId,
259
- actor_type: "Team",
260
- bypass_mode: "always",
261
- },
262
- {
263
- actor_id: actors.devopsTeamId,
264
- actor_type: "Team",
265
- bypass_mode: "always",
266
- },
267
- ],
268
- rules: [{ type: "creation" }, { type: "update" }, { type: "deletion" }],
269
- },
270
- ];
271
- }
@@ -1,65 +0,0 @@
1
- /**
2
- * Live ruleset list -> a plan of creates/updates/deletes/no-ops, against
3
- * `desired.ts`'s five fixed rulesets.
4
- *
5
- * Every live ruleset is classified exactly once, by name, into one of four
6
- * buckets — and the classification is what makes this reconciler safe to
7
- * re-run against a live org: it only ever writes to a ruleset it can name a
8
- * reason for touching.
9
- *
10
- * 1. A per-team ruleset (`isPerTeamRulesetName`) — owned by
11
- * `teamSync.ts` in DevDogsUGA. Never read past its name, never
12
- * diffed, never appears in any action list. `blast radius` here is
13
- * the whole point: a bug that let this reconciler touch a team's
14
- * ruleset would silently rewrite that team's own bypass actor.
15
- * 2. Named in `DELETE_RULESET_NAMES` — deleted outright.
16
- * 3. The canonical name (or a `LEGACY_NAME_ALIASES` entry) of a desired
17
- * ruleset — created if absent, updated (by id, renamed if the alias
18
- * matched) if its live shape differs, left as a no-op if it already
19
- * matches.
20
- * 4. Anything else — reported, never touched. An unrecognized ruleset is
21
- * as likely to be something Sloan added by hand for a reason this
22
- * reconciler doesn't know as it is to be drift, and only
23
- * `DELETE_RULESET_NAMES` is trusted to mean "remove this".
24
- */
25
- import { type RulesetActors } from "./desired.js";
26
- import type { DesiredRuleset, LiveRuleset, LiveRulesetSummary } from "./types.js";
27
- export interface CreateAction {
28
- kind: "create";
29
- desired: DesiredRuleset;
30
- }
31
- export interface UpdateAction {
32
- kind: "update";
33
- id: number;
34
- /** The live ruleset's current name, when different from `desired.name` — a rename is part of this update. */
35
- liveName: string;
36
- desired: DesiredRuleset;
37
- }
38
- export interface DeleteAction {
39
- kind: "delete";
40
- id: number;
41
- name: string;
42
- }
43
- export interface NoopEntry {
44
- name: string;
45
- id: number;
46
- }
47
- export interface RulesetPlan {
48
- creates: CreateAction[];
49
- updates: UpdateAction[];
50
- deletes: DeleteAction[];
51
- noops: NoopEntry[];
52
- /** Per-team rulesets seen live, by name — reported for visibility, never acted on. */
53
- perTeamSkipped: string[];
54
- /** Live rulesets that matched no desired slot and no delete name — reported, never acted on. */
55
- unmanaged: string[];
56
- }
57
- /**
58
- * Builds the plan. `liveDetails` must carry one fully-fetched `LiveRuleset`
59
- * for every summary in `liveSummaries` EXCEPT per-team ones — `commands.ts`
60
- * is expected to skip fetching those, since there can be up to ~70 of them
61
- * and this reconciler never reads past their name either way.
62
- */
63
- export declare function planRulesets(liveSummaries: readonly LiveRulesetSummary[], liveDetails: ReadonlyMap<number, LiveRuleset>, actors: RulesetActors, desiredRulesets: readonly DesiredRuleset[]): RulesetPlan;
64
- /** Whether a plan would write anything at all. */
65
- export declare function planHasChanges(plan: RulesetPlan): boolean;
@@ -1,170 +0,0 @@
1
- /**
2
- * Live ruleset list -> a plan of creates/updates/deletes/no-ops, against
3
- * `desired.ts`'s five fixed rulesets.
4
- *
5
- * Every live ruleset is classified exactly once, by name, into one of four
6
- * buckets — and the classification is what makes this reconciler safe to
7
- * re-run against a live org: it only ever writes to a ruleset it can name a
8
- * reason for touching.
9
- *
10
- * 1. A per-team ruleset (`isPerTeamRulesetName`) — owned by
11
- * `teamSync.ts` in DevDogsUGA. Never read past its name, never
12
- * diffed, never appears in any action list. `blast radius` here is
13
- * the whole point: a bug that let this reconciler touch a team's
14
- * ruleset would silently rewrite that team's own bypass actor.
15
- * 2. Named in `DELETE_RULESET_NAMES` — deleted outright.
16
- * 3. The canonical name (or a `LEGACY_NAME_ALIASES` entry) of a desired
17
- * ruleset — created if absent, updated (by id, renamed if the alias
18
- * matched) if its live shape differs, left as a no-op if it already
19
- * matches.
20
- * 4. Anything else — reported, never touched. An unrecognized ruleset is
21
- * as likely to be something Sloan added by hand for a reason this
22
- * reconciler doesn't know as it is to be drift, and only
23
- * `DELETE_RULESET_NAMES` is trusted to mean "remove this".
24
- */
25
- import { DELETE_RULESET_NAMES, LEGACY_NAME_ALIASES, isPerTeamRulesetName, } from "./desired.js";
26
- /**
27
- * Reduces a rule to the fields this reconciler manages, in a fixed key order,
28
- * so a live rule and its desired twin compare equal whenever they protect
29
- * the same way.
30
- *
31
- * Two ways GitHub's copy differs from what was sent, both seen live on
32
- * 2026-09-27 right after creating `production`:
33
- *
34
- * - it omits parameters that hold their default, so `update` comes back with
35
- * no `update_allows_fetch_and_merge` (see `UpdateRule.parameters`' doc in
36
- * `types.ts`);
37
- * - it adds `pull_request` parameters `desired.ts` never sets
38
- * (`dismissal_restriction`, `require_extra_approval_for_unattributed_changes`,
39
- * `required_reviewers`), in its own key order.
40
- *
41
- * Without this, every ruleset with a `pull_request` rule planned an update on
42
- * every run, so the reconciler never settled.
43
- */
44
- function canonicalizeRule(rule) {
45
- if (rule.type === "update") {
46
- return {
47
- type: "update",
48
- parameters: {
49
- update_allows_fetch_and_merge: rule.parameters?.update_allows_fetch_and_merge ?? false,
50
- },
51
- };
52
- }
53
- if (rule.type === "pull_request") {
54
- const p = rule.parameters;
55
- return {
56
- type: "pull_request",
57
- parameters: {
58
- allowed_merge_methods: [...p.allowed_merge_methods].sort(),
59
- dismiss_stale_reviews_on_push: p.dismiss_stale_reviews_on_push,
60
- require_code_owner_review: p.require_code_owner_review,
61
- require_last_push_approval: p.require_last_push_approval,
62
- required_approving_review_count: p.required_approving_review_count,
63
- required_review_thread_resolution: p.required_review_thread_resolution,
64
- },
65
- };
66
- }
67
- return { type: rule.type };
68
- }
69
- function sortRules(rules) {
70
- return [...rules]
71
- .map(canonicalizeRule)
72
- .sort((a, b) => a.type.localeCompare(b.type));
73
- }
74
- /** Sorted, and each actor rebuilt in a fixed key order for the same reason as `canonicalizeRule`. */
75
- function sortActors(actors) {
76
- return [...actors]
77
- .sort((a, b) => a.actor_type.localeCompare(b.actor_type) || a.actor_id - b.actor_id)
78
- .map((a) => ({
79
- actor_id: a.actor_id,
80
- actor_type: a.actor_type,
81
- bypass_mode: a.bypass_mode,
82
- }));
83
- }
84
- /**
85
- * A canonical, order-independent string for one ruleset's PROTECTION shape —
86
- * everything except its name and id. Two rulesets that protect identically
87
- * but list their rules or bypass actors in a different order must compare
88
- * equal; GitHub does not promise a stable order for either.
89
- */
90
- function fingerprint(r) {
91
- return JSON.stringify({
92
- target: r.target,
93
- enforcement: r.enforcement,
94
- conditions: {
95
- ref_name: {
96
- include: [...r.conditions.ref_name.include].sort(),
97
- exclude: [...r.conditions.ref_name.exclude].sort(),
98
- },
99
- },
100
- bypass_actors: sortActors(r.bypass_actors),
101
- rules: sortRules(r.rules),
102
- });
103
- }
104
- /** Every name a live ruleset could carry and still BE `desired` — its canonical name plus any legacy alias. */
105
- function candidateNames(desired) {
106
- return [desired.name, ...(LEGACY_NAME_ALIASES[desired.name] ?? [])];
107
- }
108
- /**
109
- * Builds the plan. `liveDetails` must carry one fully-fetched `LiveRuleset`
110
- * for every summary in `liveSummaries` EXCEPT per-team ones — `commands.ts`
111
- * is expected to skip fetching those, since there can be up to ~70 of them
112
- * and this reconciler never reads past their name either way.
113
- */
114
- export function planRulesets(liveSummaries, liveDetails, actors, desiredRulesets) {
115
- const plan = {
116
- creates: [],
117
- updates: [],
118
- deletes: [],
119
- noops: [],
120
- perTeamSkipped: [],
121
- unmanaged: [],
122
- };
123
- const matchedLiveIds = new Set();
124
- for (const desired of desiredRulesets) {
125
- const names = candidateNames(desired);
126
- const live = liveSummaries.find((s) => names.includes(s.name));
127
- if (!live) {
128
- plan.creates.push({ kind: "create", desired });
129
- continue;
130
- }
131
- matchedLiveIds.add(live.id);
132
- const detail = liveDetails.get(live.id);
133
- if (!detail) {
134
- throw new Error(`planRulesets: no detail fetched for live ruleset "${live.name}" (id ${live.id})`);
135
- }
136
- const renamed = detail.name !== desired.name;
137
- const drifted = fingerprint(detail) !== fingerprint(desired);
138
- if (renamed || drifted) {
139
- plan.updates.push({
140
- kind: "update",
141
- id: live.id,
142
- liveName: detail.name,
143
- desired,
144
- });
145
- }
146
- else {
147
- plan.noops.push({ name: desired.name, id: live.id });
148
- }
149
- }
150
- for (const live of liveSummaries) {
151
- if (matchedLiveIds.has(live.id))
152
- continue;
153
- if (isPerTeamRulesetName(live.name)) {
154
- plan.perTeamSkipped.push(live.name);
155
- continue;
156
- }
157
- if (DELETE_RULESET_NAMES.includes(live.name)) {
158
- plan.deletes.push({ kind: "delete", id: live.id, name: live.name });
159
- continue;
160
- }
161
- plan.unmanaged.push(live.name);
162
- }
163
- return plan;
164
- }
165
- /** Whether a plan would write anything at all. */
166
- export function planHasChanges(plan) {
167
- return (plan.creates.length > 0 ||
168
- plan.updates.length > 0 ||
169
- plan.deletes.length > 0);
170
- }