@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.
- package/README.md +44 -0
- package/bin/devtools-ci-bare.mjs +48 -0
- package/bin/devtools-ci.mjs +17 -0
- package/bin/devtools.mjs +27 -0
- package/dist/args.d.ts +26 -0
- package/dist/args.js +78 -0
- package/dist/bws/bw.d.ts +9 -0
- package/dist/bws/bw.js +49 -0
- package/dist/bws/client.d.ts +43 -0
- package/dist/bws/client.js +363 -0
- package/dist/bws/environments.d.ts +96 -0
- package/dist/bws/environments.js +111 -0
- package/dist/bws/pace.d.ts +27 -0
- package/dist/bws/pace.js +43 -0
- package/dist/bws/retry.d.ts +12 -0
- package/dist/bws/retry.js +39 -0
- package/dist/bws/store.d.ts +1 -0
- package/dist/bws/store.js +32 -0
- package/dist/bws/token.d.ts +31 -0
- package/dist/bws/token.js +88 -0
- package/dist/bws/vault.d.ts +29 -0
- package/dist/bws/vault.js +200 -0
- package/dist/catalog.d.ts +55 -0
- package/dist/catalog.js +81 -0
- package/dist/cf/commands.d.ts +1 -0
- package/dist/cf/commands.js +182 -0
- package/dist/cf/local-env.d.ts +44 -0
- package/dist/cf/local-env.js +148 -0
- package/dist/ci.d.ts +1 -0
- package/dist/ci.js +319 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +1077 -0
- package/dist/commands.d.ts +255 -0
- package/dist/commands.js +1279 -0
- package/dist/completions.d.ts +4 -0
- package/dist/completions.js +110 -0
- package/dist/cron/commands.d.ts +47 -0
- package/dist/cron/commands.js +372 -0
- package/dist/cron/discovery.d.ts +55 -0
- package/dist/cron/discovery.js +85 -0
- package/dist/cron/schema.d.ts +45 -0
- package/dist/cron/schema.js +35 -0
- package/dist/db/config-push.d.ts +8 -0
- package/dist/db/config-push.js +11 -0
- package/dist/db/connection.d.ts +79 -0
- package/dist/db/connection.js +114 -0
- package/dist/db/exec.d.ts +1 -0
- package/dist/db/exec.js +11 -0
- package/dist/db/generate-migration.d.ts +1 -0
- package/dist/db/generate-migration.js +67 -0
- package/dist/db/generate-types.d.ts +1 -0
- package/dist/db/generate-types.js +4 -0
- package/dist/db/introspect.d.ts +1 -0
- package/dist/db/introspect.js +113 -0
- package/dist/db/local-env.d.ts +65 -0
- package/dist/db/local-env.js +23 -0
- package/dist/db/new-migration.d.ts +1 -0
- package/dist/db/new-migration.js +8 -0
- package/dist/db/run.d.ts +70 -0
- package/dist/db/run.js +158 -0
- package/dist/db/seed-buckets.d.ts +2 -0
- package/dist/db/seed-buckets.js +4 -0
- package/dist/db/seed-production.d.ts +8 -0
- package/dist/db/seed-production.js +73 -0
- package/dist/db/seed-roles.d.ts +7 -0
- package/dist/db/seed-roles.js +31 -0
- package/dist/db/session-refresh.d.ts +43 -0
- package/dist/db/session-refresh.js +40 -0
- package/dist/deploy/migrations.d.ts +20 -0
- package/dist/deploy/migrations.js +68 -0
- package/dist/deploy/orphans.d.ts +20 -0
- package/dist/deploy/orphans.js +177 -0
- package/dist/deploy/preflight.d.ts +53 -0
- package/dist/deploy/preflight.js +189 -0
- package/dist/deploy/report.d.ts +26 -0
- package/dist/deploy/report.js +70 -0
- package/dist/deploy/require-planner.d.ts +2 -0
- package/dist/deploy/require-planner.js +60 -0
- package/dist/deploy/require-token.d.ts +15 -0
- package/dist/deploy/require-token.js +78 -0
- package/dist/deploy/secrets-file.d.ts +17 -0
- package/dist/deploy/secrets-file.js +167 -0
- package/dist/deploy/write-env.d.ts +40 -0
- package/dist/deploy/write-env.js +393 -0
- package/dist/docs/index-pages.d.ts +36 -0
- package/dist/docs/index-pages.js +184 -0
- package/dist/doctor.d.ts +62 -0
- package/dist/doctor.js +225 -0
- package/dist/emails/commands.d.ts +15 -0
- package/dist/emails/commands.js +130 -0
- package/dist/emails/email-types.d.ts +13 -0
- package/dist/emails/email-types.js +1 -0
- package/dist/emails/fixtures.d.ts +20 -0
- package/dist/emails/fixtures.js +20 -0
- package/dist/env/audit.d.ts +206 -0
- package/dist/env/audit.js +436 -0
- package/dist/env/cloudflare.d.ts +21 -0
- package/dist/env/cloudflare.js +61 -0
- package/dist/env/commands.d.ts +99 -0
- package/dist/env/commands.js +612 -0
- package/dist/env/discovery.d.ts +15 -0
- package/dist/env/discovery.js +226 -0
- package/dist/env/document.d.ts +119 -0
- package/dist/env/document.js +323 -0
- package/dist/env/example.d.ts +68 -0
- package/dist/env/example.js +603 -0
- package/dist/env/selection.d.ts +105 -0
- package/dist/env/selection.js +226 -0
- package/dist/environment.d.ts +92 -0
- package/dist/environment.js +206 -0
- package/dist/fingerprint.d.ts +10 -0
- package/dist/fingerprint.js +18 -0
- package/dist/gen/campus-map.d.ts +1 -0
- package/dist/gen/campus-map.js +473 -0
- package/dist/gen/commands.d.ts +1 -0
- package/dist/gen/commands.js +25 -0
- package/dist/gen/hypno.d.ts +1 -0
- package/dist/gen/hypno.js +134 -0
- package/dist/gh/client.d.ts +80 -0
- package/dist/gh/client.js +258 -0
- package/dist/gh/environments.d.ts +83 -0
- package/dist/gh/environments.js +178 -0
- package/dist/gh/rulesets/actors.d.ts +14 -0
- package/dist/gh/rulesets/actors.js +68 -0
- package/dist/gh/rulesets/api.d.ts +15 -0
- package/dist/gh/rulesets/api.js +99 -0
- package/dist/gh/rulesets/commands.d.ts +3 -0
- package/dist/gh/rulesets/commands.js +203 -0
- package/dist/gh/rulesets/desired.d.ts +112 -0
- package/dist/gh/rulesets/desired.js +247 -0
- package/dist/gh/rulesets/diff.d.ts +65 -0
- package/dist/gh/rulesets/diff.js +134 -0
- package/dist/gh/rulesets/types.d.ts +76 -0
- package/dist/gh/rulesets/types.js +13 -0
- package/dist/gh/settings/api.d.ts +40 -0
- package/dist/gh/settings/api.js +172 -0
- package/dist/gh/settings/commands.d.ts +1 -0
- package/dist/gh/settings/commands.js +245 -0
- package/dist/gh/settings/desired.d.ts +53 -0
- package/dist/gh/settings/desired.js +39 -0
- package/dist/gh/settings/diff.d.ts +57 -0
- package/dist/gh/settings/diff.js +156 -0
- package/dist/gh/settings/types.d.ts +92 -0
- package/dist/gh/settings/types.js +11 -0
- package/dist/gh/settings/workflows.d.ts +41 -0
- package/dist/gh/settings/workflows.js +124 -0
- package/dist/grantRoot.d.ts +42 -0
- package/dist/grantRoot.js +89 -0
- package/dist/help.d.ts +18 -0
- package/dist/help.js +169 -0
- package/dist/images/commands.d.ts +35 -0
- package/dist/images/commands.js +233 -0
- package/dist/images/events.d.ts +36 -0
- package/dist/images/events.js +152 -0
- package/dist/images/graphics.d.ts +70 -0
- package/dist/images/graphics.js +233 -0
- package/dist/images/open-graph-types.d.ts +75 -0
- package/dist/images/open-graph-types.js +1 -0
- package/dist/images/prompts.d.ts +34 -0
- package/dist/images/prompts.js +100 -0
- package/dist/images/render.d.ts +21 -0
- package/dist/images/render.js +57 -0
- package/dist/images/select.d.ts +64 -0
- package/dist/images/select.js +96 -0
- package/dist/instance.d.ts +67 -0
- package/dist/instance.js +129 -0
- package/dist/invocation.d.ts +49 -0
- package/dist/invocation.js +78 -0
- package/dist/launch-ci.d.ts +1 -0
- package/dist/launch-ci.js +86 -0
- package/dist/launch.d.ts +29 -0
- package/dist/launch.js +371 -0
- package/dist/menu.d.ts +31 -0
- package/dist/menu.js +277 -0
- package/dist/newsletter/commands.d.ts +16 -0
- package/dist/newsletter/commands.js +311 -0
- package/dist/newsletter/imap.d.ts +17 -0
- package/dist/newsletter/imap.js +127 -0
- package/dist/newsletter/loopback.d.ts +24 -0
- package/dist/newsletter/loopback.js +100 -0
- package/dist/newsletter/oauth.d.ts +26 -0
- package/dist/newsletter/oauth.js +130 -0
- package/dist/newsletter/smtp.d.ts +23 -0
- package/dist/newsletter/smtp.js +133 -0
- package/dist/oauth/browser.d.ts +1 -0
- package/dist/oauth/browser.js +13 -0
- package/dist/oauth/config.d.ts +24 -0
- package/dist/oauth/config.js +24 -0
- package/dist/oauth/connect-url.d.ts +17 -0
- package/dist/oauth/connect-url.js +23 -0
- package/dist/oauth/db.d.ts +37 -0
- package/dist/oauth/db.js +108 -0
- package/dist/oauth/device.d.ts +93 -0
- package/dist/oauth/device.js +190 -0
- package/dist/oauth/discovery.d.ts +28 -0
- package/dist/oauth/discovery.js +54 -0
- package/dist/oauth/env-file.d.ts +6 -0
- package/dist/oauth/env-file.js +35 -0
- package/dist/oauth/exchange.d.ts +43 -0
- package/dist/oauth/exchange.js +87 -0
- package/dist/oauth/label.d.ts +1 -0
- package/dist/oauth/label.js +25 -0
- package/dist/oauth/loopback.d.ts +52 -0
- package/dist/oauth/loopback.js +162 -0
- package/dist/oauth/pkce.d.ts +6 -0
- package/dist/oauth/pkce.js +34 -0
- package/dist/oauth/target.d.ts +54 -0
- package/dist/oauth/target.js +102 -0
- package/dist/oauth/transport.d.ts +41 -0
- package/dist/oauth/transport.js +32 -0
- package/dist/oauth/wizard.d.ts +33 -0
- package/dist/oauth/wizard.js +571 -0
- package/dist/pick.d.ts +6 -0
- package/dist/pick.js +64 -0
- package/dist/planner/checks.d.ts +26 -0
- package/dist/planner/checks.js +104 -0
- package/dist/planner/commands.d.ts +10 -0
- package/dist/planner/commands.js +367 -0
- package/dist/planner/db.d.ts +15 -0
- package/dist/planner/db.js +33 -0
- package/dist/planner/role.d.ts +33 -0
- package/dist/planner/role.js +68 -0
- package/dist/repo/peers.d.ts +24 -0
- package/dist/repo/peers.js +114 -0
- package/dist/repo/preflight.d.ts +109 -0
- package/dist/repo/preflight.js +266 -0
- package/dist/repo/resolve.d.ts +41 -0
- package/dist/repo/resolve.js +152 -0
- package/dist/repo/root.d.ts +23 -0
- package/dist/repo/root.js +91 -0
- package/dist/repo/source.d.ts +14 -0
- package/dist/repo/source.js +54 -0
- package/dist/repo/tsx-loader.d.ts +6 -0
- package/dist/repo/tsx-loader.js +41 -0
- package/dist/run/pick.d.ts +60 -0
- package/dist/run/pick.js +559 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +152 -0
- package/dist/stack.d.ts +48 -0
- package/dist/stack.js +290 -0
- package/dist/telemetry.d.ts +34 -0
- package/dist/telemetry.js +93 -0
- package/dist/tier.d.ts +40 -0
- package/dist/tier.js +93 -0
- package/dist/tsconfig.typecheck.tsbuildinfo +1 -0
- package/dist/ui.d.ts +25 -0
- package/dist/ui.js +51 -0
- package/dist/workers.d.ts +5 -0
- package/dist/workers.js +35 -0
- package/dist/workflows/commands.d.ts +44 -0
- package/dist/workflows/commands.js +668 -0
- package/env.ts +158 -0
- 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>;
|