@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,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gh api` calls this reconciler needs, over the repository-settings
|
|
3
|
+
* endpoints TASK-342 governs: security-and-analysis, Dependabot, Actions
|
|
4
|
+
* permissions, and environment deployment-branch policies.
|
|
5
|
+
*
|
|
6
|
+
* `gh`, not Octokit — same reasoning as `../rulesets/api.ts`'s header, which
|
|
7
|
+
* this module otherwise mirrors closely (its own `run`/`writeJson` rather
|
|
8
|
+
* than a shared import, matching `../environments.ts` and `../rulesets/
|
|
9
|
+
* api.ts` each carrying their own — this package's established way of
|
|
10
|
+
* keeping one `gh` wrapper's blast radius from the next).
|
|
11
|
+
*
|
|
12
|
+
* Reads are always allowed. Writes need `admin` on the repository, exactly
|
|
13
|
+
* as they do today from the Settings UI, and `commands.ts` is the only
|
|
14
|
+
* caller that reaches them, gated behind `--apply`.
|
|
15
|
+
*/
|
|
16
|
+
import { execFile, spawn } from "node:child_process";
|
|
17
|
+
import { promisify } from "node:util";
|
|
18
|
+
const run = promisify(execFile);
|
|
19
|
+
const MAX_BUFFER = 16 * 1024 * 1024;
|
|
20
|
+
export class GhSettingsError extends Error {
|
|
21
|
+
}
|
|
22
|
+
function describe(err) {
|
|
23
|
+
const e = err;
|
|
24
|
+
const stderr = (e.stderr ?? "").trim();
|
|
25
|
+
return stderr || e.message || "gh failed with no output.";
|
|
26
|
+
}
|
|
27
|
+
function repoPath(r, suffix = "") {
|
|
28
|
+
return `repos/${r.owner}/${r.repo}${suffix}`;
|
|
29
|
+
}
|
|
30
|
+
async function readJson(apiPath) {
|
|
31
|
+
try {
|
|
32
|
+
const { stdout } = await run("gh", ["api", apiPath], {
|
|
33
|
+
maxBuffer: MAX_BUFFER,
|
|
34
|
+
shell: false,
|
|
35
|
+
});
|
|
36
|
+
return JSON.parse(stdout);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
throw new GhSettingsError(describe(err));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Runs `gh api <path> -X <method>`, writing `body` on stdin (JSON) when
|
|
44
|
+
* given, and returning the parsed JSON response (or `{}` for a body-less
|
|
45
|
+
* response). See `../rulesets/api.ts`'s `writeJson` for why stdin, not `-f`
|
|
46
|
+
* field flags or a temp file.
|
|
47
|
+
*/
|
|
48
|
+
async function writeJson(method, apiPath, body) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const args = body === undefined
|
|
51
|
+
? ["api", apiPath, "-X", method]
|
|
52
|
+
: ["api", apiPath, "-X", method, "--input", "-"];
|
|
53
|
+
const child = spawn("gh", args, { stdio: ["pipe", "pipe", "pipe"], shell: false });
|
|
54
|
+
let stdout = "";
|
|
55
|
+
let stderr = "";
|
|
56
|
+
child.stdout.on("data", (c) => (stdout += c.toString()));
|
|
57
|
+
child.stderr.on("data", (c) => (stderr += c.toString()));
|
|
58
|
+
child.on("error", (err) => reject(new GhSettingsError(describe(err))));
|
|
59
|
+
child.on("close", (code) => {
|
|
60
|
+
if (code !== 0) {
|
|
61
|
+
reject(new GhSettingsError(describe({ stderr })));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const trimmed = stdout.trim();
|
|
65
|
+
resolve(trimmed === "" ? {} : JSON.parse(trimmed));
|
|
66
|
+
});
|
|
67
|
+
if (body === undefined)
|
|
68
|
+
child.stdin.end();
|
|
69
|
+
else
|
|
70
|
+
child.stdin.end(JSON.stringify(body));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// ── repository / security-and-analysis ──────────────────────────────────────
|
|
74
|
+
export async function getRepo(r) {
|
|
75
|
+
return readJson(repoPath(r));
|
|
76
|
+
}
|
|
77
|
+
export async function patchSecurityAndAnalysis(r, fields) {
|
|
78
|
+
await writeJson("PATCH", repoPath(r), { security_and_analysis: fields });
|
|
79
|
+
}
|
|
80
|
+
// ── Dependabot ───────────────────────────────────────────────────────────────
|
|
81
|
+
/**
|
|
82
|
+
* `GET /repos/{owner}/{repo}/vulnerability-alerts` returns 204 with an
|
|
83
|
+
* empty body when Dependabot alerts are on, and 404 when they are off — the
|
|
84
|
+
* one endpoint in this file with no JSON to parse either way.
|
|
85
|
+
*/
|
|
86
|
+
export async function getVulnerabilityAlertsEnabled(r) {
|
|
87
|
+
try {
|
|
88
|
+
await run("gh", ["api", repoPath(r, "/vulnerability-alerts")], {
|
|
89
|
+
maxBuffer: MAX_BUFFER,
|
|
90
|
+
shell: false,
|
|
91
|
+
});
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
if (/\b404\b/.test(describe(err)))
|
|
96
|
+
return false;
|
|
97
|
+
throw new GhSettingsError(describe(err));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export async function setVulnerabilityAlertsEnabled(r, enabled) {
|
|
101
|
+
await writeJson(enabled ? "PUT" : "DELETE", repoPath(r, "/vulnerability-alerts"));
|
|
102
|
+
}
|
|
103
|
+
export async function getAutomatedSecurityFixes(r) {
|
|
104
|
+
return readJson(repoPath(r, "/automated-security-fixes"));
|
|
105
|
+
}
|
|
106
|
+
export async function setAutomatedSecurityFixesEnabled(r, enabled) {
|
|
107
|
+
await writeJson(enabled ? "PUT" : "DELETE", repoPath(r, "/automated-security-fixes"));
|
|
108
|
+
}
|
|
109
|
+
// ── Actions permissions ──────────────────────────────────────────────────────
|
|
110
|
+
export async function getActionsPermissions(r) {
|
|
111
|
+
return readJson(repoPath(r, "/actions/permissions"));
|
|
112
|
+
}
|
|
113
|
+
export async function setActionsPermissions(r, body) {
|
|
114
|
+
await writeJson("PUT", repoPath(r, "/actions/permissions"), body);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* `GET .../actions/permissions/selected-actions` 409s outright when the
|
|
118
|
+
* repository's `allowed_actions` is not currently `"selected"` — there is
|
|
119
|
+
* no selected-actions allowlist to read when nothing restricts to one.
|
|
120
|
+
* Read as the empty, nothing-allowed list rather than propagating the
|
|
121
|
+
* error: `diff.ts` then reports the honest drift (every field disagrees
|
|
122
|
+
* with `desired.actions`) instead of `commands.ts` failing outright before
|
|
123
|
+
* it can report anything.
|
|
124
|
+
*/
|
|
125
|
+
export async function getSelectedActions(r) {
|
|
126
|
+
try {
|
|
127
|
+
return await readJson(repoPath(r, "/actions/permissions/selected-actions"));
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
if (err instanceof GhSettingsError &&
|
|
131
|
+
(/\b409\b/.test(err.message) || /\(conflict\)/i.test(err.message))) {
|
|
132
|
+
return { github_owned_allowed: false, verified_allowed: false, patterns_allowed: [] };
|
|
133
|
+
}
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export async function setSelectedActions(r, body) {
|
|
138
|
+
await writeJson("PUT", repoPath(r, "/actions/permissions/selected-actions"), body);
|
|
139
|
+
}
|
|
140
|
+
export async function getWorkflowPermissions(r) {
|
|
141
|
+
return readJson(repoPath(r, "/actions/permissions/workflow"));
|
|
142
|
+
}
|
|
143
|
+
export async function setWorkflowPermissions(r, body) {
|
|
144
|
+
await writeJson("PUT", repoPath(r, "/actions/permissions/workflow"), body);
|
|
145
|
+
}
|
|
146
|
+
// ── Environments ─────────────────────────────────────────────────────────────
|
|
147
|
+
/** `null` when the environment does not exist yet — reported, never created here. */
|
|
148
|
+
export async function getEnvironment(r, name) {
|
|
149
|
+
try {
|
|
150
|
+
return await readJson(repoPath(r, `/environments/${encodeURIComponent(name)}`));
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (err instanceof GhSettingsError && /\b404\b/.test(err.message))
|
|
154
|
+
return null;
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export async function getDeploymentBranchPolicies(r, name) {
|
|
159
|
+
const data = await readJson(repoPath(r, `/environments/${encodeURIComponent(name)}/deployment-branch-policies`));
|
|
160
|
+
return data.branch_policies ?? [];
|
|
161
|
+
}
|
|
162
|
+
export async function setDeploymentBranchPolicyMode(r, name, mode) {
|
|
163
|
+
await writeJson("PUT", repoPath(r, `/environments/${encodeURIComponent(name)}`), {
|
|
164
|
+
deployment_branch_policy: mode,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
export async function addDeploymentBranchPolicy(r, name, pattern) {
|
|
168
|
+
await writeJson("POST", repoPath(r, `/environments/${encodeURIComponent(name)}/deployment-branch-policies`), { name: pattern });
|
|
169
|
+
}
|
|
170
|
+
export async function deleteDeploymentBranchPolicy(r, name, policyId) {
|
|
171
|
+
await writeJson("DELETE", repoPath(r, `/environments/${encodeURIComponent(name)}/deployment-branch-policies/${policyId}`));
|
|
172
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runGithubSettings(argv: readonly string[]): Promise<number>;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `devtools github settings` — TASK-342.
|
|
3
|
+
*
|
|
4
|
+
* Prints the diff between the repository settings DevDogsUGA/DevDogsUGA
|
|
5
|
+
* must carry (secret scanning, Dependabot, Actions permissions and SHA
|
|
6
|
+
* pinning, and the three deploy environments' branch policies) and what
|
|
7
|
+
* `gh api` actually reports. Defaults to a dry-run plan; `--apply` writes
|
|
8
|
+
* FIXABLE drift, gated behind a confirmation unless `--yes`. Sibling of
|
|
9
|
+
* `../rulesets/commands.ts`, same flag shape (`--org`, `--repo`, `--apply`,
|
|
10
|
+
* `--yes`, `--json`).
|
|
11
|
+
*
|
|
12
|
+
* Never invents a required reviewer, and never creates a missing
|
|
13
|
+
* environment — both are reported only. See `../diff.ts`'s header for why.
|
|
14
|
+
*/
|
|
15
|
+
import { confirm } from "@clack/prompts";
|
|
16
|
+
import { addDeploymentBranchPolicy, deleteDeploymentBranchPolicy, getActionsPermissions, getAutomatedSecurityFixes, getDeploymentBranchPolicies, getEnvironment, getRepo, getSelectedActions, getVulnerabilityAlertsEnabled, getWorkflowPermissions, patchSecurityAndAnalysis, setActionsPermissions, setAutomatedSecurityFixesEnabled, setDeploymentBranchPolicyMode, setSelectedActions, setVulnerabilityAlertsEnabled, setWorkflowPermissions, } from "./api.js";
|
|
17
|
+
import { buildDesiredSettings } from "./desired.js";
|
|
18
|
+
import { planHasFixableChanges, planSettings, } from "./diff.js";
|
|
19
|
+
import { collectActionUses, computeActionPatterns, unpinnedActionUses } from "./workflows.js";
|
|
20
|
+
import { findRepoRoot } from "../../repo/root.js";
|
|
21
|
+
import { unwrap } from "../../ui.js";
|
|
22
|
+
const DEFAULT_ORG = "DevDogsUGA";
|
|
23
|
+
const DEFAULT_REPO = "DevDogsUGA";
|
|
24
|
+
function parseOptions(argv) {
|
|
25
|
+
const opts = {
|
|
26
|
+
org: DEFAULT_ORG,
|
|
27
|
+
repo: DEFAULT_REPO,
|
|
28
|
+
apply: false,
|
|
29
|
+
yes: false,
|
|
30
|
+
json: false,
|
|
31
|
+
};
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const arg = argv[i];
|
|
34
|
+
if (arg === "--org")
|
|
35
|
+
opts.org = argv[++i] ?? opts.org;
|
|
36
|
+
else if (arg === "--repo")
|
|
37
|
+
opts.repo = argv[++i] ?? opts.repo;
|
|
38
|
+
else if (arg === "--apply")
|
|
39
|
+
opts.apply = true;
|
|
40
|
+
else if (arg === "--yes")
|
|
41
|
+
opts.yes = true;
|
|
42
|
+
else if (arg === "--json")
|
|
43
|
+
opts.json = true;
|
|
44
|
+
}
|
|
45
|
+
return opts;
|
|
46
|
+
}
|
|
47
|
+
async function fetchSnapshot(r) {
|
|
48
|
+
const [repo, vulnerabilityAlerts, automatedSecurityFixes, actionsPermissions, selectedActions, workflowPermissions, staging, production, productionApply,] = await Promise.all([
|
|
49
|
+
getRepo(r),
|
|
50
|
+
getVulnerabilityAlertsEnabled(r),
|
|
51
|
+
getAutomatedSecurityFixes(r),
|
|
52
|
+
getActionsPermissions(r),
|
|
53
|
+
getSelectedActions(r),
|
|
54
|
+
getWorkflowPermissions(r),
|
|
55
|
+
fetchEnvironmentSnapshot(r, "staging"),
|
|
56
|
+
fetchEnvironmentSnapshot(r, "production"),
|
|
57
|
+
fetchEnvironmentSnapshot(r, "production-apply"),
|
|
58
|
+
]);
|
|
59
|
+
return {
|
|
60
|
+
repo,
|
|
61
|
+
vulnerabilityAlerts,
|
|
62
|
+
automatedSecurityFixes,
|
|
63
|
+
actionsPermissions,
|
|
64
|
+
selectedActions,
|
|
65
|
+
workflowPermissions,
|
|
66
|
+
environments: { staging, production, "production-apply": productionApply },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function fetchEnvironmentSnapshot(r, name) {
|
|
70
|
+
const environment = await getEnvironment(r, name);
|
|
71
|
+
if (!environment)
|
|
72
|
+
return { environment: null, branchPolicies: [] };
|
|
73
|
+
const branchPolicies = await getDeploymentBranchPolicies(r, name);
|
|
74
|
+
return { environment, branchPolicies };
|
|
75
|
+
}
|
|
76
|
+
function renderPlan(plan) {
|
|
77
|
+
const lines = [];
|
|
78
|
+
if (plan.unpinnedActions.length > 0) {
|
|
79
|
+
lines.push(`! ${plan.unpinnedActions.length} workflow uses: not pinned to a 40-hex SHA — sha_pinning_required is REFUSED:`);
|
|
80
|
+
for (const use of plan.unpinnedActions) {
|
|
81
|
+
lines.push(` ${use.file}:${use.line} uses: ${use.raw}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const check of plan.checks) {
|
|
85
|
+
const marker = check.status === "ok" ? "=" : check.status === "unsupported" ? "?" : "~";
|
|
86
|
+
lines.push(`${marker} ${check.key}`);
|
|
87
|
+
lines.push(` desired: ${check.desired}`);
|
|
88
|
+
lines.push(` live: ${check.live}`);
|
|
89
|
+
if (check.status === "drift" && !check.fixable) {
|
|
90
|
+
lines.push(` (drift reported only — not auto-fixable)`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (lines.length === 0)
|
|
94
|
+
return "(nothing to report)";
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
async function confirmApply(yes) {
|
|
98
|
+
if (yes)
|
|
99
|
+
return true;
|
|
100
|
+
if (!process.stdin.isTTY) {
|
|
101
|
+
process.stderr.write("devtools github settings: --yes is required to apply with no terminal available.\n");
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
return unwrap(await confirm({
|
|
105
|
+
message: "Apply this settings plan?",
|
|
106
|
+
initialValue: false,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Writes every drifted, fixable check in `plan`. Grouped by the GitHub
|
|
111
|
+
* endpoint each check's fix lands on, so two checks that share one PUT body
|
|
112
|
+
* (`actions.sha_pinning_required` and `actions.allowed_actions`, both on
|
|
113
|
+
* `PUT .../actions/permissions`) are written together, once.
|
|
114
|
+
*/
|
|
115
|
+
async function applyPlan(r, desired, live, plan) {
|
|
116
|
+
const drift = new Set(plan.checks.filter((c) => c.status === "drift" && c.fixable).map((c) => c.key));
|
|
117
|
+
const secretScanningFields = {};
|
|
118
|
+
if (drift.has("security_and_analysis.secret_scanning")) {
|
|
119
|
+
secretScanningFields.secret_scanning = {
|
|
120
|
+
status: desired.securityAndAnalysis.secretScanning ? "enabled" : "disabled",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (drift.has("security_and_analysis.secret_scanning_push_protection")) {
|
|
124
|
+
secretScanningFields.secret_scanning_push_protection = {
|
|
125
|
+
status: desired.securityAndAnalysis.secretScanningPushProtection ? "enabled" : "disabled",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (drift.has("security_and_analysis.secret_scanning_non_provider_patterns")) {
|
|
129
|
+
secretScanningFields.secret_scanning_non_provider_patterns = {
|
|
130
|
+
status: desired.securityAndAnalysis.secretScanningNonProviderPatterns
|
|
131
|
+
? "enabled"
|
|
132
|
+
: "disabled",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (drift.has("security_and_analysis.secret_scanning_validity_checks")) {
|
|
136
|
+
secretScanningFields.secret_scanning_validity_checks = {
|
|
137
|
+
status: desired.securityAndAnalysis.secretScanningValidityChecks ? "enabled" : "disabled",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (Object.keys(secretScanningFields).length > 0) {
|
|
141
|
+
await patchSecurityAndAnalysis(r, secretScanningFields);
|
|
142
|
+
}
|
|
143
|
+
if (drift.has("vulnerability_alerts")) {
|
|
144
|
+
await setVulnerabilityAlertsEnabled(r, desired.vulnerabilityAlerts);
|
|
145
|
+
}
|
|
146
|
+
if (drift.has("dependabot_security_updates")) {
|
|
147
|
+
await setAutomatedSecurityFixesEnabled(r, desired.dependabotSecurityUpdates);
|
|
148
|
+
}
|
|
149
|
+
if (drift.has("actions.sha_pinning_required") || drift.has("actions.allowed_actions")) {
|
|
150
|
+
await setActionsPermissions(r, {
|
|
151
|
+
enabled: live.actionsPermissions.enabled,
|
|
152
|
+
allowed_actions: drift.has("actions.allowed_actions")
|
|
153
|
+
? desired.actions.allowedActions
|
|
154
|
+
: live.actionsPermissions.allowed_actions,
|
|
155
|
+
// Never written true unless THIS run's own check approved it —
|
|
156
|
+
// `plan.refuseShaPinning` already kept it out of `drift` when refused,
|
|
157
|
+
// but guarded again here so a future caller building `drift` some
|
|
158
|
+
// other way cannot accidentally turn this on unsafely.
|
|
159
|
+
sha_pinning_required: drift.has("actions.sha_pinning_required")
|
|
160
|
+
? desired.actions.shaPinningRequired
|
|
161
|
+
: live.actionsPermissions.sha_pinning_required,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (drift.has("actions.selected_actions")) {
|
|
165
|
+
await setSelectedActions(r, {
|
|
166
|
+
github_owned_allowed: desired.actions.githubOwnedAllowed,
|
|
167
|
+
verified_allowed: desired.actions.verifiedAllowed,
|
|
168
|
+
patterns_allowed: [...desired.actions.patterns],
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (drift.has("actions.workflow_permissions")) {
|
|
172
|
+
await setWorkflowPermissions(r, {
|
|
173
|
+
default_workflow_permissions: desired.workflowPermissions.defaultWorkflowPermissions,
|
|
174
|
+
can_approve_pull_request_reviews: desired.workflowPermissions.canApprovePullRequestReviews,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
for (const env of desired.environments) {
|
|
178
|
+
if (!drift.has(`environments.${env.name}.branch_policy`))
|
|
179
|
+
continue;
|
|
180
|
+
await setDeploymentBranchPolicyMode(r, env.name, {
|
|
181
|
+
protected_branches: false,
|
|
182
|
+
custom_branch_policies: true,
|
|
183
|
+
});
|
|
184
|
+
const existing = await getDeploymentBranchPolicies(r, env.name);
|
|
185
|
+
const wanted = new Set(env.allowedBranches);
|
|
186
|
+
for (const policy of existing) {
|
|
187
|
+
if (!wanted.has(policy.name))
|
|
188
|
+
await deleteDeploymentBranchPolicy(r, env.name, policy.id);
|
|
189
|
+
}
|
|
190
|
+
const already = new Set(existing.map((p) => p.name));
|
|
191
|
+
for (const branch of env.allowedBranches) {
|
|
192
|
+
if (!already.has(branch))
|
|
193
|
+
await addDeploymentBranchPolicy(r, env.name, branch);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export async function runGithubSettings(argv) {
|
|
198
|
+
const opts = parseOptions(argv);
|
|
199
|
+
const r = { owner: opts.org, repo: opts.repo };
|
|
200
|
+
let uses;
|
|
201
|
+
try {
|
|
202
|
+
uses = collectActionUses(findRepoRoot());
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
process.stderr.write(`devtools github settings: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
const unpinned = unpinnedActionUses(uses);
|
|
209
|
+
const patterns = computeActionPatterns(uses);
|
|
210
|
+
const desired = buildDesiredSettings(patterns);
|
|
211
|
+
let live;
|
|
212
|
+
try {
|
|
213
|
+
live = await fetchSnapshot(r);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
process.stderr.write(`devtools github settings: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
const plan = planSettings(desired, live, unpinned);
|
|
220
|
+
if (opts.json) {
|
|
221
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
console.log(renderPlan(plan));
|
|
225
|
+
}
|
|
226
|
+
if (!opts.apply) {
|
|
227
|
+
if (!opts.json && planHasFixableChanges(plan)) {
|
|
228
|
+
console.log("\n(dry run — pass --apply to write this plan)");
|
|
229
|
+
}
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
if (!planHasFixableChanges(plan))
|
|
233
|
+
return 0;
|
|
234
|
+
if (!(await confirmApply(opts.yes)))
|
|
235
|
+
return 1;
|
|
236
|
+
try {
|
|
237
|
+
await applyPlan(r, desired, live, plan);
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
process.stderr.write(`devtools github settings: apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
console.log("Applied.");
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What TASK-342's `devtools github settings` wants DevDogsUGA/DevDogsUGA's
|
|
3
|
+
* repository settings to look like.
|
|
4
|
+
*
|
|
5
|
+
* Sibling of `../rulesets/desired.ts` — same shape of module (a pure
|
|
6
|
+
* builder, no network, so `desired.test.ts` can assert it directly) covering
|
|
7
|
+
* a different slice of repository configuration: security-and-analysis,
|
|
8
|
+
* Dependabot, Actions permissions, and the three deploy environments'
|
|
9
|
+
* branch policies. `diff.ts` compares this against what `api.ts`'s reads
|
|
10
|
+
* report; `commands.ts` is the `devtools github settings` command that
|
|
11
|
+
* prints or applies the difference.
|
|
12
|
+
*/
|
|
13
|
+
export type SettingsEnvironment = "staging" | "production" | "production-apply";
|
|
14
|
+
export interface DesiredEnvironmentPolicy {
|
|
15
|
+
name: SettingsEnvironment;
|
|
16
|
+
/** Deployment-branch-policy patterns this environment allows — exact branch names here, not glob patterns. */
|
|
17
|
+
allowedBranches: readonly string[];
|
|
18
|
+
/** Whether this environment must carry at least one required reviewer (never auto-added — see `diff.ts`). */
|
|
19
|
+
requireReviewers: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface DesiredSettings {
|
|
22
|
+
securityAndAnalysis: {
|
|
23
|
+
secretScanning: boolean;
|
|
24
|
+
secretScanningPushProtection: boolean;
|
|
25
|
+
/** Advanced-Security features GitHub grants free on PUBLIC repos; `diff.ts` treats an absent live field as "unsupported", never as drift. */
|
|
26
|
+
secretScanningNonProviderPatterns: boolean;
|
|
27
|
+
secretScanningValidityChecks: boolean;
|
|
28
|
+
};
|
|
29
|
+
vulnerabilityAlerts: boolean;
|
|
30
|
+
dependabotSecurityUpdates: boolean;
|
|
31
|
+
actions: {
|
|
32
|
+
/** Gated on every workflow `uses:` already being SHA-pinned — see `workflows.ts` and `diff.ts`'s `refuseShaPinning`. */
|
|
33
|
+
shaPinningRequired: boolean;
|
|
34
|
+
allowedActions: "selected";
|
|
35
|
+
githubOwnedAllowed: boolean;
|
|
36
|
+
verifiedAllowed: boolean;
|
|
37
|
+
/** `owner/repo@*`, computed from the repo's own workflows by `workflows.ts#computeActionPatterns`. */
|
|
38
|
+
patterns: readonly string[];
|
|
39
|
+
};
|
|
40
|
+
workflowPermissions: {
|
|
41
|
+
defaultWorkflowPermissions: "read";
|
|
42
|
+
canApprovePullRequestReviews: false;
|
|
43
|
+
};
|
|
44
|
+
environments: readonly DesiredEnvironmentPolicy[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Builds the desired settings. `actionPatterns` comes from scanning the
|
|
48
|
+
* repo's own workflow files (`workflows.ts#computeActionPatterns`) —
|
|
49
|
+
* `commands.ts` is the only real caller, and passes the live scan result
|
|
50
|
+
* rather than this module reading the filesystem itself, so `desired.test.ts`
|
|
51
|
+
* can assert the shape with a fixture list and no repo on disk.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildDesiredSettings(actionPatterns: readonly string[]): DesiredSettings;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the desired settings. `actionPatterns` comes from scanning the
|
|
3
|
+
* repo's own workflow files (`workflows.ts#computeActionPatterns`) —
|
|
4
|
+
* `commands.ts` is the only real caller, and passes the live scan result
|
|
5
|
+
* rather than this module reading the filesystem itself, so `desired.test.ts`
|
|
6
|
+
* can assert the shape with a fixture list and no repo on disk.
|
|
7
|
+
*/
|
|
8
|
+
export function buildDesiredSettings(actionPatterns) {
|
|
9
|
+
return {
|
|
10
|
+
securityAndAnalysis: {
|
|
11
|
+
secretScanning: true,
|
|
12
|
+
secretScanningPushProtection: true,
|
|
13
|
+
secretScanningNonProviderPatterns: true,
|
|
14
|
+
secretScanningValidityChecks: true,
|
|
15
|
+
},
|
|
16
|
+
vulnerabilityAlerts: true,
|
|
17
|
+
dependabotSecurityUpdates: true,
|
|
18
|
+
actions: {
|
|
19
|
+
shaPinningRequired: true,
|
|
20
|
+
allowedActions: "selected",
|
|
21
|
+
githubOwnedAllowed: true,
|
|
22
|
+
verifiedAllowed: true,
|
|
23
|
+
patterns: [...actionPatterns].sort(),
|
|
24
|
+
},
|
|
25
|
+
workflowPermissions: {
|
|
26
|
+
defaultWorkflowPermissions: "read",
|
|
27
|
+
canApprovePullRequestReviews: false,
|
|
28
|
+
},
|
|
29
|
+
environments: [
|
|
30
|
+
{ name: "staging", allowedBranches: ["main"], requireReviewers: false },
|
|
31
|
+
{ name: "production", allowedBranches: ["production"], requireReviewers: false },
|
|
32
|
+
{
|
|
33
|
+
name: "production-apply",
|
|
34
|
+
allowedBranches: ["production"],
|
|
35
|
+
requireReviewers: true,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live settings snapshot -> a plan of per-setting checks, against
|
|
3
|
+
* `desired.ts`'s desired shape.
|
|
4
|
+
*
|
|
5
|
+
* Unlike `../rulesets/diff.ts` (one shape, N instances), TASK-342's settings
|
|
6
|
+
* are N unrelated resources on one repository — security-and-analysis,
|
|
7
|
+
* Dependabot, three Actions endpoints, and three environments' branch
|
|
8
|
+
* policies. So the plan here is a flat list of named checks, each its own
|
|
9
|
+
* `ok` / `drift` / `unsupported` verdict, rather than create/update/delete
|
|
10
|
+
* buckets. `fixable` marks which drifted checks `commands.ts`'s `--apply`
|
|
11
|
+
* step is allowed to write — `false` for anything this reconciler must only
|
|
12
|
+
* ever report: a missing environment (nothing here creates one), and a
|
|
13
|
+
* missing required reviewer (nothing here invents one).
|
|
14
|
+
*/
|
|
15
|
+
import type { DesiredEnvironmentPolicy, DesiredSettings } from "./desired.js";
|
|
16
|
+
import type { LiveActionsPermissions, LiveAutomatedSecurityFixes, LiveBranchPolicy, LiveEnvironment, LiveRepo, LiveSelectedActions, LiveWorkflowPermissions } from "./types.js";
|
|
17
|
+
import type { ActionUse } from "./workflows.js";
|
|
18
|
+
export type CheckStatus = "ok" | "drift" | "unsupported";
|
|
19
|
+
export interface SettingCheck {
|
|
20
|
+
key: string;
|
|
21
|
+
description: string;
|
|
22
|
+
desired: string;
|
|
23
|
+
live: string;
|
|
24
|
+
status: CheckStatus;
|
|
25
|
+
/** Whether `commands.ts`'s apply step may write this check's fix. */
|
|
26
|
+
fixable: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface LiveEnvironmentSnapshot {
|
|
29
|
+
environment: LiveEnvironment | null;
|
|
30
|
+
branchPolicies: LiveBranchPolicy[];
|
|
31
|
+
}
|
|
32
|
+
export interface LiveSettingsSnapshot {
|
|
33
|
+
repo: LiveRepo;
|
|
34
|
+
vulnerabilityAlerts: boolean;
|
|
35
|
+
automatedSecurityFixes: LiveAutomatedSecurityFixes;
|
|
36
|
+
actionsPermissions: LiveActionsPermissions;
|
|
37
|
+
selectedActions: LiveSelectedActions;
|
|
38
|
+
workflowPermissions: LiveWorkflowPermissions;
|
|
39
|
+
environments: Record<DesiredEnvironmentPolicy["name"], LiveEnvironmentSnapshot>;
|
|
40
|
+
}
|
|
41
|
+
export interface SettingsPlan {
|
|
42
|
+
checks: SettingCheck[];
|
|
43
|
+
unpinnedActions: readonly ActionUse[];
|
|
44
|
+
/** Whether `sha_pinning_required` was refused this run because the repo's own workflows carry an unpinned `uses:`. */
|
|
45
|
+
refuseShaPinning: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Builds the plan. `unpinnedActions` comes from `workflows.ts` — passed in
|
|
49
|
+
* rather than recomputed here so this stays a pure function over data
|
|
50
|
+
* `commands.ts` already gathered (and so `diff.test.ts` can assert
|
|
51
|
+
* `refuseShaPinning` with a plain fixture array, no filesystem involved).
|
|
52
|
+
*/
|
|
53
|
+
export declare function planSettings(desired: DesiredSettings, live: LiveSettingsSnapshot, unpinnedActions: readonly ActionUse[]): SettingsPlan;
|
|
54
|
+
/** Whether a plan has any FIXABLE drift `--apply` would write. */
|
|
55
|
+
export declare function planHasFixableChanges(plan: SettingsPlan): boolean;
|
|
56
|
+
/** Whether a plan reports any drift at all, fixable or not (for exit-code / `--check` style callers). */
|
|
57
|
+
export declare function planHasChanges(plan: SettingsPlan): boolean;
|