@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,178 +0,0 @@
1
- /**
2
- * The GitHub environments, and which BWS project (if any) backs each.
3
- *
4
- * GitHub environments and BWS projects are NOT the same set, and conflating
5
- * them is the mistake this file exists to prevent. There are FOUR GitHub
6
- * environments and THREE BWS projects:
7
- *
8
- * | GitHub environment | BWS project | Receives |
9
- * |--------------------|--------------|------------------------------|
10
- * | `preflight` | `preflight` | everything in the project |
11
- * | `staging` | `staging` | everything in the project |
12
- * | `production` | `production` | everything EXCEPT apply-only |
13
- * | `production-apply` | `production` | everything, apply-only too |
14
- *
15
- * The last two split one project between two GitHub environments, and that
16
- * split IS the reviewer gate. `production` deploys on a push with nothing in
17
- * front of it; `production-apply` has required reviewers. A write-capable
18
- * credential reaching the first makes the second decorative, so the routing is
19
- * enforced here rather than left to whoever last edited a file.
20
- *
21
- * ⚠️ THE GATE IS ONE-DIRECTIONAL, and reading it as symmetric is the mistake
22
- * this file made until 2026-08-17. What the gate asserts, through
23
- * `production.excludeKeys`, is that apply-tier credentials never reach the
24
- * UNREVIEWED environment, and nothing more. What the reviewed environment also
25
- * holds buys no privilege: it is the same Bitwarden project, and
26
- * `production-apply` is the strictly MORE trusted of the two. Withholding
27
- * ordinary keys from it broke every job that runs there: the config push wanted
28
- * deploy-tier OAuth secrets, and the orphan prune wanted a deploy-tier API
29
- * token. So `production-apply` now receives a SUPERSET of `production`.
30
- */
31
- import { assertRegistryLoaded } from "../env/discovery.js";
32
- import { getEnvSync } from "../repo/peers.js";
33
- export const GITHUB_ENVIRONMENTS = [
34
- "preflight",
35
- "staging",
36
- "production",
37
- "production-apply",
38
- ];
39
- export function isGithubEnvironment(v) {
40
- return GITHUB_ENVIRONMENTS.includes(v);
41
- }
42
- /**
43
- * The apply-only set, derived from the env manifests (`tier: "apply"` on the
44
- * declarations in `packages/devtools/env.ts`) rather than listed here.
45
- *
46
- * Read at ACCESS time, not module load: this module is imported by the CLI
47
- * before any manifest is, so a snapshot taken now would be empty, and an empty
48
- * apply set routes the write-capable credentials to the unreviewed
49
- * `production` environment, which is the exact failure the split exists to
50
- * prevent. The guard turns "forgot to loadRegistry()" into a crash instead.
51
- */
52
- function applyOnly() {
53
- assertRegistryLoaded();
54
- return getEnvSync().applyOnlyKeys();
55
- }
56
- /**
57
- * The plan-tier set, same shape and same load-time caveat as `applyOnly()`.
58
- * Read by `main-plan` (preflight) and `production-plan` (production); a copy
59
- * anywhere else is a credential nothing reads, so `staging` excludes it.
60
- */
61
- function planOnly() {
62
- assertRegistryLoaded();
63
- return getEnvSync().planOnlyKeys();
64
- }
65
- export const GITHUB_ENVIRONMENT_SPECS = {
66
- preflight: {
67
- bwsProject: "preflight",
68
- branch: "main",
69
- onlyKeys: null,
70
- excludeKeys: [],
71
- guarded: false,
72
- },
73
- staging: {
74
- bwsProject: "staging",
75
- branch: "main",
76
- onlyKeys: null,
77
- // Both narrow tiers: no staging job plans, and none may apply.
78
- get excludeKeys() {
79
- return [...applyOnly(), ...planOnly()];
80
- },
81
- guarded: false,
82
- },
83
- // ⚠️ THE REVIEWER GATE, in one property. `excludeKeys` here is the only
84
- // thing keeping the apply-tier credentials out of an environment that
85
- // deploys on a push to `production` with nobody in front of it. Widening it
86
- // to `[]` hands a write-capable token to an unreviewed deploy, and so does
87
- // letting `applyOnly()` answer empty, which is what `assertRegistryLoaded()`
88
- // exists to prevent. `environments.test.ts` asserts both apply keys by name
89
- // against exactly this.
90
- production: {
91
- bwsProject: "production",
92
- branch: "production",
93
- onlyKeys: null,
94
- get excludeKeys() {
95
- return applyOnly();
96
- },
97
- guarded: true,
98
- },
99
- // A SUPERSET of `production`, deliberately: everything that environment
100
- // receives, plus the apply-tier credential it may not have.
101
- //
102
- // Not a relaxation of the gate. This is the same Bitwarden project behind
103
- // required reviewers, the more trusted half of the split, so withholding a
104
- // key from it protects nothing and only starves the jobs that run here
105
- // (`production-config`, `prune-orphans`, both of which needed a deploy-tier
106
- // secret and got none). The gate is what `production` may NOT have, one row
107
- // above.
108
- "production-apply": {
109
- bwsProject: "production",
110
- branch: "production",
111
- onlyKeys: null,
112
- excludeKeys: [],
113
- guarded: true,
114
- },
115
- };
116
- // ── Routing ──────────────────────────────────────────────────────────────────
117
- //
118
- // One Bitwarden project can feed more than one GitHub environment; production
119
- // feeds two. So a push has to know which key goes where. Derived from the table
120
- // above rather than written out a second time: the split IS the reviewer gate,
121
- // and a hardcoded copy of it is a copy that can disagree.
122
- /** The GitHub environments fed by one Bitwarden project, in precedence order. */
123
- export function githubTargets(bwsProject) {
124
- return GITHUB_ENVIRONMENTS.filter((e) => GITHUB_ENVIRONMENT_SPECS[e].bwsProject === bwsProject);
125
- }
126
- /** Whether one environment takes a given key. */
127
- export function accepts(environment, key) {
128
- const spec = GITHUB_ENVIRONMENT_SPECS[environment];
129
- return spec.onlyKeys
130
- ? spec.onlyKeys.includes(key)
131
- : !spec.excludeKeys.includes(key);
132
- }
133
- /**
134
- * The PRIMARY environment a key belongs in, or `null` for "nowhere here".
135
- *
136
- * `null` is not an error. Pushing `staging` with an apply-only credential in
137
- * the file is the ordinary case: that key belongs to production and has no home
138
- * in the staging environment.
139
- *
140
- * ⚠️ "Primary", not "only". Since `production-apply` became a superset, an
141
- * ordinary production key legitimately lives in TWO environments and this
142
- * returns the first, the unreviewed one, where the deploy reads it. A caller
143
- * asking "is this copy misplaced?" wants `acceptedBy()` instead; answering that
144
- * question with this function reports every correctly-pushed copy in
145
- * `production-apply` as a stray that somebody should delete.
146
- */
147
- export function routeTo(bwsProject, key) {
148
- return acceptedBy(bwsProject, key)[0] ?? null;
149
- }
150
- /**
151
- * EVERY environment of a project that takes a key, in precedence order.
152
- *
153
- * The set-shaped question, for callers that compare against what a push
154
- * actually wrote rather than against one destination. `routeTo()` is this with
155
- * `[0]` taken, and the difference is only ever visible for `production`, the
156
- * one project that fans out to two environments.
157
- */
158
- export function acceptedBy(bwsProject, key) {
159
- return githubTargets(bwsProject).filter((e) => accepts(e, key));
160
- }
161
- /**
162
- * `accepts()` for an environment name that has not been narrowed yet, in the
163
- * argument order `AuditInput.accepted` wants.
164
- *
165
- * Exists so that the predicate `env audit` hands the audit is a NAME rather
166
- * than a lambda written at the call site: `runEnvAudit` cannot be unit-tested
167
- * without mocking Bitwarden, GitHub and Cloudflare at once, so anything spelled
168
- * out there is untested by construction.
169
- *
170
- * ⚠️ An unrecognised environment is REFUSED, not accepted. The string comes
171
- * from whatever `gh` listed, and the audit uses this to decide whether a found
172
- * copy is a stray. Failing open here would silence the finding for a copy
173
- * sitting in an environment this repository has never heard of, which is
174
- * exactly the case worth hearing about.
175
- */
176
- export function acceptsKey(key, environment) {
177
- return isGithubEnvironment(environment) && accepts(environment, key);
178
- }
@@ -1,14 +0,0 @@
1
- /** `GET /orgs/{org}/teams/{slug}` -> the team's numeric id. */
2
- export declare function resolveTeamId(org: string, slug: string): Promise<number>;
3
- /**
4
- * `GET /orgs/{org}/installations` -> the id of the App installation whose
5
- * `app_slug` matches, e.g. `devdogs-platform` (production) or
6
- * `devdogs-platform-staging`.
7
- *
8
- * `orgs/{org}/installations` rather than the App's own `/app` endpoint: the
9
- * latter needs App-JWT auth this CLI has no path to, while the installations
10
- * list is a plain org-admin GET — the exact command DevDogsUGA's own
11
- * `docs/platform/guides/identity/github-app.md` already tells an operator to
12
- * run to verify the App's permission grant.
13
- */
14
- export declare function resolveAppId(org: string, appSlug: string): Promise<number>;
@@ -1,69 +0,0 @@
1
- /**
2
- * Resolves the numeric ids `desired.ts`'s bypass actors need, by NAME —
3
- * never hardcoded, because a team or App id is only stable until somebody
4
- * deletes and recreates it, and a stale id in a bypass actor is the
5
- * dangerous failure: the ruleset then blocks the team or App it is named
6
- * for while letting whoever inherited the old id through (the same
7
- * reasoning `teamSync.ts`'s `ensureTeamRuleset` gives for always re-writing
8
- * rather than skipping on an existing ruleset).
9
- *
10
- * Both come from plain, read-only `gh api` GETs — no Octokit, same as every
11
- * other GitHub call in this package.
12
- */
13
- import { execFile } from "node:child_process";
14
- import { promisify } from "node:util";
15
- import { GhRulesetsError } from "./api.js";
16
- const run = promisify(execFile);
17
- function describe(err) {
18
- const e = err;
19
- const stderr = (e.stderr ?? "").trim();
20
- const message = e.message ?? "";
21
- return stderr || message || "gh failed with no output.";
22
- }
23
- /** `GET /orgs/{org}/teams/{slug}` -> the team's numeric id. */
24
- export async function resolveTeamId(org, slug) {
25
- try {
26
- const { stdout } = await run("gh", ["api", `orgs/${org}/teams/${slug}`], {
27
- shell: false,
28
- });
29
- const data = JSON.parse(stdout);
30
- if (typeof data.id !== "number") {
31
- throw new GhRulesetsError(`orgs/${org}/teams/${slug} returned no numeric id`);
32
- }
33
- return data.id;
34
- }
35
- catch (err) {
36
- if (err instanceof GhRulesetsError)
37
- throw err;
38
- throw new GhRulesetsError(`resolving team "${slug}" in ${org}: ${describe(err)}`);
39
- }
40
- }
41
- /**
42
- * `GET /orgs/{org}/installations` -> the id of the App installation whose
43
- * `app_slug` matches, e.g. `devdogs-platform` (production) or
44
- * `devdogs-platform-staging`.
45
- *
46
- * `orgs/{org}/installations` rather than the App's own `/app` endpoint: the
47
- * latter needs App-JWT auth this CLI has no path to, while the installations
48
- * list is a plain org-admin GET — the exact command DevDogsUGA's own
49
- * `docs/platform/guides/identity/github-app.md` already tells an operator to
50
- * run to verify the App's permission grant.
51
- */
52
- export async function resolveAppId(org, appSlug) {
53
- try {
54
- const { stdout } = await run("gh", ["api", `orgs/${org}/installations`], {
55
- shell: false,
56
- });
57
- const data = JSON.parse(stdout);
58
- const match = (data.installations ?? []).find((i) => i.app_slug === appSlug);
59
- if (!match) {
60
- throw new GhRulesetsError(`no installation with app_slug "${appSlug}" found in orgs/${org}/installations`);
61
- }
62
- return match.app_id;
63
- }
64
- catch (err) {
65
- if (err instanceof GhRulesetsError)
66
- throw err;
67
- throw new GhRulesetsError(`resolving App "${appSlug}" in ${org}: ${describe(err)}`);
68
- }
69
- }
@@ -1,15 +0,0 @@
1
- import type { DesiredRuleset, LiveRuleset, LiveRulesetSummary } from "./types.js";
2
- export declare class GhRulesetsError extends Error {
3
- }
4
- /** `owner/repo`, e.g. `DevDogsUGA/DevDogsUGA`. */
5
- export interface Repo {
6
- owner: string;
7
- repo: string;
8
- }
9
- /** `GET /repos/{owner}/{repo}/rulesets` — id, name and target only. */
10
- export declare function listRulesets(r: Repo): Promise<LiveRulesetSummary[]>;
11
- /** `GET /repos/{owner}/{repo}/rulesets/{id}` — the full ruleset. */
12
- export declare function getRuleset(r: Repo, id: number): Promise<LiveRuleset>;
13
- export declare function createRuleset(r: Repo, desired: DesiredRuleset): Promise<LiveRuleset>;
14
- export declare function updateRuleset(r: Repo, id: number, desired: DesiredRuleset): Promise<LiveRuleset>;
15
- export declare function deleteRuleset(r: Repo, id: number): Promise<void>;
@@ -1,104 +0,0 @@
1
- /**
2
- * `gh api` calls this reconciler needs: listing and reading rulesets, and
3
- * writing them (create/update/delete).
4
- *
5
- * `gh`, not Octokit — devtools has no Octokit dependency, and every other
6
- * GitHub call in this package (`../client.ts`) already goes through the
7
- * `gh` CLI for the same reason `bws` goes through its own CLI: `gh` already
8
- * owns authentication (a contributor's `gh auth login`, or `GH_TOKEN` in
9
- * CI), and re-deriving that here would mean a second credential path to
10
- * keep in step with the first.
11
- *
12
- * Reads (`listRulesets`, `getRuleset`) run unauthenticated-safe GETs and are
13
- * always allowed. Writes (`createRuleset`, `updateRuleset`, `deleteRuleset`)
14
- * need `admin` on the repository — the same requirement `gh api
15
- * .../rulesets -X POST` has always had — and `commands.ts` is the only
16
- * caller that reaches them, gated behind `--apply`.
17
- */
18
- import { execFile, spawn } from "node:child_process";
19
- import { promisify } from "node:util";
20
- const run = promisify(execFile);
21
- const MAX_BUFFER = 16 * 1024 * 1024;
22
- export class GhRulesetsError extends Error {
23
- }
24
- function describe(err) {
25
- const e = err;
26
- const stderr = (e.stderr ?? "").trim();
27
- const message = e.message ?? "";
28
- return stderr || message || "gh failed with no output.";
29
- }
30
- function path(r, suffix = "") {
31
- return `repos/${r.owner}/${r.repo}/rulesets${suffix}`;
32
- }
33
- /** `GET /repos/{owner}/{repo}/rulesets` — id, name and target only. */
34
- export async function listRulesets(r) {
35
- try {
36
- const { stdout } = await run("gh", ["api", path(r), "--paginate"], {
37
- maxBuffer: MAX_BUFFER,
38
- shell: false,
39
- });
40
- return stdout.trim() === ""
41
- ? []
42
- : JSON.parse(stdout);
43
- }
44
- catch (err) {
45
- throw new GhRulesetsError(describe(err));
46
- }
47
- }
48
- /** `GET /repos/{owner}/{repo}/rulesets/{id}` — the full ruleset. */
49
- export async function getRuleset(r, id) {
50
- try {
51
- const { stdout } = await run("gh", ["api", path(r, `/${id}`)], {
52
- maxBuffer: MAX_BUFFER,
53
- shell: false,
54
- });
55
- return JSON.parse(stdout);
56
- }
57
- catch (err) {
58
- throw new GhRulesetsError(describe(err));
59
- }
60
- }
61
- /**
62
- * Runs `gh api <path> -X <method> --input -`, writing `body` on stdin (JSON)
63
- * and returning the parsed JSON response.
64
- *
65
- * stdin, not `-f`/`-F` field flags: this reconciler always sends the WHOLE
66
- * ruleset payload as one JSON document (the shape `desired.ts` already
67
- * builds), and `gh api`'s field flags exist for building that document one
68
- * key at a time from shell arguments — a worse fit than handing over the
69
- * object this code already has. Not `-X POST --input path` via a temp file
70
- * either: stdin needs no cleanup and cannot be left behind by a crash.
71
- */
72
- async function writeJson(method, apiPath, body) {
73
- return new Promise((resolve, reject) => {
74
- const child = spawn("gh", ["api", apiPath, "-X", method, "--input", "-"], {
75
- stdio: ["pipe", "pipe", "pipe"],
76
- shell: false,
77
- });
78
- let stdout = "";
79
- let stderr = "";
80
- child.stdout.on("data", (c) => (stdout += c.toString()));
81
- child.stderr.on("data", (c) => (stderr += c.toString()));
82
- child.on("error", (err) => reject(new GhRulesetsError(describe(err))));
83
- child.on("close", (code) => {
84
- if (code !== 0) {
85
- reject(new GhRulesetsError(describe({ stderr })));
86
- return;
87
- }
88
- const trimmed = stdout.trim();
89
- resolve(trimmed === "" ? {} : JSON.parse(trimmed));
90
- });
91
- child.stdin.end(JSON.stringify(body));
92
- });
93
- }
94
- export async function createRuleset(r, desired) {
95
- return writeJson("POST", path(r), desired);
96
- }
97
- export async function updateRuleset(r, id, desired) {
98
- // PUT, not PATCH: GitHub's rulesets API has no PATCH route and answers one
99
- // with a 404, which reads like a missing ruleset rather than a wrong verb.
100
- return writeJson("PUT", path(r, `/${id}`), desired);
101
- }
102
- export async function deleteRuleset(r, id) {
103
- await writeJson("DELETE", path(r, `/${id}`), {});
104
- }
@@ -1,3 +0,0 @@
1
- import { type RulesetPlan } from "./diff.js";
2
- export declare function renderPlan(plan: RulesetPlan): string;
3
- export declare function runGithubRulesets(argv: readonly string[]): Promise<number>;
@@ -1,203 +0,0 @@
1
- /**
2
- * `devtools github rulesets` — TASK-322.
3
- *
4
- * Prints the diff between the five fixed rulesets DevDogsUGA's
5
- * `apps/platform/src/server/github/rulesets.ts` has always assumed exist
6
- * (`main`, `production`, `~ALL`, `team/**`, and the tag ruleset — see
7
- * `desired.ts`'s header) and what `gh api .../rulesets` actually reports.
8
- * Defaults to a dry-run plan; `--apply` writes it, gated behind a
9
- * confirmation unless `--yes`.
10
- *
11
- * Never touches a per-team ruleset (`teamRulesetName()`'s `team/<slug>`) —
12
- * those are `teamSync.ts`'s, not this command's — and never deletes or
13
- * creates anything outside `desired.ts`'s five names plus the one explicit
14
- * deletion (`comp-branches`). See `diff.ts`'s header for the full
15
- * classification.
16
- */
17
- import { confirm } from "@clack/prompts";
18
- import { resolveAppId, resolveTeamId } from "./actors.js";
19
- import { createRuleset, deleteRuleset, getRuleset, listRulesets, updateRuleset, } from "./api.js";
20
- import { buildDesiredRulesets, isPerTeamRulesetName } from "./desired.js";
21
- import { planHasChanges, planRulesets } from "./diff.js";
22
- import { unwrap } from "../../ui.js";
23
- const DEFAULT_ORG = "DevDogsUGA";
24
- const DEFAULT_REPO = "DevDogsUGA";
25
- const DEFAULT_APP_SLUG = "devdogs-platform";
26
- const DEFAULT_DEVOPS_SLUG = "devops";
27
- const DEFAULT_ADMINS_SLUG = "admins";
28
- /** TASK-299 — installed separately from the platform App; may not exist yet. */
29
- const DEFAULT_RENOVATE_SLUG = "renovate";
30
- function parseOptions(argv) {
31
- const opts = {
32
- org: DEFAULT_ORG,
33
- repo: DEFAULT_REPO,
34
- appSlug: DEFAULT_APP_SLUG,
35
- apply: false,
36
- yes: false,
37
- json: false,
38
- };
39
- for (let i = 0; i < argv.length; i++) {
40
- const arg = argv[i];
41
- if (arg === "--org")
42
- opts.org = argv[++i] ?? opts.org;
43
- else if (arg === "--repo")
44
- opts.repo = argv[++i] ?? opts.repo;
45
- else if (arg === "--app-slug")
46
- opts.appSlug = argv[++i] ?? opts.appSlug;
47
- else if (arg === "--apply")
48
- opts.apply = true;
49
- else if (arg === "--yes")
50
- opts.yes = true;
51
- else if (arg === "--json")
52
- opts.json = true;
53
- }
54
- return opts;
55
- }
56
- /**
57
- * Fetches full detail for every live ruleset EXCEPT per-team ones —
58
- * `diff.ts`'s `planRulesets` never reads past a per-team ruleset's name, and
59
- * there can be up to ~70 of them, so fetching each would be up to ~70 extra
60
- * GETs this reconciler has no use for.
61
- */
62
- async function fetchDetails(r, summaries) {
63
- const details = new Map();
64
- for (const s of summaries) {
65
- if (isPerTeamRulesetName(s.name))
66
- continue;
67
- details.set(s.id, await getRuleset(r, s.id));
68
- }
69
- return details;
70
- }
71
- function renderRuleLine(rule) {
72
- return rule.type;
73
- }
74
- function renderDesired(desired) {
75
- return [
76
- ` rules: ${desired.rules.map(renderRuleLine).join(", ")}`,
77
- ` bypass: ${desired.bypass_actors.length === 0
78
- ? "(none)"
79
- : desired.bypass_actors
80
- .map((a) => `${a.actor_type}#${a.actor_id} (${a.bypass_mode})`)
81
- .join(", ")}`,
82
- ];
83
- }
84
- export function renderPlan(plan) {
85
- const lines = [];
86
- for (const create of plan.creates) {
87
- lines.push(`+ create "${create.desired.name}"`);
88
- lines.push(...renderDesired(create.desired));
89
- }
90
- for (const update of plan.updates) {
91
- const renameNote = update.liveName !== update.desired.name
92
- ? ` (renamed from "${update.liveName}")`
93
- : "";
94
- lines.push(`~ update "${update.desired.name}"${renameNote} [id ${update.id}]`);
95
- lines.push(...renderDesired(update.desired));
96
- }
97
- for (const del of plan.deletes) {
98
- lines.push(`- delete "${del.name}" [id ${del.id}]`);
99
- }
100
- for (const noop of plan.noops) {
101
- lines.push(`= ok "${noop.name}" [id ${noop.id}] — already matches`);
102
- }
103
- if (plan.perTeamSkipped.length > 0) {
104
- lines.push(` skipped ${plan.perTeamSkipped.length} per-team ruleset(s), owned by teamSync.ts: ` +
105
- plan.perTeamSkipped.join(", "));
106
- }
107
- if (plan.unmanaged.length > 0) {
108
- lines.push(` unmanaged ${plan.unmanaged.join(", ")} — not a fixed ruleset or a deletion target, left alone`);
109
- }
110
- if (lines.length === 0)
111
- return "(nothing to report)";
112
- return lines.join("\n");
113
- }
114
- async function confirmApply(yes) {
115
- if (yes)
116
- return true;
117
- if (!process.stdin.isTTY) {
118
- process.stderr.write("devtools github rulesets: --yes is required to apply with no terminal available.\n");
119
- return false;
120
- }
121
- return unwrap(await confirm({
122
- message: "Apply this ruleset plan?",
123
- initialValue: false,
124
- }));
125
- }
126
- export async function runGithubRulesets(argv) {
127
- const opts = parseOptions(argv);
128
- const r = { owner: opts.org, repo: opts.repo };
129
- let devopsTeamId;
130
- let adminsTeamId;
131
- let appId;
132
- try {
133
- [devopsTeamId, adminsTeamId, appId] = await Promise.all([
134
- resolveTeamId(opts.org, DEFAULT_DEVOPS_SLUG),
135
- resolveTeamId(opts.org, DEFAULT_ADMINS_SLUG),
136
- resolveAppId(opts.org, opts.appSlug),
137
- ]);
138
- }
139
- catch (err) {
140
- process.stderr.write(`devtools github rulesets: ${err instanceof Error ? err.message : String(err)}\n`);
141
- return 1;
142
- }
143
- // Renovate (TASK-299) is resolved SEPARATELY and is allowed to fail: Sloan
144
- // may not have installed it yet, and that must not block reconciling
145
- // everything else. A failure here prints a warning and the plan simply
146
- // omits it from `~ALL`'s bypass list — re-running after the App is
147
- // installed picks it up with no further action.
148
- let renovateAppId;
149
- try {
150
- renovateAppId = await resolveAppId(opts.org, DEFAULT_RENOVATE_SLUG);
151
- }
152
- catch (err) {
153
- process.stderr.write(`devtools github rulesets: warning: could not resolve the Renovate App ` +
154
- `("${DEFAULT_RENOVATE_SLUG}") — ~ALL's plan omits it as a bypass actor ` +
155
- `until it is installed (TASK-299). ${err instanceof Error ? err.message : String(err)}\n`);
156
- }
157
- const actors = { devopsTeamId, adminsTeamId, appId, renovateAppId };
158
- const desired = buildDesiredRulesets(actors);
159
- let summaries;
160
- let details;
161
- try {
162
- summaries = await listRulesets(r);
163
- details = await fetchDetails(r, summaries);
164
- }
165
- catch (err) {
166
- process.stderr.write(`devtools github rulesets: ${err instanceof Error ? err.message : String(err)}\n`);
167
- return 1;
168
- }
169
- const plan = planRulesets(summaries, details, actors, desired);
170
- if (opts.json) {
171
- console.log(JSON.stringify(plan, null, 2));
172
- }
173
- else {
174
- console.log(renderPlan(plan));
175
- }
176
- if (!opts.apply) {
177
- if (!opts.json && planHasChanges(plan)) {
178
- console.log("\n(dry run — pass --apply to write this plan)");
179
- }
180
- return 0;
181
- }
182
- if (!planHasChanges(plan))
183
- return 0;
184
- if (!(await confirmApply(opts.yes)))
185
- return 1;
186
- try {
187
- for (const create of plan.creates) {
188
- await createRuleset(r, create.desired);
189
- }
190
- for (const update of plan.updates) {
191
- await updateRuleset(r, update.id, update.desired);
192
- }
193
- for (const del of plan.deletes) {
194
- await deleteRuleset(r, del.id);
195
- }
196
- }
197
- catch (err) {
198
- process.stderr.write(`devtools github rulesets: apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
199
- return 1;
200
- }
201
- console.log("Applied.");
202
- return 0;
203
- }