@devdogsuga/devtools 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/README.md +44 -0
  2. package/bin/devtools-ci-bare.mjs +48 -0
  3. package/bin/devtools-ci.mjs +17 -0
  4. package/bin/devtools.mjs +27 -0
  5. package/dist/args.d.ts +26 -0
  6. package/dist/args.js +78 -0
  7. package/dist/bws/bw.d.ts +9 -0
  8. package/dist/bws/bw.js +49 -0
  9. package/dist/bws/client.d.ts +43 -0
  10. package/dist/bws/client.js +363 -0
  11. package/dist/bws/environments.d.ts +96 -0
  12. package/dist/bws/environments.js +111 -0
  13. package/dist/bws/pace.d.ts +27 -0
  14. package/dist/bws/pace.js +43 -0
  15. package/dist/bws/retry.d.ts +12 -0
  16. package/dist/bws/retry.js +39 -0
  17. package/dist/bws/store.d.ts +1 -0
  18. package/dist/bws/store.js +32 -0
  19. package/dist/bws/token.d.ts +31 -0
  20. package/dist/bws/token.js +88 -0
  21. package/dist/bws/vault.d.ts +29 -0
  22. package/dist/bws/vault.js +200 -0
  23. package/dist/catalog.d.ts +55 -0
  24. package/dist/catalog.js +81 -0
  25. package/dist/cf/commands.d.ts +1 -0
  26. package/dist/cf/commands.js +182 -0
  27. package/dist/cf/local-env.d.ts +44 -0
  28. package/dist/cf/local-env.js +148 -0
  29. package/dist/ci.d.ts +1 -0
  30. package/dist/ci.js +319 -0
  31. package/dist/cli.d.ts +11 -0
  32. package/dist/cli.js +1077 -0
  33. package/dist/commands.d.ts +255 -0
  34. package/dist/commands.js +1279 -0
  35. package/dist/completions.d.ts +4 -0
  36. package/dist/completions.js +110 -0
  37. package/dist/cron/commands.d.ts +47 -0
  38. package/dist/cron/commands.js +372 -0
  39. package/dist/cron/discovery.d.ts +55 -0
  40. package/dist/cron/discovery.js +85 -0
  41. package/dist/cron/schema.d.ts +45 -0
  42. package/dist/cron/schema.js +35 -0
  43. package/dist/db/config-push.d.ts +8 -0
  44. package/dist/db/config-push.js +11 -0
  45. package/dist/db/connection.d.ts +79 -0
  46. package/dist/db/connection.js +114 -0
  47. package/dist/db/exec.d.ts +1 -0
  48. package/dist/db/exec.js +11 -0
  49. package/dist/db/generate-migration.d.ts +1 -0
  50. package/dist/db/generate-migration.js +67 -0
  51. package/dist/db/generate-types.d.ts +1 -0
  52. package/dist/db/generate-types.js +4 -0
  53. package/dist/db/introspect.d.ts +1 -0
  54. package/dist/db/introspect.js +113 -0
  55. package/dist/db/local-env.d.ts +65 -0
  56. package/dist/db/local-env.js +23 -0
  57. package/dist/db/new-migration.d.ts +1 -0
  58. package/dist/db/new-migration.js +8 -0
  59. package/dist/db/run.d.ts +70 -0
  60. package/dist/db/run.js +158 -0
  61. package/dist/db/seed-buckets.d.ts +2 -0
  62. package/dist/db/seed-buckets.js +4 -0
  63. package/dist/db/seed-production.d.ts +8 -0
  64. package/dist/db/seed-production.js +73 -0
  65. package/dist/db/seed-roles.d.ts +7 -0
  66. package/dist/db/seed-roles.js +31 -0
  67. package/dist/db/session-refresh.d.ts +43 -0
  68. package/dist/db/session-refresh.js +40 -0
  69. package/dist/deploy/migrations.d.ts +20 -0
  70. package/dist/deploy/migrations.js +68 -0
  71. package/dist/deploy/orphans.d.ts +20 -0
  72. package/dist/deploy/orphans.js +177 -0
  73. package/dist/deploy/preflight.d.ts +53 -0
  74. package/dist/deploy/preflight.js +189 -0
  75. package/dist/deploy/report.d.ts +26 -0
  76. package/dist/deploy/report.js +70 -0
  77. package/dist/deploy/require-planner.d.ts +2 -0
  78. package/dist/deploy/require-planner.js +60 -0
  79. package/dist/deploy/require-token.d.ts +15 -0
  80. package/dist/deploy/require-token.js +78 -0
  81. package/dist/deploy/secrets-file.d.ts +17 -0
  82. package/dist/deploy/secrets-file.js +167 -0
  83. package/dist/deploy/write-env.d.ts +40 -0
  84. package/dist/deploy/write-env.js +393 -0
  85. package/dist/docs/index-pages.d.ts +36 -0
  86. package/dist/docs/index-pages.js +184 -0
  87. package/dist/doctor.d.ts +62 -0
  88. package/dist/doctor.js +225 -0
  89. package/dist/emails/commands.d.ts +15 -0
  90. package/dist/emails/commands.js +130 -0
  91. package/dist/emails/email-types.d.ts +13 -0
  92. package/dist/emails/email-types.js +1 -0
  93. package/dist/emails/fixtures.d.ts +20 -0
  94. package/dist/emails/fixtures.js +20 -0
  95. package/dist/env/audit.d.ts +206 -0
  96. package/dist/env/audit.js +436 -0
  97. package/dist/env/cloudflare.d.ts +21 -0
  98. package/dist/env/cloudflare.js +61 -0
  99. package/dist/env/commands.d.ts +99 -0
  100. package/dist/env/commands.js +612 -0
  101. package/dist/env/discovery.d.ts +15 -0
  102. package/dist/env/discovery.js +226 -0
  103. package/dist/env/document.d.ts +119 -0
  104. package/dist/env/document.js +323 -0
  105. package/dist/env/example.d.ts +68 -0
  106. package/dist/env/example.js +603 -0
  107. package/dist/env/selection.d.ts +105 -0
  108. package/dist/env/selection.js +226 -0
  109. package/dist/environment.d.ts +92 -0
  110. package/dist/environment.js +206 -0
  111. package/dist/fingerprint.d.ts +10 -0
  112. package/dist/fingerprint.js +18 -0
  113. package/dist/gen/campus-map.d.ts +1 -0
  114. package/dist/gen/campus-map.js +473 -0
  115. package/dist/gen/commands.d.ts +1 -0
  116. package/dist/gen/commands.js +25 -0
  117. package/dist/gen/hypno.d.ts +1 -0
  118. package/dist/gen/hypno.js +134 -0
  119. package/dist/gh/client.d.ts +80 -0
  120. package/dist/gh/client.js +258 -0
  121. package/dist/gh/environments.d.ts +83 -0
  122. package/dist/gh/environments.js +178 -0
  123. package/dist/gh/rulesets/actors.d.ts +14 -0
  124. package/dist/gh/rulesets/actors.js +68 -0
  125. package/dist/gh/rulesets/api.d.ts +15 -0
  126. package/dist/gh/rulesets/api.js +99 -0
  127. package/dist/gh/rulesets/commands.d.ts +3 -0
  128. package/dist/gh/rulesets/commands.js +203 -0
  129. package/dist/gh/rulesets/desired.d.ts +112 -0
  130. package/dist/gh/rulesets/desired.js +247 -0
  131. package/dist/gh/rulesets/diff.d.ts +65 -0
  132. package/dist/gh/rulesets/diff.js +134 -0
  133. package/dist/gh/rulesets/types.d.ts +76 -0
  134. package/dist/gh/rulesets/types.js +13 -0
  135. package/dist/gh/settings/api.d.ts +40 -0
  136. package/dist/gh/settings/api.js +172 -0
  137. package/dist/gh/settings/commands.d.ts +1 -0
  138. package/dist/gh/settings/commands.js +245 -0
  139. package/dist/gh/settings/desired.d.ts +53 -0
  140. package/dist/gh/settings/desired.js +39 -0
  141. package/dist/gh/settings/diff.d.ts +57 -0
  142. package/dist/gh/settings/diff.js +156 -0
  143. package/dist/gh/settings/types.d.ts +92 -0
  144. package/dist/gh/settings/types.js +11 -0
  145. package/dist/gh/settings/workflows.d.ts +41 -0
  146. package/dist/gh/settings/workflows.js +124 -0
  147. package/dist/grantRoot.d.ts +42 -0
  148. package/dist/grantRoot.js +89 -0
  149. package/dist/help.d.ts +18 -0
  150. package/dist/help.js +169 -0
  151. package/dist/images/commands.d.ts +35 -0
  152. package/dist/images/commands.js +233 -0
  153. package/dist/images/events.d.ts +36 -0
  154. package/dist/images/events.js +152 -0
  155. package/dist/images/graphics.d.ts +70 -0
  156. package/dist/images/graphics.js +233 -0
  157. package/dist/images/open-graph-types.d.ts +75 -0
  158. package/dist/images/open-graph-types.js +1 -0
  159. package/dist/images/prompts.d.ts +34 -0
  160. package/dist/images/prompts.js +100 -0
  161. package/dist/images/render.d.ts +21 -0
  162. package/dist/images/render.js +57 -0
  163. package/dist/images/select.d.ts +64 -0
  164. package/dist/images/select.js +96 -0
  165. package/dist/instance.d.ts +67 -0
  166. package/dist/instance.js +129 -0
  167. package/dist/invocation.d.ts +49 -0
  168. package/dist/invocation.js +78 -0
  169. package/dist/launch-ci.d.ts +1 -0
  170. package/dist/launch-ci.js +86 -0
  171. package/dist/launch.d.ts +29 -0
  172. package/dist/launch.js +371 -0
  173. package/dist/menu.d.ts +31 -0
  174. package/dist/menu.js +277 -0
  175. package/dist/newsletter/commands.d.ts +16 -0
  176. package/dist/newsletter/commands.js +311 -0
  177. package/dist/newsletter/imap.d.ts +17 -0
  178. package/dist/newsletter/imap.js +127 -0
  179. package/dist/newsletter/loopback.d.ts +24 -0
  180. package/dist/newsletter/loopback.js +100 -0
  181. package/dist/newsletter/oauth.d.ts +26 -0
  182. package/dist/newsletter/oauth.js +130 -0
  183. package/dist/newsletter/smtp.d.ts +23 -0
  184. package/dist/newsletter/smtp.js +133 -0
  185. package/dist/oauth/browser.d.ts +1 -0
  186. package/dist/oauth/browser.js +13 -0
  187. package/dist/oauth/config.d.ts +24 -0
  188. package/dist/oauth/config.js +24 -0
  189. package/dist/oauth/connect-url.d.ts +17 -0
  190. package/dist/oauth/connect-url.js +23 -0
  191. package/dist/oauth/db.d.ts +37 -0
  192. package/dist/oauth/db.js +108 -0
  193. package/dist/oauth/device.d.ts +93 -0
  194. package/dist/oauth/device.js +190 -0
  195. package/dist/oauth/discovery.d.ts +28 -0
  196. package/dist/oauth/discovery.js +54 -0
  197. package/dist/oauth/env-file.d.ts +6 -0
  198. package/dist/oauth/env-file.js +35 -0
  199. package/dist/oauth/exchange.d.ts +43 -0
  200. package/dist/oauth/exchange.js +87 -0
  201. package/dist/oauth/label.d.ts +1 -0
  202. package/dist/oauth/label.js +25 -0
  203. package/dist/oauth/loopback.d.ts +52 -0
  204. package/dist/oauth/loopback.js +162 -0
  205. package/dist/oauth/pkce.d.ts +6 -0
  206. package/dist/oauth/pkce.js +34 -0
  207. package/dist/oauth/target.d.ts +54 -0
  208. package/dist/oauth/target.js +102 -0
  209. package/dist/oauth/transport.d.ts +41 -0
  210. package/dist/oauth/transport.js +32 -0
  211. package/dist/oauth/wizard.d.ts +33 -0
  212. package/dist/oauth/wizard.js +571 -0
  213. package/dist/pick.d.ts +6 -0
  214. package/dist/pick.js +64 -0
  215. package/dist/planner/checks.d.ts +26 -0
  216. package/dist/planner/checks.js +104 -0
  217. package/dist/planner/commands.d.ts +10 -0
  218. package/dist/planner/commands.js +367 -0
  219. package/dist/planner/db.d.ts +15 -0
  220. package/dist/planner/db.js +33 -0
  221. package/dist/planner/role.d.ts +33 -0
  222. package/dist/planner/role.js +68 -0
  223. package/dist/repo/peers.d.ts +24 -0
  224. package/dist/repo/peers.js +114 -0
  225. package/dist/repo/preflight.d.ts +109 -0
  226. package/dist/repo/preflight.js +266 -0
  227. package/dist/repo/resolve.d.ts +41 -0
  228. package/dist/repo/resolve.js +152 -0
  229. package/dist/repo/root.d.ts +23 -0
  230. package/dist/repo/root.js +91 -0
  231. package/dist/repo/source.d.ts +14 -0
  232. package/dist/repo/source.js +54 -0
  233. package/dist/repo/tsx-loader.d.ts +6 -0
  234. package/dist/repo/tsx-loader.js +41 -0
  235. package/dist/run/pick.d.ts +60 -0
  236. package/dist/run/pick.js +559 -0
  237. package/dist/setup.d.ts +1 -0
  238. package/dist/setup.js +152 -0
  239. package/dist/stack.d.ts +48 -0
  240. package/dist/stack.js +290 -0
  241. package/dist/telemetry.d.ts +34 -0
  242. package/dist/telemetry.js +93 -0
  243. package/dist/tier.d.ts +40 -0
  244. package/dist/tier.js +93 -0
  245. package/dist/tsconfig.typecheck.tsbuildinfo +1 -0
  246. package/dist/ui.d.ts +25 -0
  247. package/dist/ui.js +51 -0
  248. package/dist/workers.d.ts +5 -0
  249. package/dist/workers.js +35 -0
  250. package/dist/workflows/commands.d.ts +44 -0
  251. package/dist/workflows/commands.js +668 -0
  252. package/env.ts +158 -0
  253. package/package.json +80 -0
@@ -0,0 +1,247 @@
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
+ { actor_id: actors.devopsTeamId, actor_type: "Team", bypass_mode: "always" },
52
+ ],
53
+ rules: [
54
+ { type: "deletion" },
55
+ { type: "non_fast_forward" },
56
+ // The bug this closes: `main-protection` carried `deletion` and
57
+ // `non_fast_forward` only, so a plain `git push origin main` landed
58
+ // directly. `update` restricts that to bypass actors.
59
+ {
60
+ type: "update",
61
+ parameters: { update_allows_fetch_and_merge: false },
62
+ },
63
+ // Competition entries merge into `main` by squash — one commit per
64
+ // entry, not the team branch's whole history.
65
+ {
66
+ type: "pull_request",
67
+ parameters: {
68
+ allowed_merge_methods: ["squash"],
69
+ dismiss_stale_reviews_on_push: false,
70
+ require_code_owner_review: false,
71
+ require_last_push_approval: false,
72
+ required_approving_review_count: 0,
73
+ required_review_thread_resolution: false,
74
+ },
75
+ },
76
+ ],
77
+ },
78
+ {
79
+ name: "production",
80
+ target: "branch",
81
+ enforcement: "active",
82
+ conditions: {
83
+ ref_name: { include: ["refs/heads/production"], exclude: [] },
84
+ },
85
+ // A push to `production` deploys (see `GITHUB_COMPETITION_REPO`'s doc
86
+ // in DevDogsUGA's env.ts: "the production branch, not a second repo,
87
+ // is the deploy boundary"). `devops` bypasses on PULL REQUESTS only —
88
+ // `bypass_mode: "pull_request"` — so a Promote PR still needs the
89
+ // code-owner review below even from devops; only a direct push
90
+ // outside a PR bypasses freely, which devops should not have either
91
+ // for the deploy boundary. Kept at `pull_request` rather than
92
+ // dropping the bypass entirely because devops still needs to be able
93
+ // to merge a Promote PR without waiting on a second reviewer when
94
+ // nobody else is around.
95
+ bypass_actors: [
96
+ {
97
+ actor_id: actors.devopsTeamId,
98
+ actor_type: "Team",
99
+ bypass_mode: "pull_request",
100
+ },
101
+ ],
102
+ rules: [
103
+ { type: "deletion" },
104
+ { type: "non_fast_forward" },
105
+ {
106
+ type: "update",
107
+ parameters: { update_allows_fetch_and_merge: false },
108
+ },
109
+ // Promote PRs merge as a single merge commit — `production`'s
110
+ // history should read as one commit per promotion, traceable back
111
+ // to the `main` state it promoted.
112
+ {
113
+ type: "pull_request",
114
+ parameters: {
115
+ allowed_merge_methods: ["merge"],
116
+ dismiss_stale_reviews_on_push: false,
117
+ require_code_owner_review: true,
118
+ require_last_push_approval: false,
119
+ required_approving_review_count: 1,
120
+ required_review_thread_resolution: false,
121
+ },
122
+ },
123
+ ],
124
+ },
125
+ {
126
+ // `~ALL` is GitHub's own wildcard for "every branch in the
127
+ // repository" (`target: "branch"` scopes it to `refs/heads/*`, never
128
+ // tags). It exists to close the gap `teamSync.ts`'s header names
129
+ // directly: every team member holds a repository-wide `push` grant,
130
+ // because GitHub team permissions have no branch dimension, so
131
+ // WITHOUT this ruleset any team member can push straight to any
132
+ // branch that is not their own team's — including `main` and
133
+ // `production`, if either of THOSE rulesets were ever misconfigured,
134
+ // and including any other stray branch nothing else names. This is
135
+ // deliberately the blunt, redundant backstop.
136
+ //
137
+ // TASK-324's access-control checklist requires refusing a non-admin
138
+ // member who tries to CREATE an arbitrary new branch too, not only
139
+ // push to an existing one — `creation`, `deletion` and
140
+ // `non_fast_forward` join `update` here for that. `grep`-verified
141
+ // (2026-09-24) that no platform code path (`teamSync.ts`, the rest of
142
+ // `server/github/`) ever creates, deletes or force-pushes a ref
143
+ // outside `refs/heads/team/**` — `cutTeamBranch`'s `createRef` is the
144
+ // only ref-creation call in the app, and it targets a team ref, which
145
+ // `~ALL` already excludes. So the platform's own App does NOT bypass
146
+ // `~ALL`; nothing here needs it to.
147
+ name: "~ALL",
148
+ target: "branch",
149
+ enforcement: "active",
150
+ conditions: {
151
+ ref_name: {
152
+ include: ["~ALL"],
153
+ // MUST exclude `team/**`: rules aggregate across rulesets, and a
154
+ // per-team ruleset's bypass (its own team, `always`) does not
155
+ // exempt that team from a DIFFERENT ruleset's `update` rule
156
+ // matching the same ref (`rulesets.ts`'s constraint 2). Without
157
+ // this exclude, every team would be correctly bypassing its own
158
+ // per-team ruleset and STILL blocked here — readable, unpushable,
159
+ // and the cause invisible from either ruleset read alone.
160
+ exclude: ["refs/heads/team/**"],
161
+ },
162
+ },
163
+ bypass_actors: [
164
+ { actor_id: actors.devopsTeamId, actor_type: "Team", bypass_mode: "always" },
165
+ // Renovate (TASK-299) creates and updates `renovate/*` branches for
166
+ // its dependency-update PRs — an ordinary bot workflow, not a hole,
167
+ // so it bypasses the same way the platform App bypasses `team/**`.
168
+ // Conditional: the App may not be installed yet, in which case
169
+ // `commands.ts` never resolves an id for it and this entry is
170
+ // omitted rather than the whole plan failing — see
171
+ // `RulesetActors.renovateAppId`'s doc.
172
+ ...(actors.renovateAppId !== undefined
173
+ ? [
174
+ {
175
+ actor_id: actors.renovateAppId,
176
+ actor_type: "Integration",
177
+ bypass_mode: "always",
178
+ },
179
+ ]
180
+ : []),
181
+ ],
182
+ rules: [
183
+ {
184
+ type: "update",
185
+ parameters: { update_allows_fetch_and_merge: false },
186
+ },
187
+ { type: "creation" },
188
+ { type: "deletion" },
189
+ { type: "non_fast_forward" },
190
+ ],
191
+ },
192
+ {
193
+ // `creation`, `deletion` and `non_fast_forward` — deliberately NOT
194
+ // `update`. Pushes to a team's own branch stay governed entirely by
195
+ // that team's per-team ruleset (`teamRulesetPayload`, which grants
196
+ // exactly that team `update`+`deletion` bypass on its own ref); this
197
+ // ruleset must not add a SECOND `update` rule here, because rules
198
+ // aggregate and only the App/devops bypass this one — an `update` rule
199
+ // here would block every team's own pushes despite their per-team
200
+ // ruleset correctly bypassing (`rulesets.ts`'s constraint 2, the exact
201
+ // failure this file's header already warns about).
202
+ //
203
+ // `deletion` and `non_fast_forward`, by contrast, are SAFE to add here
204
+ // precisely because they aggregate: a team bypasses its own per-team
205
+ // ruleset's `deletion` rule, but is not a bypass actor on THIS one, so
206
+ // the aggregate result is that nobody but the App or devops can delete
207
+ // or force-push any `team/**` branch — including a team acting on its
208
+ // own. See `rulesets.ts`'s `teamRulesetPayload` doc, updated alongside
209
+ // this file, for what that changes about the per-team ruleset's own
210
+ // "right trade" reasoning.
211
+ name: "team/**",
212
+ target: "branch",
213
+ enforcement: "active",
214
+ conditions: {
215
+ ref_name: { include: ["refs/heads/team/**"], exclude: [] },
216
+ },
217
+ bypass_actors: [
218
+ { actor_id: actors.appId, actor_type: "Integration", bypass_mode: "always" },
219
+ { actor_id: actors.devopsTeamId, actor_type: "Team", bypass_mode: "always" },
220
+ ],
221
+ rules: [
222
+ { type: "creation" },
223
+ { type: "deletion" },
224
+ { type: "non_fast_forward" },
225
+ ],
226
+ },
227
+ {
228
+ // The tag ruleset `rulesets.ts`'s constraint 3 counts toward the fixed
229
+ // cost but never named a shape for. Kept at its live name,
230
+ // `tag-protection` — nothing about its current shape is the bug this
231
+ // task fixes, so there is no rename to make, only a shape to hold
232
+ // steady (a plan against the live fixture is a no-op here; see
233
+ // `diff.test.ts`'s idempotence case).
234
+ name: "tag-protection",
235
+ target: "tag",
236
+ enforcement: "active",
237
+ conditions: {
238
+ ref_name: { include: ["refs/tags/**"], exclude: [] },
239
+ },
240
+ bypass_actors: [
241
+ { actor_id: actors.adminsTeamId, actor_type: "Team", bypass_mode: "always" },
242
+ { actor_id: actors.devopsTeamId, actor_type: "Team", bypass_mode: "always" },
243
+ ],
244
+ rules: [{ type: "creation" }, { type: "update" }, { type: "deletion" }],
245
+ },
246
+ ];
247
+ }
@@ -0,0 +1,65 @@
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;
@@ -0,0 +1,134 @@
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
+ * Fills in defaults GitHub omits on the wire so an absent field and an
28
+ * explicit default-valued one compare equal — see `UpdateRule.parameters`'s
29
+ * doc in `types.ts`.
30
+ */
31
+ function canonicalizeRule(rule) {
32
+ if (rule.type === "update") {
33
+ return {
34
+ type: "update",
35
+ parameters: {
36
+ update_allows_fetch_and_merge: rule.parameters?.update_allows_fetch_and_merge ?? false,
37
+ },
38
+ };
39
+ }
40
+ return rule;
41
+ }
42
+ function sortRules(rules) {
43
+ return [...rules]
44
+ .map(canonicalizeRule)
45
+ .sort((a, b) => a.type.localeCompare(b.type));
46
+ }
47
+ function sortActors(actors) {
48
+ return [...actors].sort((a, b) => a.actor_type.localeCompare(b.actor_type) || a.actor_id - b.actor_id);
49
+ }
50
+ /**
51
+ * A canonical, order-independent string for one ruleset's PROTECTION shape —
52
+ * everything except its name and id. Two rulesets that protect identically
53
+ * but list their rules or bypass actors in a different order must compare
54
+ * equal; GitHub does not promise a stable order for either.
55
+ */
56
+ function fingerprint(r) {
57
+ return JSON.stringify({
58
+ target: r.target,
59
+ enforcement: r.enforcement,
60
+ conditions: {
61
+ ref_name: {
62
+ include: [...r.conditions.ref_name.include].sort(),
63
+ exclude: [...r.conditions.ref_name.exclude].sort(),
64
+ },
65
+ },
66
+ bypass_actors: sortActors(r.bypass_actors),
67
+ rules: sortRules(r.rules),
68
+ });
69
+ }
70
+ /** Every name a live ruleset could carry and still BE `desired` — its canonical name plus any legacy alias. */
71
+ function candidateNames(desired) {
72
+ return [desired.name, ...(LEGACY_NAME_ALIASES[desired.name] ?? [])];
73
+ }
74
+ /**
75
+ * Builds the plan. `liveDetails` must carry one fully-fetched `LiveRuleset`
76
+ * for every summary in `liveSummaries` EXCEPT per-team ones — `commands.ts`
77
+ * is expected to skip fetching those, since there can be up to ~70 of them
78
+ * and this reconciler never reads past their name either way.
79
+ */
80
+ export function planRulesets(liveSummaries, liveDetails, actors, desiredRulesets) {
81
+ const plan = {
82
+ creates: [],
83
+ updates: [],
84
+ deletes: [],
85
+ noops: [],
86
+ perTeamSkipped: [],
87
+ unmanaged: [],
88
+ };
89
+ const matchedLiveIds = new Set();
90
+ for (const desired of desiredRulesets) {
91
+ const names = candidateNames(desired);
92
+ const live = liveSummaries.find((s) => names.includes(s.name));
93
+ if (!live) {
94
+ plan.creates.push({ kind: "create", desired });
95
+ continue;
96
+ }
97
+ matchedLiveIds.add(live.id);
98
+ const detail = liveDetails.get(live.id);
99
+ if (!detail) {
100
+ throw new Error(`planRulesets: no detail fetched for live ruleset "${live.name}" (id ${live.id})`);
101
+ }
102
+ const renamed = detail.name !== desired.name;
103
+ const drifted = fingerprint(detail) !== fingerprint(desired);
104
+ if (renamed || drifted) {
105
+ plan.updates.push({
106
+ kind: "update",
107
+ id: live.id,
108
+ liveName: detail.name,
109
+ desired,
110
+ });
111
+ }
112
+ else {
113
+ plan.noops.push({ name: desired.name, id: live.id });
114
+ }
115
+ }
116
+ for (const live of liveSummaries) {
117
+ if (matchedLiveIds.has(live.id))
118
+ continue;
119
+ if (isPerTeamRulesetName(live.name)) {
120
+ plan.perTeamSkipped.push(live.name);
121
+ continue;
122
+ }
123
+ if (DELETE_RULESET_NAMES.includes(live.name)) {
124
+ plan.deletes.push({ kind: "delete", id: live.id, name: live.name });
125
+ continue;
126
+ }
127
+ plan.unmanaged.push(live.name);
128
+ }
129
+ return plan;
130
+ }
131
+ /** Whether a plan would write anything at all. */
132
+ export function planHasChanges(plan) {
133
+ return (plan.creates.length > 0 || plan.updates.length > 0 || plan.deletes.length > 0);
134
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The shapes this reconciler reads and writes.
3
+ *
4
+ * Deliberately a narrower slice of GitHub's repository-ruleset schema than
5
+ * Octokit's generated types (devtools has no Octokit dependency — every
6
+ * GitHub call in this package goes through the `gh` CLI, see `../client.ts`'s
7
+ * header for why). Only the rule types and bypass-actor shapes this
8
+ * reconciler actually emits are represented; a rule type nothing here builds
9
+ * is deliberately unrepresentable, the same reasoning
10
+ * `apps/platform/src/server/github/rulesets.ts`'s `RulesetRule` gives for
11
+ * doing the same with a smaller set.
12
+ */
13
+ export type RulesetTarget = "branch" | "tag";
14
+ export interface BypassActor {
15
+ actor_id: number;
16
+ actor_type: "Team" | "Integration";
17
+ bypass_mode: "always" | "pull_request";
18
+ }
19
+ export interface UpdateRule {
20
+ type: "update";
21
+ parameters?: {
22
+ update_allows_fetch_and_merge: boolean;
23
+ };
24
+ }
25
+ export interface DeletionRule {
26
+ type: "deletion";
27
+ }
28
+ export interface NonFastForwardRule {
29
+ type: "non_fast_forward";
30
+ }
31
+ export interface CreationRule {
32
+ type: "creation";
33
+ }
34
+ export interface PullRequestRule {
35
+ type: "pull_request";
36
+ parameters: {
37
+ allowed_merge_methods: ("merge" | "squash" | "rebase")[];
38
+ dismiss_stale_reviews_on_push: boolean;
39
+ require_code_owner_review: boolean;
40
+ require_last_push_approval: boolean;
41
+ required_approving_review_count: number;
42
+ required_review_thread_resolution: boolean;
43
+ };
44
+ }
45
+ export type Rule = UpdateRule | DeletionRule | NonFastForwardRule | CreationRule | PullRequestRule;
46
+ export interface RulesetConditions {
47
+ ref_name: {
48
+ include: string[];
49
+ exclude: string[];
50
+ };
51
+ }
52
+ /** What this reconciler wants a ruleset to look like, name included. */
53
+ export interface DesiredRuleset {
54
+ name: string;
55
+ target: RulesetTarget;
56
+ enforcement: "active";
57
+ bypass_actors: BypassActor[];
58
+ conditions: RulesetConditions;
59
+ rules: Rule[];
60
+ }
61
+ /** One entry from `GET /repos/{owner}/{repo}/rulesets` — name and id only. */
62
+ export interface LiveRulesetSummary {
63
+ id: number;
64
+ name: string;
65
+ target?: string;
66
+ }
67
+ /** The full shape from `GET /repos/{owner}/{repo}/rulesets/{id}`. */
68
+ export interface LiveRuleset {
69
+ id: number;
70
+ name: string;
71
+ target: RulesetTarget;
72
+ enforcement: string;
73
+ bypass_actors: BypassActor[];
74
+ conditions: RulesetConditions;
75
+ rules: Rule[];
76
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The shapes this reconciler reads and writes.
3
+ *
4
+ * Deliberately a narrower slice of GitHub's repository-ruleset schema than
5
+ * Octokit's generated types (devtools has no Octokit dependency — every
6
+ * GitHub call in this package goes through the `gh` CLI, see `../client.ts`'s
7
+ * header for why). Only the rule types and bypass-actor shapes this
8
+ * reconciler actually emits are represented; a rule type nothing here builds
9
+ * is deliberately unrepresentable, the same reasoning
10
+ * `apps/platform/src/server/github/rulesets.ts`'s `RulesetRule` gives for
11
+ * doing the same with a smaller set.
12
+ */
13
+ export {};
@@ -0,0 +1,40 @@
1
+ import type { LiveActionsPermissions, LiveAutomatedSecurityFixes, LiveBranchPolicy, LiveDeploymentBranchPolicy, LiveEnvironment, LiveRepo, LiveSecurityAndAnalysis, LiveSelectedActions, LiveWorkflowPermissions } from "./types.js";
2
+ export declare class GhSettingsError extends Error {
3
+ }
4
+ /** `owner/repo`, e.g. `DevDogsUGA/DevDogsUGA`. */
5
+ export interface Repo {
6
+ owner: string;
7
+ repo: string;
8
+ }
9
+ export declare function getRepo(r: Repo): Promise<LiveRepo>;
10
+ export declare function patchSecurityAndAnalysis(r: Repo, fields: Partial<LiveSecurityAndAnalysis>): Promise<void>;
11
+ /**
12
+ * `GET /repos/{owner}/{repo}/vulnerability-alerts` returns 204 with an
13
+ * empty body when Dependabot alerts are on, and 404 when they are off — the
14
+ * one endpoint in this file with no JSON to parse either way.
15
+ */
16
+ export declare function getVulnerabilityAlertsEnabled(r: Repo): Promise<boolean>;
17
+ export declare function setVulnerabilityAlertsEnabled(r: Repo, enabled: boolean): Promise<void>;
18
+ export declare function getAutomatedSecurityFixes(r: Repo): Promise<LiveAutomatedSecurityFixes>;
19
+ export declare function setAutomatedSecurityFixesEnabled(r: Repo, enabled: boolean): Promise<void>;
20
+ export declare function getActionsPermissions(r: Repo): Promise<LiveActionsPermissions>;
21
+ export declare function setActionsPermissions(r: Repo, body: LiveActionsPermissions): Promise<void>;
22
+ /**
23
+ * `GET .../actions/permissions/selected-actions` 409s outright when the
24
+ * repository's `allowed_actions` is not currently `"selected"` — there is
25
+ * no selected-actions allowlist to read when nothing restricts to one.
26
+ * Read as the empty, nothing-allowed list rather than propagating the
27
+ * error: `diff.ts` then reports the honest drift (every field disagrees
28
+ * with `desired.actions`) instead of `commands.ts` failing outright before
29
+ * it can report anything.
30
+ */
31
+ export declare function getSelectedActions(r: Repo): Promise<LiveSelectedActions>;
32
+ export declare function setSelectedActions(r: Repo, body: LiveSelectedActions): Promise<void>;
33
+ export declare function getWorkflowPermissions(r: Repo): Promise<LiveWorkflowPermissions>;
34
+ export declare function setWorkflowPermissions(r: Repo, body: LiveWorkflowPermissions): Promise<void>;
35
+ /** `null` when the environment does not exist yet — reported, never created here. */
36
+ export declare function getEnvironment(r: Repo, name: string): Promise<LiveEnvironment | null>;
37
+ export declare function getDeploymentBranchPolicies(r: Repo, name: string): Promise<LiveBranchPolicy[]>;
38
+ export declare function setDeploymentBranchPolicyMode(r: Repo, name: string, mode: LiveDeploymentBranchPolicy): Promise<void>;
39
+ export declare function addDeploymentBranchPolicy(r: Repo, name: string, pattern: string): Promise<void>;
40
+ export declare function deleteDeploymentBranchPolicy(r: Repo, name: string, policyId: number): Promise<void>;