@bragi-gmbh/codebus 1.2.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/bin/codebus.js +2 -0
- package/dist/binding-guard.js +97 -0
- package/dist/binding-guard.js.map +1 -0
- package/dist/client.js +94 -0
- package/dist/client.js.map +1 -0
- package/dist/commands/ack.js +47 -0
- package/dist/commands/ack.js.map +1 -0
- package/dist/commands/archive.js +21 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/as.js +299 -0
- package/dist/commands/as.js.map +1 -0
- package/dist/commands/ask.js +189 -0
- package/dist/commands/ask.js.map +1 -0
- package/dist/commands/auth.js +33 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/checkin.js +235 -0
- package/dist/commands/checkin.js.map +1 -0
- package/dist/commands/decision.js +52 -0
- package/dist/commands/decision.js.map +1 -0
- package/dist/commands/digest.js +117 -0
- package/dist/commands/digest.js.map +1 -0
- package/dist/commands/doctor.js +106 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/factory.js +191 -0
- package/dist/commands/factory.js.map +1 -0
- package/dist/commands/github-login.js +205 -0
- package/dist/commands/github-login.js.map +1 -0
- package/dist/commands/graph.js +88 -0
- package/dist/commands/graph.js.map +1 -0
- package/dist/commands/health.js +34 -0
- package/dist/commands/health.js.map +1 -0
- package/dist/commands/inbox.js +83 -0
- package/dist/commands/inbox.js.map +1 -0
- package/dist/commands/ingest.js +553 -0
- package/dist/commands/ingest.js.map +1 -0
- package/dist/commands/init.js +97 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/install.js +520 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/invite.js +152 -0
- package/dist/commands/invite.js.map +1 -0
- package/dist/commands/jira.js +237 -0
- package/dist/commands/jira.js.map +1 -0
- package/dist/commands/join.js +96 -0
- package/dist/commands/join.js.map +1 -0
- package/dist/commands/links.js +66 -0
- package/dist/commands/links.js.map +1 -0
- package/dist/commands/login.js +84 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/morning.js +121 -0
- package/dist/commands/morning.js.map +1 -0
- package/dist/commands/next.js +66 -0
- package/dist/commands/next.js.map +1 -0
- package/dist/commands/onboarding.js +109 -0
- package/dist/commands/onboarding.js.map +1 -0
- package/dist/commands/pack.js +276 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/plan-gh.js +425 -0
- package/dist/commands/plan-gh.js.map +1 -0
- package/dist/commands/plan.js +465 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/pr.js +88 -0
- package/dist/commands/pr.js.map +1 -0
- package/dist/commands/question.js +142 -0
- package/dist/commands/question.js.map +1 -0
- package/dist/commands/read.js +84 -0
- package/dist/commands/read.js.map +1 -0
- package/dist/commands/refs.js +357 -0
- package/dist/commands/refs.js.map +1 -0
- package/dist/commands/reply.js +38 -0
- package/dist/commands/reply.js.map +1 -0
- package/dist/commands/repo.js +1198 -0
- package/dist/commands/repo.js.map +1 -0
- package/dist/commands/report.js +41 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/resolve-refs.js +21 -0
- package/dist/commands/resolve-refs.js.map +1 -0
- package/dist/commands/resolve.js +93 -0
- package/dist/commands/resolve.js.map +1 -0
- package/dist/commands/send.js +168 -0
- package/dist/commands/send.js.map +1 -0
- package/dist/commands/sent.js +22 -0
- package/dist/commands/sent.js.map +1 -0
- package/dist/commands/session.js +426 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/statusline.js +43 -0
- package/dist/commands/statusline.js.map +1 -0
- package/dist/commands/task.js +140 -0
- package/dist/commands/task.js.map +1 -0
- package/dist/commands/tick.js +233 -0
- package/dist/commands/tick.js.map +1 -0
- package/dist/commands/upgrade.js +76 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/watch.js +169 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/commands/whoami.js +27 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/config.js +103 -0
- package/dist/config.js.map +1 -0
- package/dist/directives.js +102 -0
- package/dist/directives.js.map +1 -0
- package/dist/errors.js +110 -0
- package/dist/errors.js.map +1 -0
- package/dist/git-hooks.js +116 -0
- package/dist/git-hooks.js.map +1 -0
- package/dist/graph-bootstrap.js +95 -0
- package/dist/graph-bootstrap.js.map +1 -0
- package/dist/graph-remote.js +889 -0
- package/dist/graph-remote.js.map +1 -0
- package/dist/graph.js +335 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.js +154 -0
- package/dist/index.js.map +1 -0
- package/dist/kit/graph-publish.mjs +400 -0
- package/dist/mail-latency.js +120 -0
- package/dist/mail-latency.js.map +1 -0
- package/dist/mcp-registry.js +859 -0
- package/dist/mcp-registry.js.map +1 -0
- package/dist/mcp.js +193 -0
- package/dist/mcp.js.map +1 -0
- package/dist/output.js +12 -0
- package/dist/output.js.map +1 -0
- package/dist/positioning.js +21 -0
- package/dist/positioning.js.map +1 -0
- package/dist/rolestore.js +89 -0
- package/dist/rolestore.js.map +1 -0
- package/dist/session.js +49 -0
- package/dist/session.js.map +1 -0
- package/dist/sessionstore.js +736 -0
- package/dist/sessionstore.js.map +1 -0
- package/dist/version-check.js +97 -0
- package/dist/version-check.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,1198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* repo.ts — `codebus repo connect|status|list` (docs/plans/repo-intelligence.md
|
|
3
|
+
* Track A4).
|
|
4
|
+
*
|
|
5
|
+
* `repo connect <github-url> [--slug <s>] [--default-branch <b>] [--force]
|
|
6
|
+
* [--write-workflow] [--build] [--no-pr-sync]`
|
|
7
|
+
* (`--branch` is accepted as a deprecated alias for `--default-branch` —
|
|
8
|
+
* round-11 review finding F6: the plan's own file spec names the flag
|
|
9
|
+
* `--default-branch`, matching the RPC parameter it feeds.)
|
|
10
|
+
* Parses <github-url> (https, ssh, or bare owner/repo — always normalized
|
|
11
|
+
* to a canonical https://github.com/<owner>/<repo> URL before it reaches
|
|
12
|
+
* the RPC, since 0070's trg_repos_enforce_remote_url_normalized only
|
|
13
|
+
* accepts an https(s) scheme). The resolved default branch is validated
|
|
14
|
+
* against git's OWN ref-format rules (round-12 review finding 7,
|
|
15
|
+
* CONFIRMED) before anything else runs — see `isValidGitBranchName`.
|
|
16
|
+
* Calls `repo_connect` (0070). If the caller is not this repo's authority
|
|
17
|
+
* (workspace admin or designated curator — 0050 graph_repo_authority),
|
|
18
|
+
* prints remediation and stops WITHOUT attempting a token mint
|
|
19
|
+
* (re-runnable later once authority is granted). Otherwise mints a fresh
|
|
20
|
+
* `ingest`-scope token (graph_ingest_token_mint, 0059) — round-12 review
|
|
21
|
+
* finding 5 (CONFIRMED): minting now happens LAST among the fallible
|
|
22
|
+
* steps below (after the workflow write/validate and the ci_wired
|
|
23
|
+
* transition), immediately before the one remaining step that structurally
|
|
24
|
+
* needs the plaintext (`--build`), so a failure in an EARLIER step never
|
|
25
|
+
* wastes an unrecoverable token row; see repoConnectFull's own doc for the
|
|
26
|
+
* residual `--build`-failure case and how it is covered. `--json` mode
|
|
27
|
+
* prints the token ONLY as a field inside the single JSON object on
|
|
28
|
+
* stdout — nothing else follows it (round-12 review finding 2: this now
|
|
29
|
+
* also holds when `--build` is combined with `--json` — the child
|
|
30
|
+
* process's own stdout no longer shares this process's stdout, see
|
|
31
|
+
* repoConnectFull); human mode prints it exactly once, inline with the
|
|
32
|
+
* `gh secret set` instructions (round-11 review finding F3: plaintext
|
|
33
|
+
* token must never hit `--json` output mixed with free text; round-12
|
|
34
|
+
* review finding 6, CONFIRMED: it must also never print twice within
|
|
35
|
+
* human mode's own block — the `gh secret set` line now references the
|
|
36
|
+
* token printed just above it rather than repeating it inline).
|
|
37
|
+
* `--write-workflow` validates this cwd's own `origin` remote against the
|
|
38
|
+
* connected repo and emits `.github/workflows/codebus.yml`, with the
|
|
39
|
+
* resolved default branch single-quoted and escaped into the YAML (round-
|
|
40
|
+
* 12 review finding 7, CONFIRMED — a comma or `]` is legal in a git
|
|
41
|
+
* branch name but breaks an UNQUOTED YAML flow sequence). Round-12 review
|
|
42
|
+
* finding 1 (CONFIRMED, BLOCKER): `--write-workflow` no longer transitions
|
|
43
|
+
* the attachment to `ci_wired` by itself. Round-13 review finding 2
|
|
44
|
+
* (CONFIRMED, BLOCKER) fixed the underlying reusable workflow itself
|
|
45
|
+
* (`.github/workflows/graphify-publish.yml` now checks out CodeBus's own
|
|
46
|
+
* scripts/lockfiles into `.codebus-kit/` for a foreign caller, see that
|
|
47
|
+
* file's own comment) — the emitted workflow CAN now actually run for a
|
|
48
|
+
* foreign repo — but a file merely existing on disk still proves nothing
|
|
49
|
+
* about whether IT ACTUALLY DID (secrets not yet added to the repo's
|
|
50
|
+
* `graph-publish` environment, the environment itself not yet created, a
|
|
51
|
+
* network blip, ...), so this command still does not claim `ci_wired`
|
|
52
|
+
* just because the write succeeded. `ci_wired` must mean "CI actually
|
|
53
|
+
* ran," not "a file was written" — writing the file is necessary but
|
|
54
|
+
* never sufficient by itself. Two things now earn `ci_wired`: `--build`
|
|
55
|
+
* transitions pending -> ci_wired BEFORE running (plan's own state-machine
|
|
56
|
+
* trap: attachment must not sit `pending` while a build has already run),
|
|
57
|
+
* then actually runs `scripts/graph-publish.mjs` locally against the
|
|
58
|
+
* freshly minted token, surfacing a quarantine/25MB-reject verbatim — a
|
|
59
|
+
* real, local, end-to-end proof that ingest works; and, round-13 review
|
|
60
|
+
* finding 3 (CONFIRMED, MAJOR), a genuine CI-sourced ingest arriving while
|
|
61
|
+
* still `pending` (0063's `graph_ingest_artifact`) now itself advances
|
|
62
|
+
* pending -> ci_wired -> first_build_ok -> live in one call — equally
|
|
63
|
+
* strong proof, since it is a REAL external caller authenticating with
|
|
64
|
+
* this repo's own token and actually landing servable content. Nothing
|
|
65
|
+
* else auto-promotes OUT of `pending`, and `--write-workflow` deliberately
|
|
66
|
+
* still does not. Round-11 review finding F4 (CONFIRMED, still enforced):
|
|
67
|
+
* the transition used to run BEFORE the origin-match validation and file
|
|
68
|
+
* write, so a mismatched checkout or a filesystem failure could leave the
|
|
69
|
+
* database falsely claiming CI is wired despite no workflow file ever
|
|
70
|
+
* landing; validate-then-write still fully precedes the `--build`
|
|
71
|
+
* transition. Round-13 review finding 3 additionally moved the MINT
|
|
72
|
+
* ahead of that transition too (see `repoConnectFull`'s own doc) — minting
|
|
73
|
+
* is itself fallible, and a false `ci_wired` claim on a failed mint would
|
|
74
|
+
* be exactly the same class of bug this whole paragraph is about.
|
|
75
|
+
*
|
|
76
|
+
* `repo status <slug>` — graph_status (0050) + a PR status aggregate.
|
|
77
|
+
* `repo list` — every repo in this workspace + its attachment state.
|
|
78
|
+
*/
|
|
79
|
+
import { execFileSync, spawnSync, execFile } from "node:child_process";
|
|
80
|
+
import { promisify } from "node:util";
|
|
81
|
+
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
|
82
|
+
import { dirname, join } from "node:path";
|
|
83
|
+
import { fileURLToPath } from "node:url";
|
|
84
|
+
import { resolveConfig } from "../config.js";
|
|
85
|
+
import { makeCtx, workspaceId, repoId as resolveRepoIdBySlug } from "../client.js";
|
|
86
|
+
import { render } from "../output.js";
|
|
87
|
+
import { CodebusError, PostMintFailureError } from "../errors.js";
|
|
88
|
+
import { redactUrlUserinfo } from "./plan-gh.js";
|
|
89
|
+
const OWNER_REPO_TAIL = "([A-Za-z0-9._-]+)/([A-Za-z0-9._-]+?)(?:\\.git)?/?$";
|
|
90
|
+
/**
|
|
91
|
+
* Accepts https://github.com/<owner>/<repo>[.git], git@github.com:<owner>/
|
|
92
|
+
* <repo>[.git], ssh://git@github.com[:port]/<owner>/<repo>[.git], and a bare
|
|
93
|
+
* <owner>/<repo>. Returns null (never throws) on anything else. Always
|
|
94
|
+
* returns the canonical https form — repo_connect's trigger requires it.
|
|
95
|
+
*/
|
|
96
|
+
export function parseGithubUrl(input) {
|
|
97
|
+
const s = input.trim();
|
|
98
|
+
const patterns = [
|
|
99
|
+
new RegExp(`^https?://(?:[^/@\\s]+@)?github\\.com(?::\\d+)?/${OWNER_REPO_TAIL}`, "i"),
|
|
100
|
+
new RegExp(`^git@github\\.com:${OWNER_REPO_TAIL}`, "i"),
|
|
101
|
+
new RegExp(`^ssh://git@github\\.com(?::\\d+)?/${OWNER_REPO_TAIL}`, "i"),
|
|
102
|
+
];
|
|
103
|
+
for (const re of patterns) {
|
|
104
|
+
const m = s.match(re);
|
|
105
|
+
if (m)
|
|
106
|
+
return { owner: m[1], name: m[2], canonicalUrl: `https://github.com/${m[1]}/${m[2]}` };
|
|
107
|
+
}
|
|
108
|
+
// Bare "owner/repo" — only when the input doesn't already look like a URL
|
|
109
|
+
// or scp-style remote (no scheme separator, no '@').
|
|
110
|
+
if (!s.includes("://") && !s.includes("@")) {
|
|
111
|
+
const m = s.match(new RegExp(`^${OWNER_REPO_TAIL}`, "i"));
|
|
112
|
+
if (m)
|
|
113
|
+
return { owner: m[1], name: m[2], canonicalUrl: `https://github.com/${m[1]}/${m[2]}` };
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
/** `git remote get-url origin`, or null if there is no such remote / this
|
|
118
|
+
* isn't a git checkout. Never throws. */
|
|
119
|
+
export function getGitRemoteUrl(repoRoot) {
|
|
120
|
+
try {
|
|
121
|
+
const url = execFileSync("git", ["remote", "get-url", "origin"], { cwd: repoRoot, encoding: "utf-8" }).trim();
|
|
122
|
+
return url || null;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Round-16 review finding 1, part 2 (CONFIRMED, BLOCKER): this checkout's
|
|
130
|
+
* own git origin must match the connected repo before EITHER
|
|
131
|
+
* `--write-workflow` writes a file for it OR `--build` runs a local
|
|
132
|
+
* ingest against whatever code sits in it. The previous revision only ran
|
|
133
|
+
* this check for `--write-workflow` — `--build` alone (no
|
|
134
|
+
* `--write-workflow`) could run unchecked against ANY cwd, so an operator
|
|
135
|
+
* (or a script) that minted a token for repo A while sitting in an
|
|
136
|
+
* unrelated checkout B would locally build+ingest B's code into A's graph,
|
|
137
|
+
* using A's own credentials, with nothing catching the mismatch. Extracted
|
|
138
|
+
* so both flags run the IDENTICAL check rather than two copies that can
|
|
139
|
+
* drift; `flagName` only changes the error message's wording, never the
|
|
140
|
+
* logic.
|
|
141
|
+
*/
|
|
142
|
+
export function validateOriginMatchesConnectedRepo(repoRoot, owner, name, flagName) {
|
|
143
|
+
const originUrl = getGitRemoteUrl(repoRoot);
|
|
144
|
+
const originParsed = originUrl ? parseGithubUrl(originUrl) : null;
|
|
145
|
+
if (!originParsed || originParsed.owner.toLowerCase() !== owner.toLowerCase() || originParsed.name.toLowerCase() !== name.toLowerCase()) {
|
|
146
|
+
// Round-19 review finding (CONFIRMED, credential-leakage theme): this
|
|
147
|
+
// checkout's own 'origin' is fully local-environment-controlled, and a
|
|
148
|
+
// git remote legally embeds HTTP Basic-style userinfo
|
|
149
|
+
// (`https://token@github.com/...`) — never echo it raw.
|
|
150
|
+
throw new CodebusError(`${flagName} refused: this checkout's origin remote (${originUrl ? redactUrlUserinfo(originUrl) : "none"}) does not match the ` +
|
|
151
|
+
`connected repo (${owner}/${name}). Run this from a checkout of the connected repo.`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const execFileP = promisify(execFile);
|
|
155
|
+
const CODEBUS_ORG = "Bragi-GmbH";
|
|
156
|
+
export function realRepoVisibilityChecker() {
|
|
157
|
+
return {
|
|
158
|
+
/* c8 ignore start -- shells out to the real `gh` binary/GitHub API,
|
|
159
|
+
* same irreducible-external-I/O exemption as plan-gh.ts's
|
|
160
|
+
* realGhRunner() (DEC-CB-009 EX-001 class) — every test below injects
|
|
161
|
+
* its own mock RepoVisibilityChecker instead. */
|
|
162
|
+
async visibility(owner, name) {
|
|
163
|
+
const { stdout } = await execFileP("gh", ["repo", "view", `${owner}/${name}`, "--json", "visibility"]);
|
|
164
|
+
const parsed = JSON.parse(stdout);
|
|
165
|
+
if (!parsed.visibility)
|
|
166
|
+
throw new Error(`\`gh repo view ${owner}/${name} --json visibility\` returned no visibility field`);
|
|
167
|
+
return parsed.visibility.toLowerCase();
|
|
168
|
+
},
|
|
169
|
+
/* c8 ignore stop */
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Round-12 review finding F1 (CONFIRMED, MAJOR): the reusable workflow this
|
|
174
|
+
* CLI's own generated `codebus.yml` calls (`uses: Bragi-GmbH/CodeBus/
|
|
175
|
+
* .github/workflows/graphify-publish.yml@main`) lives in Bragi-GmbH/CodeBus
|
|
176
|
+
* — a PRIVATE repository (verified via `gh repo view Bragi-GmbH/CodeBus
|
|
177
|
+
* --json visibility` -> PRIVATE, round-14 review finding 1). GitHub's own
|
|
178
|
+
* reusable-workflow access rules (docs/actions/reference/workflows-and-
|
|
179
|
+
* actions/reusing-workflow-configurations#access-to-reusable-workflows,
|
|
180
|
+
* verified against the docs before writing this predicate) resolve the
|
|
181
|
+
* `uses:` reference BEFORE the calling workflow run's secrets — including
|
|
182
|
+
* this CLI's own emitted `CODEBUS_KIT_TOKEN` — are ever read:
|
|
183
|
+
* - A PUBLIC caller repo can NEVER resolve a private called workflow, in
|
|
184
|
+
* any configuration; no secret or token can retroactively authorize a
|
|
185
|
+
* reference GitHub already refused to resolve.
|
|
186
|
+
* - A PRIVATE or INTERNAL caller repo can resolve it only if it is owned
|
|
187
|
+
* by the SAME organization as the called repo (here: Bragi-GmbH) AND
|
|
188
|
+
* Bragi-GmbH/CodeBus's own Actions "Access" setting (Settings ->
|
|
189
|
+
* Actions -> General -> "Access", on Bragi-GmbH/CodeBus itself) is set
|
|
190
|
+
* to "Accessible from repositories in the Bragi-GmbH organization" (or
|
|
191
|
+
* broader) rather than its more restrictive default ("Not
|
|
192
|
+
* accessible"). This CLI cannot verify that second, CodeBus-side
|
|
193
|
+
* setting remotely (it requires admin access to Bragi-GmbH/CodeBus,
|
|
194
|
+
* which an arbitrary connecting repo's operator will not have) — the
|
|
195
|
+
* error text below names it explicitly so an operator whose repo DOES
|
|
196
|
+
* pass this predicate but still sees a dead workflow at push time knows
|
|
197
|
+
* exactly which CodeBus-side setting to ask an admin about, rather than
|
|
198
|
+
* assuming the CLI itself is broken.
|
|
199
|
+
*
|
|
200
|
+
* Before this fix, `--write-workflow` (and `--build`, which always follows
|
|
201
|
+
* the identical write-workflow gate in repoConnectFull) accepted ANY
|
|
202
|
+
* connected repo and silently emitted a workflow file that could never
|
|
203
|
+
* actually run for one owned outside Bragi-GmbH, or one that is public —
|
|
204
|
+
* GitHub itself would refuse the `uses:` resolution on the very first push,
|
|
205
|
+
* with no signal back to this CLI or its operator at connect time.
|
|
206
|
+
*
|
|
207
|
+
* `checker` is injectable (default: `realRepoVisibilityChecker()`) so tests
|
|
208
|
+
* never shell out to the real `gh` — matches plan-gh.ts's GhRunner
|
|
209
|
+
* injection convention.
|
|
210
|
+
*/
|
|
211
|
+
export async function assertEligibleForCodebusReusableWorkflow(owner, name, checker = realRepoVisibilityChecker()) {
|
|
212
|
+
// Round-13 review finding (CONFIRMED, MINOR): GitHub owner/org identities
|
|
213
|
+
// are case-INSENSITIVE (github.com/Bragi-GmbH and github.com/bragi-gmbh
|
|
214
|
+
// are the identical entity) — `validateOriginMatchesConnectedRepo` above
|
|
215
|
+
// already compares owner/name via `.toLowerCase()` for exactly this
|
|
216
|
+
// reason, but this check used a bare `!==`, so a perfectly valid,
|
|
217
|
+
// differently-cased reference to the SAME organization (e.g.
|
|
218
|
+
// `bragi-gmbh/repo`, or `BRAGI-GMBH/repo` — `parseGithubUrl` preserves
|
|
219
|
+
// whatever casing the caller typed, it does not itself normalize) was
|
|
220
|
+
// incorrectly rejected as "not owned by Bragi-GmbH". Fixed by comparing
|
|
221
|
+
// lowercase-normalized values, matching the origin check's own
|
|
222
|
+
// convention. `name` is never compared against anything in this
|
|
223
|
+
// function (only interpolated into messages and passed through to
|
|
224
|
+
// `checker.visibility`, which — like `gh repo view`/GitHub itself — is
|
|
225
|
+
// already case-insensitive), so no equivalent fix is needed for it.
|
|
226
|
+
if (owner.toLowerCase() !== CODEBUS_ORG.toLowerCase()) {
|
|
227
|
+
throw new CodebusError(`--write-workflow refused: '${owner}/${name}' is not owned by ${CODEBUS_ORG}. The generated workflow calls a ` +
|
|
228
|
+
`PRIVATE reusable workflow (${CODEBUS_ORG}/CodeBus/.github/workflows/graphify-publish.yml) — GitHub's own ` +
|
|
229
|
+
"reusable-workflow access rules mean a repo outside that organization can never resolve this reference at " +
|
|
230
|
+
"all, regardless of any secret this repo supplies (CODEBUS_KIT_TOKEN arrives only after GitHub would already " +
|
|
231
|
+
`have refused the resolution). Connect a repo owned by ${CODEBUS_ORG}, or omit --write-workflow/--build.`);
|
|
232
|
+
}
|
|
233
|
+
let vis;
|
|
234
|
+
try {
|
|
235
|
+
vis = await checker.visibility(owner, name);
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
throw new CodebusError(`--write-workflow refused: could not determine '${owner}/${name}'s visibility via \`gh repo view\` (required ` +
|
|
239
|
+
`to confirm it is eligible to call ${CODEBUS_ORG}/CodeBus's private reusable workflow) — ` +
|
|
240
|
+
`${e instanceof Error ? e.message : String(e)}`);
|
|
241
|
+
}
|
|
242
|
+
if (vis !== "private" && vis !== "internal") {
|
|
243
|
+
throw new CodebusError(`--write-workflow refused: '${owner}/${name}' is ${vis}, but ${CODEBUS_ORG}/CodeBus's reusable workflow ` +
|
|
244
|
+
"(graphify-publish.yml) is PRIVATE. GitHub only allows a private or internal caller repository to resolve a " +
|
|
245
|
+
"private called workflow — a public caller can never resolve it, in any configuration. Make this repository " +
|
|
246
|
+
`private or internal within ${CODEBUS_ORG}, or omit --write-workflow/--build. Even once private/internal, ` +
|
|
247
|
+
`${CODEBUS_ORG}/CodeBus's own Settings -> Actions -> General -> "Access" setting must ALSO be configured to ` +
|
|
248
|
+
`"Accessible from repositories in the ${CODEBUS_ORG} organization" (or broader) — this CLI cannot verify that ` +
|
|
249
|
+
"setting remotely; ask a CodeBus admin to confirm it if the emitted workflow still cannot resolve the " +
|
|
250
|
+
"reusable workflow after this repo is made private/internal.");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Round-16 review finding 1, part 1 (CONFIRMED, BLOCKER): `--build` used to
|
|
255
|
+
* execute `<opts.repoRoot>/scripts/graph-publish.mjs` — a path INSIDE the
|
|
256
|
+
* repo the operator just connected. For any repo other than CodeBus itself
|
|
257
|
+
* that script normally does not exist at all (the build simply fails) —
|
|
258
|
+
* but far worse, it is a path a malicious or merely-compromised connected
|
|
259
|
+
* repo fully controls: planting its OWN file there would hand it the
|
|
260
|
+
* freshly-minted ingest token plus SUPABASE_URL/SUPABASE_ANON_KEY, in full,
|
|
261
|
+
* the instant `--build` ran. Fixed: `--build` now ALWAYS resolves and runs
|
|
262
|
+
* a TRUSTED copy of `graph-publish.mjs` shipped alongside the CLI's OWN
|
|
263
|
+
* installation — never anything inside the connected repo's checkout,
|
|
264
|
+
* regardless of what that checkout contains.
|
|
265
|
+
*
|
|
266
|
+
* Resolution walks up from the CURRENTLY RUNNING module's own file
|
|
267
|
+
* location (`moduleUrl`, always this file's own `import.meta.url` at the
|
|
268
|
+
* real call site — never `opts.repoRoot`) to this CLI package's own root:
|
|
269
|
+
* exactly three levels up from `<pkg>/src/commands/repo.ts` (dev, tsx) or
|
|
270
|
+
* `<pkg>/dist/commands/repo.js` (compiled) lands at `<pkg>/` either way,
|
|
271
|
+
* since `src/` and `dist/` are each exactly one level under the package
|
|
272
|
+
* root and `commands/` is exactly one level under EACH of those — the
|
|
273
|
+
* level count is identical for both the dev-worktree layout and the
|
|
274
|
+
* installed-npm-package layout (cli/package.json's own `files: ["bin",
|
|
275
|
+
* "dist"]`). From there, the ONLY path ever trusted is
|
|
276
|
+
* `<pkg>/dist/kit/graph-publish.mjs` — the copy `npm run build` places
|
|
277
|
+
* there from this monorepo's own `scripts/graph-publish.mjs` (see
|
|
278
|
+
* cli/package.json's `build` script), which `files: ["dist"]` therefore
|
|
279
|
+
* already ships inside the published npm package too (no `files` entry
|
|
280
|
+
* change needed — the whole `dist/` tree, `kit/` included, is already
|
|
281
|
+
* covered).
|
|
282
|
+
*
|
|
283
|
+
* Deliberately NO fallback to any monorepo-relative path (e.g.
|
|
284
|
+
* `<pkg>/../scripts/...`) — that would reintroduce exactly the kind of
|
|
285
|
+
* "resolve relative to something outside the CLI's own trusted
|
|
286
|
+
* installation" hazard this fix exists to close, for a dev-convenience
|
|
287
|
+
* savings that is not worth it. A dev worktree run without `npm run build`
|
|
288
|
+
* having populated `dist/kit/` first fails loudly here instead — the
|
|
289
|
+
* correct, safe behavior; never a silent wrong-file execution.
|
|
290
|
+
*/
|
|
291
|
+
export function resolveTrustedGraphPublishScript(moduleUrl) {
|
|
292
|
+
const packageRoot = dirname(dirname(dirname(fileURLToPath(moduleUrl))));
|
|
293
|
+
const trustedPath = join(packageRoot, "dist", "kit", "graph-publish.mjs");
|
|
294
|
+
if (!existsSync(trustedPath)) {
|
|
295
|
+
throw new CodebusError(`--build cannot find its trusted graph-publish.mjs at ${trustedPath} — this CLI installation looks ` +
|
|
296
|
+
"incomplete (run `npm run build` in the CodeBus monorepo, or reinstall the @bragi-gmbh/codebus package). " +
|
|
297
|
+
"Refusing to fall back to any script inside the connected repo's own checkout.");
|
|
298
|
+
}
|
|
299
|
+
return trustedPath;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Round-12 review finding 7 (CONFIRMED): the previous revision interpolated
|
|
303
|
+
* `p_default_branch` straight into the emitted workflow's flow-style YAML
|
|
304
|
+
* (`branches: [${branch}]`) with no quoting and no validation. Two DISTINCT
|
|
305
|
+
* problems, fixed independently, neither one a substitute for the other:
|
|
306
|
+
*
|
|
307
|
+
* 1. Git's own ref-format rules are far more permissive than the set of
|
|
308
|
+
* characters that are safe to drop unquoted into a YAML flow sequence
|
|
309
|
+
* — a comma (`release,prod`) or a `]` are both perfectly legal git
|
|
310
|
+
* branch names, yet either one changes the meaning (or breaks the
|
|
311
|
+
* parse) of an unquoted `[...]` list. `renderCodebusWorkflow` fixes
|
|
312
|
+
* this half by always emitting the branch as a single-quoted YAML
|
|
313
|
+
* scalar with embedded `'` doubled — the one escaping rule single-
|
|
314
|
+
* quoted YAML scalars need — regardless of what this function decides.
|
|
315
|
+
* 2. Separately, a caller can still pass a string that is not a legal git
|
|
316
|
+
* branch name AT ALL (stray whitespace, a leading `-`, a control
|
|
317
|
+
* character, `..`, a leading `/`, etc.) — quoting alone would make that
|
|
318
|
+
* syntactically safe YAML but semantically useless (a push trigger
|
|
319
|
+
* naming a branch nothing can ever push to). This function rejects
|
|
320
|
+
* those outright, BEFORE `repo_connect` ever writes `p_default_branch`
|
|
321
|
+
* to the database or `renderCodebusWorkflow` ever emits it.
|
|
322
|
+
*
|
|
323
|
+
* Delegates the actual rule set to git's own `check-ref-format` rather than
|
|
324
|
+
* re-deriving it by regex (git's rules — no ASCII control chars/space/
|
|
325
|
+
* `~^:?*[`, no leading/trailing/doubled `/`, no leading `.` component, no
|
|
326
|
+
* trailing `.lock`, no `@{`, not bare `@`, no `\`, no trailing `.`, no `..`
|
|
327
|
+
* — are exactly the kind of detail a hand-rolled pattern drifts out of sync
|
|
328
|
+
* with over time). Deliberately validates `refs/heads/<name>` rather than
|
|
329
|
+
* using `check-ref-format --branch <name>` directly: `--branch` mode ALSO
|
|
330
|
+
* expands shorthand like a bare `@` or `@{-1}` (git's "previous branch"
|
|
331
|
+
* syntax) into a different, expanded ref — useful for a human typing at a
|
|
332
|
+
* shell, actively wrong here, where the input is expected to be an exact,
|
|
333
|
+
* literal branch name and any shorthand-expansion behavior would validate
|
|
334
|
+
* (and silently rewrite the meaning of) a string this function should
|
|
335
|
+
* simply accept-or-reject as-is.
|
|
336
|
+
*/
|
|
337
|
+
export function isValidGitBranchName(name) {
|
|
338
|
+
if (!name)
|
|
339
|
+
return false;
|
|
340
|
+
try {
|
|
341
|
+
execFileSync("git", ["check-ref-format", `refs/heads/${name}`], { stdio: "pipe" });
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Round-9 review finding (CONFIRMED, MINOR): `isValidGitBranchName` only
|
|
350
|
+
* enforces git's OWN ref-format rules, which are far more permissive than
|
|
351
|
+
* what GitHub Actions treats as safe once that same string is dropped into
|
|
352
|
+
* a `branches:` push filter or interpolated into a workflow file GitHub
|
|
353
|
+
* itself parses and evaluates. `yamlSingleQuoted` (below) protects the YAML
|
|
354
|
+
* *parse* — it does not, and cannot, protect against GitHub's OWN
|
|
355
|
+
* downstream interpretation of the parsed value. Two concrete failure
|
|
356
|
+
* modes, both using perfectly legal git branch names `isValidGitBranchName`
|
|
357
|
+
* happily accepts:
|
|
358
|
+
*
|
|
359
|
+
* 1. A branch named `!release` (or anything else starting with `!`) is
|
|
360
|
+
* valid to git, but GitHub's `on.push.branches` filter treats a
|
|
361
|
+
* leading `!` as a NEGATION glob, not a literal branch name — the
|
|
362
|
+
* emitted workflow would simply never trigger for that branch,
|
|
363
|
+
* silently, with no error anywhere.
|
|
364
|
+
* 2. A branch named `${{github.actor}}` is also valid to git
|
|
365
|
+
* (`check-ref-format` does not forbid `$`, `{`, or `}`), but GitHub
|
|
366
|
+
* evaluates `${{ ... }}` as an expression wherever it appears in a
|
|
367
|
+
* workflow file — regardless of YAML quoting, since quoting only
|
|
368
|
+
* determines the parsed STRING value, and GitHub's expression
|
|
369
|
+
* evaluator runs on that value afterward. This is an expression
|
|
370
|
+
* injection into the emitted CI file itself.
|
|
371
|
+
*
|
|
372
|
+
* Such default-branch names are vanishingly rare in practice (git's own
|
|
373
|
+
* rules already reject the far more common `*`, `?`, and `[` for unrelated
|
|
374
|
+
* reasons — see `isValidGitBranchName`'s doc above), so this rejects
|
|
375
|
+
* outright with a clear, specific error naming the offending character
|
|
376
|
+
* rather than attempting to escape GitHub's own branch-filter glob syntax,
|
|
377
|
+
* which GitHub documents inconsistently across `branches:`/
|
|
378
|
+
* `branches-ignore:`/`paths:` filters. Rejection is a strictly more honest
|
|
379
|
+
* contract than escaping semantics this codebase would then be on the hook
|
|
380
|
+
* to keep exactly right forever.
|
|
381
|
+
*
|
|
382
|
+
* Round-14 review finding (CONFIRMED, MINOR): `+` was missing from this
|
|
383
|
+
* list — `release+prod` is a perfectly legal git branch name
|
|
384
|
+
* (`check-ref-format` does not forbid `+`) but GitHub's branch/tag/path
|
|
385
|
+
* filter glob syntax treats `+` as a special pattern character exactly
|
|
386
|
+
* like `*`/`?`/`[]`/`!`, so the emitted workflow would silently never
|
|
387
|
+
* trigger for that branch — the identical failure mode as the `!release`
|
|
388
|
+
* case above, just via a different character. Cross-checked GitHub's own
|
|
389
|
+
* enumeration of branch-filter special characters again against this
|
|
390
|
+
* list (docs/actions/reference/workflows-and-actions/workflow-syntax's
|
|
391
|
+
* filter pattern cheat sheet, and the on.push.branches section, both
|
|
392
|
+
* verified via direct fetch and independent search before this fix): the
|
|
393
|
+
* complete documented set is `* ? + [ ] !` plus `\` for escaping any of
|
|
394
|
+
* them literally. Accounting for every one of those against this list:
|
|
395
|
+
* - `*`, `?`, `!`, `[`, `]` — already present above.
|
|
396
|
+
* - `+` — was missing; added by this fix.
|
|
397
|
+
* - `]` alone is git-legal (only `[` is git-banned) and was already
|
|
398
|
+
* covered before this fix.
|
|
399
|
+
* - `\` (backslash) needs NO entry here: git's OWN ref-format rules
|
|
400
|
+
* (`check-ref-format`, enforced by `isValidGitBranchName` BEFORE this
|
|
401
|
+
* function ever runs — see `repoConnectCore`) already reject any raw
|
|
402
|
+
* backslash outright (verified: `git check-ref-format
|
|
403
|
+
* refs/heads/back\slash` rejects), so a branch name reaching this
|
|
404
|
+
* function can never contain one in the first place — nothing here
|
|
405
|
+
* would ever see it.
|
|
406
|
+
* `{`/`}` are kept even though GitHub's branch-glob cheat sheet itself
|
|
407
|
+
* does not list them as glob metacharacters — they exist for the
|
|
408
|
+
* SEPARATE `${{...}}` expression-injection concern above, not the glob
|
|
409
|
+
* syntax, and the explicit `${{` substring check just below already
|
|
410
|
+
* covers that case more precisely than a bare `{`/`}` would.
|
|
411
|
+
*/
|
|
412
|
+
const GITHUB_ACTIONS_UNSAFE_BRANCH_CHARS = ["!", "*", "?", "+", "[", "]", "{", "}"];
|
|
413
|
+
/** Returns the first GitHub-Actions-unsafe character (or the `${{`
|
|
414
|
+
* expression-start sequence) found in `name`, or `null` if none is
|
|
415
|
+
* present. Checked SEPARATELY from — and in addition to —
|
|
416
|
+
* `isValidGitBranchName`: a name can pass git's own ref-format rules and
|
|
417
|
+
* still be unsafe to drop into a `branches:` filter or workflow
|
|
418
|
+
* expression. */
|
|
419
|
+
export function findGithubActionsUnsafeBranchChar(name) {
|
|
420
|
+
// Checked before the single-character scan below: `${{` on its own does
|
|
421
|
+
// not contain any character this function would otherwise flag (`$` is
|
|
422
|
+
// not in GITHUB_ACTIONS_UNSAFE_BRANCH_CHARS), yet it is the exact
|
|
423
|
+
// substring GitHub's workflow parser treats as the start of an
|
|
424
|
+
// expression — naming it specifically produces a far clearer error than
|
|
425
|
+
// reporting a bare `{`.
|
|
426
|
+
if (name.includes("${{"))
|
|
427
|
+
return "${{";
|
|
428
|
+
for (const ch of GITHUB_ACTIONS_UNSAFE_BRANCH_CHARS) {
|
|
429
|
+
if (name.includes(ch))
|
|
430
|
+
return ch;
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
/** Emits `s` as a single-quoted YAML scalar — the one escaping rule
|
|
435
|
+
* single-quoted YAML needs (embedded `'` doubled to `''`). Used for the
|
|
436
|
+
* branch name in `renderCodebusWorkflow` (round-12 review finding 7): a
|
|
437
|
+
* git branch name may legally contain a comma, `]`, or `'`, any of which
|
|
438
|
+
* would corrupt an unquoted flow-sequence `[...]` entry. */
|
|
439
|
+
function yamlSingleQuoted(s) {
|
|
440
|
+
return `'${s.replace(/'/g, "''")}'`;
|
|
441
|
+
}
|
|
442
|
+
/** The workflow content emitted by --write-workflow. Exported for a
|
|
443
|
+
* snapshot test. `pr-sync` calls B2's own reusable workflow
|
|
444
|
+
* (.github/workflows/codebus-pr-sync.yml, this repo,
|
|
445
|
+
* 0072_github_ingestion.sql) — B2 has now landed, so this job is emitted
|
|
446
|
+
* LIVE, per the plan's own unified-caller-workflow design (one caller
|
|
447
|
+
* workflow file per connected repo, one job per active track, never a
|
|
448
|
+
* second commented-out revision waiting on a later chunk).
|
|
449
|
+
*
|
|
450
|
+
* Round-11 review finding F2 (repo review a4-r1, BLOCKER, CONFIRMED,
|
|
451
|
+
* A4-era): a LIVE `pull_request:` trigger with no push-only guard on the
|
|
452
|
+
* `graphify-publish` job would have let a pull_request-triggered run
|
|
453
|
+
* invoke graph-publish's reusable workflow, handing PR-controlled code the
|
|
454
|
+
* ingest-scope token secret. `graphify-publish` has carried its own
|
|
455
|
+
* explicit `if: github.event_name == 'push'` guard as defense-in-depth
|
|
456
|
+
* ever since — kept unchanged now that `pull_request`/`pull_request_review`
|
|
457
|
+
* are themselves live triggers too, for the identical reason: a future
|
|
458
|
+
* edit to `on:` must never silently widen which events graphify-publish
|
|
459
|
+
* runs on merely by adding a new trigger alongside it. `pr-sync` (below)
|
|
460
|
+
* carries the mirror-image guard, `github.event_name == 'pull_request' ||
|
|
461
|
+
* github.event_name == 'pull_request_review'`, so the two jobs can never
|
|
462
|
+
* cross-fire onto each other's event either. */
|
|
463
|
+
export function renderCodebusWorkflow(opts) {
|
|
464
|
+
// Round-11 review finding F6 (CONFIRMED): the previous revision always
|
|
465
|
+
// emitted `branches: [main]` here regardless of what default_branch this
|
|
466
|
+
// repo was actually connected with — a repo using another default branch
|
|
467
|
+
// recorded that branch in the database but received CI that could never
|
|
468
|
+
// run (no push to `main` ever happens there). The push trigger now uses
|
|
469
|
+
// the SAME resolved default branch repo_connect itself was called with.
|
|
470
|
+
// Round-12 review finding 7 (CONFIRMED): always emitted as a single-quoted
|
|
471
|
+
// YAML scalar (never bare inside the flow sequence) — see
|
|
472
|
+
// `yamlSingleQuoted`'s own doc for why quoting alone (not merely the
|
|
473
|
+
// caller-side `isValidGitBranchName` check) is required here.
|
|
474
|
+
const branch = yamlSingleQuoted(opts.defaultBranch ?? "main");
|
|
475
|
+
// B2 landed: `includePrSyncStub` (the constructor parameter name is kept
|
|
476
|
+
// for call-site compatibility — every existing call site still passes
|
|
477
|
+
// `includePrSyncStub: opts.prSync !== false`, cli/src/commands/repo.ts's
|
|
478
|
+
// repoConnectFull — see below) now means "emit the pr-sync job LIVE",
|
|
479
|
+
// never "emit a commented-out stub". `--no-pr-sync` still fully omits
|
|
480
|
+
// both the trigger and the job, unchanged.
|
|
481
|
+
//
|
|
482
|
+
// Round-2 (Sol) review finding F4 (CONFIRMED, MAJOR): the original type
|
|
483
|
+
// list (opened/synchronize/reopened/closed/edited) omitted every event
|
|
484
|
+
// that changes reviewer presence or draft state —
|
|
485
|
+
// `review_requested`/`review_request_removed` (the ONLY events GitHub
|
|
486
|
+
// fires when a PR's requested-reviewers set changes at all) and
|
|
487
|
+
// `converted_to_draft`/`ready_for_review` (the draft flag's own
|
|
488
|
+
// transitions). Without these, `requested_reviewers_present`,
|
|
489
|
+
// `review_state`, and the derived `draft`/PR `status` (0064) never get a
|
|
490
|
+
// CI-push sync when ONLY one of these things changes with no other edit.
|
|
491
|
+
// github-sync's own reconcile poll now ALSO covers requested-reviewer
|
|
492
|
+
// presence (github-sync/logic.mjs's mapPullToPollPayload maps the
|
|
493
|
+
// pulls-list endpoint's `requested_reviewers`/`requested_teams`) AND, for
|
|
494
|
+
// a fork-origin PR specifically, `review_decision` too (PR review, Luna,
|
|
495
|
+
// comment 3839437819, F2 — github-sync/index.ts spends one extra,
|
|
496
|
+
// budget-bounded `/pulls/{number}/reviews` call per fork PR to derive
|
|
497
|
+
// it, since that class never gets a CI-push sync at all — see the
|
|
498
|
+
// fork-origin `if:` guard below). For a SAME-repo PR, these CI-only
|
|
499
|
+
// trigger types remain the richer, more timely path for `review_decision`
|
|
500
|
+
// — the poll only spends its bounded reviews-fetch budget on fork PRs,
|
|
501
|
+
// never on one already covered by CI-push. Added below — this is not
|
|
502
|
+
// "every possible pull_request type" (labeled/assigned/milestoned/etc.
|
|
503
|
+
// map to no CodeBus PR field and are deliberately still omitted), it is
|
|
504
|
+
// every type that changes a field pr_upsert_synced (0064) actually
|
|
505
|
+
// tracks.
|
|
506
|
+
const pullRequestTrigger = opts.includePrSyncStub
|
|
507
|
+
? " pull_request:\n" +
|
|
508
|
+
" types: [opened, synchronize, reopened, closed, edited, ready_for_review, converted_to_draft, review_requested, review_request_removed]\n" +
|
|
509
|
+
" pull_request_review:\n" +
|
|
510
|
+
" types: [submitted, edited, dismissed]\n"
|
|
511
|
+
: "";
|
|
512
|
+
const graphify = `# codebus: emitted by \`codebus repo connect --write-workflow\`.
|
|
513
|
+
#
|
|
514
|
+
# Round-12 review finding F1 (CONFIRMED, MAJOR): the graphify-publish job
|
|
515
|
+
# below calls a REUSABLE workflow that lives in Bragi-GmbH/CodeBus, a
|
|
516
|
+
# PRIVATE repository. GitHub's own reusable-workflow access rules mean this
|
|
517
|
+
# file can only ever run if THIS repo is (a) owned by Bragi-GmbH and (b)
|
|
518
|
+
# itself private or internal — \`codebus repo connect --write-workflow\`
|
|
519
|
+
# already checked both of those before emitting this file, so if you are
|
|
520
|
+
# reading this file inside such a repo, this half already holds. The
|
|
521
|
+
# remaining half is NOT something the CLI can verify remotely: Bragi-GmbH/
|
|
522
|
+
# CodeBus's own Settings -> Actions -> General -> "Access" setting must
|
|
523
|
+
# ALSO be configured to "Accessible from repositories in the Bragi-GmbH
|
|
524
|
+
# organization" (or broader) — ask a CodeBus admin to confirm this if
|
|
525
|
+
# graphify-publish still fails to resolve on the first real push.
|
|
526
|
+
name: codebus
|
|
527
|
+
|
|
528
|
+
on:
|
|
529
|
+
push:
|
|
530
|
+
branches: [${branch}]
|
|
531
|
+
${pullRequestTrigger}
|
|
532
|
+
permissions:
|
|
533
|
+
contents: read
|
|
534
|
+
|
|
535
|
+
jobs:
|
|
536
|
+
graphify-publish:
|
|
537
|
+
# Defense-in-depth (round-11 review finding F2): now that pull_request/
|
|
538
|
+
# pull_request_review are LIVE triggers alongside push (B2 landed),
|
|
539
|
+
# this guard is what actually keeps graphify-publish (and its
|
|
540
|
+
# ingest-token secret) from ever running on a PR-controlled event —
|
|
541
|
+
# never merely "always true because nothing else triggers this
|
|
542
|
+
# workflow" the way it was before B2's own triggers existed.
|
|
543
|
+
if: github.event_name == 'push'
|
|
544
|
+
# Cross-repo reusable-workflow reference, pinned to a ref. Round-13
|
|
545
|
+
# review finding 2 (CONFIRMED, BLOCKER, fixed): this job used to fail at
|
|
546
|
+
# \`npm ci\` for any repo other than CodeBus itself, since the graphify
|
|
547
|
+
# script/lockfile only existed in the CodeBus repo — the reusable
|
|
548
|
+
# workflow (.github/workflows/graphify-publish.yml, CodeBus side) now
|
|
549
|
+
# checks its OWN scripts/lockfiles out into \`.codebus-kit/\` for any
|
|
550
|
+
# caller, so this job can actually run once the operator has also added
|
|
551
|
+
# the required secrets to this repo's \`graph-publish\` environment
|
|
552
|
+
# (docs/plans/repo-intelligence.md A4, "Open questions" — RESOLVED,
|
|
553
|
+
# round-19 PR #55 review: the reusable workflow's own second checkout
|
|
554
|
+
# stays pinned to \`main\`, matching THIS \`uses:\` line just below —
|
|
555
|
+
# this CLI always emits \`@main\` here, so a caller already trusts
|
|
556
|
+
# CodeBus's \`main\` wholesale; SHA-pinning only the inner checkout would
|
|
557
|
+
# protect nothing while adding a silent-staleness hazard. See that
|
|
558
|
+
# checkout step's own comment in graphify-publish.yml for the full
|
|
559
|
+
# argument).
|
|
560
|
+
# Round-14 review finding 1 (CONFIRMED, BLOCKER): Bragi-GmbH/CodeBus is
|
|
561
|
+
# PRIVATE — the reusable workflow's own \`github.token\` cannot check it
|
|
562
|
+
# out for any caller other than CodeBus itself, so THIS (foreign)
|
|
563
|
+
# caller must supply CODEBUS_KIT_TOKEN below (a GitHub PAT/App token
|
|
564
|
+
# with read access to Bragi-GmbH/CodeBus) or the reusable workflow's
|
|
565
|
+
# own fail-fast check refuses with a clear, actionable error naming
|
|
566
|
+
# exactly this secret.
|
|
567
|
+
uses: Bragi-GmbH/CodeBus/.github/workflows/graphify-publish.yml@main
|
|
568
|
+
# Round-8 review finding (CONFIRMED, MAJOR): the reusable workflow's own
|
|
569
|
+
# branch gate is driven by this \`default-branch\` input, not a hardcoded
|
|
570
|
+
# \`main\` — pass the SAME resolved default branch this repo was
|
|
571
|
+
# connected with (the identical value used for \`on: push: branches:\`
|
|
572
|
+
# above) so a repo whose default branch isn't \`main\` can actually
|
|
573
|
+
# publish, instead of the reusable workflow silently no-op'ing forever.
|
|
574
|
+
with:
|
|
575
|
+
default-branch: ${branch}
|
|
576
|
+
secrets:
|
|
577
|
+
SUPABASE_URL: \${{ secrets.SUPABASE_URL }}
|
|
578
|
+
SUPABASE_ANON_KEY: \${{ secrets.SUPABASE_ANON_KEY }}
|
|
579
|
+
GRAPH_INGEST_TOKEN: \${{ secrets.GRAPH_INGEST_TOKEN }}
|
|
580
|
+
ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
|
|
581
|
+
CODEBUS_KIT_TOKEN: \${{ secrets.CODEBUS_KIT_TOKEN }}
|
|
582
|
+
`;
|
|
583
|
+
const prSyncJob = opts.includePrSyncStub
|
|
584
|
+
? `
|
|
585
|
+
pr-sync:
|
|
586
|
+
# Mirror-image guard of graphify-publish's own \`if\` above — this job
|
|
587
|
+
# must never fire on a push event either, even though only pull_request/
|
|
588
|
+
# pull_request_review currently trigger it. The second clause skips a
|
|
589
|
+
# fork-origin PR specifically: GitHub never grants \`id-token: write\` to
|
|
590
|
+
# a pull_request run whose head lives in a fork — a documented platform
|
|
591
|
+
# restriction, not something this job's own permissions block can
|
|
592
|
+
# override — so left ungated, every fork PR would reach codebus-pr-
|
|
593
|
+
# sync.yml's OIDC-minting step and fail it red permanently (never
|
|
594
|
+
# resolves by retrying, looks identical to a real incident). Skipping
|
|
595
|
+
# is safe for what the reconcile poller (github-sync) actually covers:
|
|
596
|
+
# it independently re-derives every PR's title/branches/head sha/draft/
|
|
597
|
+
# open-closed-merged state AND requested-reviewer presence, fork-origin
|
|
598
|
+
# included, within 15 minutes (github-sync/logic.mjs's
|
|
599
|
+
# mapPullToPollPayload). review_decision (approved/changes_requested)
|
|
600
|
+
# is also covered for exactly this class now (PR review, Luna,
|
|
601
|
+
# comment 3839437819, F2): the pulls-list endpoint itself can't return
|
|
602
|
+
# it, but for a PR the poll identifies as fork-origin, github-sync/
|
|
603
|
+
# index.ts spends one extra, budget-bounded \`/pulls/{number}/reviews\`
|
|
604
|
+
# call to derive it — a same-repo PR never spends that budget, since
|
|
605
|
+
# its CI-push sync (pull_request_review events, reaching this job
|
|
606
|
+
# normally) is the richer, more timely source already. The one
|
|
607
|
+
# remaining honest gap: that per-sweep budget is finite, so a
|
|
608
|
+
# fork-origin PR's review_decision update can lag by a poll cycle or
|
|
609
|
+
# two under heavy load rather than update instantly — never a
|
|
610
|
+
# permanent zero the way it used to be.
|
|
611
|
+
if: |
|
|
612
|
+
(github.event_name == 'pull_request' || github.event_name == 'pull_request_review') &&
|
|
613
|
+
github.event.pull_request.head.repo.full_name == github.repository
|
|
614
|
+
# id-token: write at the JOB level (not just the workflow-level
|
|
615
|
+
# \`permissions:\` above, which deliberately stays contents:read-only for
|
|
616
|
+
# graphify-publish's sake) is what actually lets codebus-pr-sync.yml's
|
|
617
|
+
# own OIDC-token-minting step succeed once this job calls it via
|
|
618
|
+
# \`uses:\` — a reusable-workflow-calling job only passes down the
|
|
619
|
+
# permissions IT ITSELF declares, not merely whatever the calling
|
|
620
|
+
# workflow's top-level \`permissions:\` block happens to list.
|
|
621
|
+
# pull-requests: read is equally load-bearing (proven live by the
|
|
622
|
+
# CodeBus dogfood caller's own first run, PR #67): the reusable
|
|
623
|
+
# workflow's \`gh pr view\` reads PR metadata with THIS caller's token,
|
|
624
|
+
# and caller permissions can never be elevated by the callee — without
|
|
625
|
+
# it the job dies with "Resource not accessible by integration".
|
|
626
|
+
permissions:
|
|
627
|
+
contents: read
|
|
628
|
+
id-token: write
|
|
629
|
+
pull-requests: read
|
|
630
|
+
checks: read
|
|
631
|
+
statuses: read
|
|
632
|
+
actions: read
|
|
633
|
+
# Cross-repo reusable-workflow reference, pinned to a ref — same \`uses:\`
|
|
634
|
+
# shape as graphify-publish above. No secrets at all (unlike
|
|
635
|
+
# graphify-publish's GRAPH_INGEST_TOKEN): this job's ENTIRE credential is
|
|
636
|
+
# the OIDC token it mints for itself inside codebus-pr-sync.yml — see
|
|
637
|
+
# that file's own header for the full auth model. B2's own "Blast
|
|
638
|
+
# radius" note (docs/plans/repo-intelligence.md) is explicit that OIDC
|
|
639
|
+
# tokens are minted per-request and never stored, which is exactly why
|
|
640
|
+
# this job needs no secrets block.
|
|
641
|
+
uses: Bragi-GmbH/CodeBus/.github/workflows/codebus-pr-sync.yml@main
|
|
642
|
+
`
|
|
643
|
+
: "";
|
|
644
|
+
return graphify + prSyncJob;
|
|
645
|
+
}
|
|
646
|
+
export async function repoConnectCore(ctx, url, opts) {
|
|
647
|
+
const parsed = parseGithubUrl(url);
|
|
648
|
+
if (!parsed) {
|
|
649
|
+
// Round-19 review finding (CONFIRMED, credential-leakage theme — Luna
|
|
650
|
+
// PR #55 review, comment 3826839028): `url` is the caller-supplied
|
|
651
|
+
// argument to `codebus repo connect <url>` — parseGithubUrl's own
|
|
652
|
+
// accepted forms include userinfo (`https://token@github.com/...`,
|
|
653
|
+
// one of its own documented https-form matches), so a URL that fails
|
|
654
|
+
// to parse for some OTHER reason (a malformed owner/repo tail, say)
|
|
655
|
+
// still had that credential echoed back here verbatim.
|
|
656
|
+
throw new CodebusError(`could not parse a GitHub owner/repo from '${redactUrlUserinfo(url)}' — expected an https://github.com/<owner>/<repo> URL, ` +
|
|
657
|
+
"a git@github.com:<owner>/<repo>.git SSH URL, or a bare <owner>/<repo>.");
|
|
658
|
+
}
|
|
659
|
+
// Round-15 review finding (CONFIRMED, MINOR): named `requestedSlug` (not
|
|
660
|
+
// `slug`) deliberately — this is only what the CALLER asked to connect
|
|
661
|
+
// as. repo_connect (0070) can resolve it, via the retired_into chain or
|
|
662
|
+
// the normalized-url fallback (round-11 review finding F5), onto a row
|
|
663
|
+
// whose OWN current slug differs (the real collapse script renames a
|
|
664
|
+
// retired loser's slug and frees the original name). The RPC's own
|
|
665
|
+
// returned `slug` field (spread from `result` below, never shadowed by
|
|
666
|
+
// this variable) is the CANONICAL slug the CLI must actually surface —
|
|
667
|
+
// `repo status` only ever resolves that one, never a stale request.
|
|
668
|
+
const requestedSlug = opts.slug ?? parsed.name;
|
|
669
|
+
const branch = opts.branch ?? "main";
|
|
670
|
+
// Round-12 review finding 7 (CONFIRMED): validated BEFORE anything is
|
|
671
|
+
// written — a syntactically illegal branch name must never reach
|
|
672
|
+
// `repos.default_branch` (repo_connect's own p_default_branch) even when
|
|
673
|
+
// no workflow is ever emitted for it.
|
|
674
|
+
if (!isValidGitBranchName(branch)) {
|
|
675
|
+
throw new CodebusError(`'${branch}' is not a legal git branch name (checked via 'git check-ref-format') — refusing to connect ` +
|
|
676
|
+
"with an unusable default branch.");
|
|
677
|
+
}
|
|
678
|
+
// Round-9 review finding (CONFIRMED, MINOR): a name can pass git's own
|
|
679
|
+
// ref-format check above and STILL be unsafe once dropped into the
|
|
680
|
+
// emitted workflow's `branches:` filter or interpolated as a raw
|
|
681
|
+
// expression by GitHub itself — see `findGithubActionsUnsafeBranchChar`'s
|
|
682
|
+
// own doc for the two concrete failure modes (`!release` silently never
|
|
683
|
+
// triggers; `${{github.actor}}` is expression-injected). Checked
|
|
684
|
+
// separately, before `repo_connect` ever writes `p_default_branch`.
|
|
685
|
+
const unsafeChar = findGithubActionsUnsafeBranchChar(branch);
|
|
686
|
+
if (unsafeChar) {
|
|
687
|
+
throw new CodebusError(`'${branch}' is a legal git branch name but contains '${unsafeChar}', which GitHub Actions treats specially ` +
|
|
688
|
+
"in a push-trigger branch filter or as the start of a workflow expression (not as a literal character) — " +
|
|
689
|
+
"refusing to connect with a default branch that would silently corrupt or hijack the emitted CI workflow.");
|
|
690
|
+
}
|
|
691
|
+
const ws = await workspaceId(ctx);
|
|
692
|
+
const { data, error } = await ctx.supabase.rpc("repo_connect", {
|
|
693
|
+
p_workspace: ws,
|
|
694
|
+
p_slug: requestedSlug,
|
|
695
|
+
p_remote_url: parsed.canonicalUrl,
|
|
696
|
+
p_default_branch: branch,
|
|
697
|
+
p_force: !!opts.force,
|
|
698
|
+
});
|
|
699
|
+
if (error)
|
|
700
|
+
throw new CodebusError(error.message);
|
|
701
|
+
const result = data;
|
|
702
|
+
// `...result` carries the RPC's own canonical `slug` — deliberately NOT
|
|
703
|
+
// overridden by `requestedSlug` here (see this function's own doc above).
|
|
704
|
+
return { ...result, canonicalUrl: parsed.canonicalUrl, owner: parsed.owner, name: parsed.name, requestedSlug };
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Full `repo connect` side effects: the repo_connect RPC, `--write-workflow`,
|
|
708
|
+
* the pending -> ci_wired transition, the ingest-token mint (authority
|
|
709
|
+
* only), and `--build` — in that order. Exported for unit testing (round-11
|
|
710
|
+
* review findings F3/F4/F6 and round-12 review findings 1/2/5 regression
|
|
711
|
+
* coverage) and reuse.
|
|
712
|
+
*
|
|
713
|
+
* Deliberately returns a plain result object and does NOT print the
|
|
714
|
+
* connect/token/transition/workflow summary itself — `connectAction` below
|
|
715
|
+
* is a thin CLI wrapper that does ALL of that printing, exactly once, after
|
|
716
|
+
* this function has fully resolved. That split is what round-11 review
|
|
717
|
+
* finding F3 requires: in `--json` mode the caller must be able to print
|
|
718
|
+
* the finished result as a single JSON object with nothing before or after
|
|
719
|
+
* it, which is only possible if transitioning/writing/minting/building have
|
|
720
|
+
* already happened by the time anything is printed.
|
|
721
|
+
*
|
|
722
|
+
* Round-11 review finding F4 (CONFIRMED, still enforced): the previous
|
|
723
|
+
* revision transitioned pending -> ci_wired BEFORE validating the origin
|
|
724
|
+
* match and writing the workflow file, so a mismatched checkout or a
|
|
725
|
+
* filesystem failure could leave the database falsely claiming CI is wired
|
|
726
|
+
* with no file to show for it. Validate-then-write still fully precedes the
|
|
727
|
+
* transition below it, never in parallel with it and never ahead of it.
|
|
728
|
+
*
|
|
729
|
+
* Round-12 review finding 1 (CONFIRMED, BLOCKER): `--write-workflow` no
|
|
730
|
+
* longer makes the transition happen — a file landing on disk is not
|
|
731
|
+
* evidence CI actually ran, even now that round-13 review finding 2 fixed
|
|
732
|
+
* the underlying workflow so it CAN run for a foreign repo (see
|
|
733
|
+
* `renderCodebusWorkflow`'s own comment) — the operator's GitHub-side setup
|
|
734
|
+
* (secrets, the `graph-publish` environment) is still unverified at the
|
|
735
|
+
* moment this file is written. `--build`'s own transition-before-build
|
|
736
|
+
* state-machine requirement (plan's own "Blast radius" note — the
|
|
737
|
+
* attachment must not sit `pending` while a build has already run) is
|
|
738
|
+
* unaffected: `--build` transitions unconditionally before it runs, since a
|
|
739
|
+
* `--build` run is a real, local, end-to-end proof that ingest works, not
|
|
740
|
+
* merely that a file exists. (Round-13 review finding 3: a genuine CI
|
|
741
|
+
* ingest arriving while still `pending` now ALSO advances the attachment —
|
|
742
|
+
* see 0063_graph_content.sql's `graph_ingest_artifact` — which is what
|
|
743
|
+
* actually closes the loop for the foreign-repo case this fix targets.)
|
|
744
|
+
*
|
|
745
|
+
* Round-12 review finding 5 (CONFIRMED, MAJOR): the previous revision
|
|
746
|
+
* minted the ingest token FIRST, before the workflow validate/write and the
|
|
747
|
+
* transition — either one failing afterward threw past the point where the
|
|
748
|
+
* token could ever be printed, leaving an active, unrecoverable plaintext
|
|
749
|
+
* token that no caller ever saw. Fixed by moving the mint to LAST among the
|
|
750
|
+
* fallible steps that do not themselves need it — after the workflow write
|
|
751
|
+
* and the transition, both of which can fail for reasons having nothing to
|
|
752
|
+
* do with the token, so neither one can waste a mint anymore. The one step
|
|
753
|
+
* that still runs AFTER the mint is `--build` itself, which cannot run
|
|
754
|
+
* without a live token to authenticate as — an unavoidable ordering, not an
|
|
755
|
+
* oversight, so a `--build` failure past this point still throws with the
|
|
756
|
+
* token embedded directly in the thrown error's message (see below) rather
|
|
757
|
+
* than silently discarding the only copy the way a bare re-throw would.
|
|
758
|
+
*/
|
|
759
|
+
export async function repoConnectFull(ctx, cfg, url, opts, visibilityChecker = realRepoVisibilityChecker()) {
|
|
760
|
+
const core = await repoConnectCore(ctx, url, opts);
|
|
761
|
+
const defaultBranch = opts.branch ?? "main";
|
|
762
|
+
const result = { ...core, defaultBranch };
|
|
763
|
+
if (!result.is_authority)
|
|
764
|
+
return result;
|
|
765
|
+
// (F4) Validate + write BEFORE transitioning — see the function doc above.
|
|
766
|
+
if (opts.writeWorkflow) {
|
|
767
|
+
// Round-12 review finding F1 (CONFIRMED, MAJOR): checked BEFORE the
|
|
768
|
+
// origin check below (and, transitively, before mint/transition) —
|
|
769
|
+
// this is a property of the CONNECTED repo alone (owner + GitHub
|
|
770
|
+
// visibility), independent of the local checkout, so it belongs with
|
|
771
|
+
// the other fallible-but-side-effect-free preflight checks per the
|
|
772
|
+
// round-12 "mint last" rule. See assertEligibleForCodebusReusableWorkflow's
|
|
773
|
+
// own doc for why an ineligible repo's emitted workflow can never
|
|
774
|
+
// actually run.
|
|
775
|
+
await assertEligibleForCodebusReusableWorkflow(result.owner, result.name, visibilityChecker);
|
|
776
|
+
validateOriginMatchesConnectedRepo(opts.repoRoot, result.owner, result.name, "--write-workflow");
|
|
777
|
+
const workflowPath = join(opts.repoRoot, ".github", "workflows", "codebus.yml");
|
|
778
|
+
mkdirSync(dirname(workflowPath), { recursive: true });
|
|
779
|
+
writeFileSync(workflowPath, renderCodebusWorkflow({
|
|
780
|
+
owner: result.owner,
|
|
781
|
+
name: result.name,
|
|
782
|
+
includePrSyncStub: opts.prSync !== false,
|
|
783
|
+
defaultBranch, // (F6) the workflow's push trigger uses the resolved default branch, not a hardcoded 'main'.
|
|
784
|
+
}));
|
|
785
|
+
result.workflowWritten = workflowPath;
|
|
786
|
+
}
|
|
787
|
+
// Round-16 review finding 1, part 2 (CONFIRMED, BLOCKER): --build gets
|
|
788
|
+
// the IDENTICAL origin check, independently of --write-workflow — see
|
|
789
|
+
// `validateOriginMatchesConnectedRepo`'s own doc for why this must not
|
|
790
|
+
// be conditioned on --write-workflow having also been passed. Runs here
|
|
791
|
+
// (before mint, like the --write-workflow check above) so a mismatched
|
|
792
|
+
// checkout never wastes a mint either.
|
|
793
|
+
//
|
|
794
|
+
// Round-17 review finding 1 (CONFIRMED, MAJOR): `resolveTrustedGraph
|
|
795
|
+
// PublishScript` used to run INSIDE the `if (opts.build)` block below —
|
|
796
|
+
// i.e. AFTER the mint and the ci_wired transition — so a CLI installation
|
|
797
|
+
// missing its shipped `dist/kit/graph-publish.mjs` artifact threw a
|
|
798
|
+
// plain, token-free `CodebusError` at that point, silently discarding the
|
|
799
|
+
// token the mint just above it had already produced (the exact class of
|
|
800
|
+
// bug round-14 finding 4 and round-15 finding 2 already closed for the
|
|
801
|
+
// transition/build-execution failure sites — this was the one remaining
|
|
802
|
+
// pre-mint-eligible check that hadn't been moved yet). It is a PURE
|
|
803
|
+
// filesystem check with no side effects of its own, so — like the origin
|
|
804
|
+
// check immediately above — it belongs here, with the other fallible-
|
|
805
|
+
// but-safe pre-flight validations, per the round-12 "mint last" rule
|
|
806
|
+
// (repoConnectFull's own doc above): nothing after this point up to the
|
|
807
|
+
// mint call itself can still fail for a reason having nothing to do with
|
|
808
|
+
// the token. `trustedScriptPath` is resolved ONCE here and reused by the
|
|
809
|
+
// `if (opts.build)` block further below — never re-resolved there — so
|
|
810
|
+
// there is exactly one call site, not two that could disagree.
|
|
811
|
+
let trustedScriptPath;
|
|
812
|
+
if (opts.build) {
|
|
813
|
+
validateOriginMatchesConnectedRepo(opts.repoRoot, result.owner, result.name, "--build");
|
|
814
|
+
trustedScriptPath = resolveTrustedGraphPublishScript(import.meta.url);
|
|
815
|
+
}
|
|
816
|
+
// Round-12 review finding 5 (CONFIRMED): mint the token BEFORE the
|
|
817
|
+
// transition below — see the function doc above for why the ordering
|
|
818
|
+
// among these fallible steps that don't yet need `--build` itself is
|
|
819
|
+
// (workflow write) -> (mint) -> (transition) -> (build). Round-13 review
|
|
820
|
+
// finding 3 (CONFIRMED, MAJOR) is exactly why mint now precedes the
|
|
821
|
+
// transition rather than following it as an earlier revision had it:
|
|
822
|
+
// minting is itself fallible, and transitioning to `ci_wired` BEFORE a
|
|
823
|
+
// mint that then fails would leave the database claiming CI is wired
|
|
824
|
+
// while the operator never received a token to prove it — a second,
|
|
825
|
+
// narrower instance of the same "don't claim ci_wired for something that
|
|
826
|
+
// didn't actually succeed" principle round-12 finding 1 established for
|
|
827
|
+
// `--write-workflow`. `--build` itself still needs a real token to run,
|
|
828
|
+
// so mint must precede it either way; the only question this fix
|
|
829
|
+
// answers is mint-vs-transition order, and the answer is mint first.
|
|
830
|
+
const { data: token, error: tokenErr } = await ctx.supabase.rpc("graph_ingest_token_mint", {
|
|
831
|
+
p_repo_id: result.repo_id,
|
|
832
|
+
p_scope: "ingest",
|
|
833
|
+
});
|
|
834
|
+
if (tokenErr)
|
|
835
|
+
throw new CodebusError(`repo connected, but minting an ingest token failed: ${tokenErr.message}`);
|
|
836
|
+
result.token = String(token);
|
|
837
|
+
// Round-12 review finding 1 (CONFIRMED): --write-workflow alone no longer
|
|
838
|
+
// drives this transition — only --build does, since only --build is real
|
|
839
|
+
// evidence CI (or a local stand-in for it) can actually run. See the
|
|
840
|
+
// function doc above. Round-13 review finding 3: now gated on the mint
|
|
841
|
+
// above having already succeeded (see that comment).
|
|
842
|
+
const wantsTransition = !!opts.build;
|
|
843
|
+
if (wantsTransition && result.attachment_state === "pending") {
|
|
844
|
+
const { error: transErr } = await ctx.supabase.rpc("graph_attachment_transition", {
|
|
845
|
+
p_repo_id: result.repo_id, p_to_state: "ci_wired", p_note: "codebus repo connect",
|
|
846
|
+
});
|
|
847
|
+
// Round-14 review finding 4 (CONFIRMED, MAJOR): this throw used to be a
|
|
848
|
+
// plain, token-free CodebusError — but it runs AFTER the mint above
|
|
849
|
+
// already succeeded, the exact same "a later failure must never lose
|
|
850
|
+
// the operator's only copy of the plaintext" hazard PostMintFailureError
|
|
851
|
+
// exists for. Every post-mint failure site uses it now, not just
|
|
852
|
+
// --build's.
|
|
853
|
+
if (transErr)
|
|
854
|
+
throw new PostMintFailureError(`repo connected and minted an ingest token, but the pending -> ci_wired transition failed: ${transErr.message}`, result.token, "transition");
|
|
855
|
+
result.transitioned = true;
|
|
856
|
+
}
|
|
857
|
+
if (opts.build) {
|
|
858
|
+
// Suppressed under --json (not one of the raw child-process bytes below,
|
|
859
|
+
// which stdio:'inherit' cannot redirect without a scope change): this is
|
|
860
|
+
// OUR OWN informational line, and a --json caller piping stdout to a
|
|
861
|
+
// parser must see nothing but the final JSON object.
|
|
862
|
+
if (!opts.json)
|
|
863
|
+
console.log("\nrunning graphify locally (trusted graph-publish.mjs)...");
|
|
864
|
+
// Round-16 review finding 1, part 1: the trusted, CLI-shipped script —
|
|
865
|
+
// never anything inside opts.repoRoot. Round-17 review finding 1:
|
|
866
|
+
// resolved BEFORE mint now (see above) — the non-null assertion here is
|
|
867
|
+
// safe by construction: `trustedScriptPath` is only ever left
|
|
868
|
+
// `undefined` when `opts.build` is falsy, and this whole block only
|
|
869
|
+
// runs when it's truthy (the SAME condition, checked just above).
|
|
870
|
+
const scriptPath = trustedScriptPath;
|
|
871
|
+
const buildEnv = {
|
|
872
|
+
...process.env,
|
|
873
|
+
SUPABASE_URL: cfg.url,
|
|
874
|
+
SUPABASE_ANON_KEY: cfg.anonKey ?? "",
|
|
875
|
+
GRAPH_INGEST_TOKEN: result.token,
|
|
876
|
+
// Round-16 review finding 1: explicit, not inherited — the trusted
|
|
877
|
+
// script's own boundary/containment check (round-15 review finding 1)
|
|
878
|
+
// must be anchored at opts.repoRoot regardless of whatever
|
|
879
|
+
// GITHUB_WORKSPACE/GRAPHIFY_ROOT_BOUNDARY this PROCESS'S OWN
|
|
880
|
+
// environment happens to carry (`...process.env` above would
|
|
881
|
+
// otherwise let a stray inherited value leak through unchallenged).
|
|
882
|
+
// There is no separate "kit" directory to keep graphify away from in
|
|
883
|
+
// this LOCAL --build path — the trusted script itself lives inside
|
|
884
|
+
// the CLI's own installation, nowhere near opts.repoRoot — so
|
|
885
|
+
// workspace root, boundary, and target are all simply opts.repoRoot.
|
|
886
|
+
GITHUB_WORKSPACE: opts.repoRoot,
|
|
887
|
+
GRAPHIFY_ROOT_BOUNDARY: opts.repoRoot,
|
|
888
|
+
GRAPHIFY_REPO_ROOT: ".",
|
|
889
|
+
};
|
|
890
|
+
if (opts.json) {
|
|
891
|
+
// Round-12 review finding 2 (CONFIRMED, BLOCKER): stdio:'inherit' here
|
|
892
|
+
// let graph-publish.mjs's OWN stdout writes land directly on THIS
|
|
893
|
+
// process's stdout, ahead of (and mixed into) the single JSON object
|
|
894
|
+
// connectAction prints once repoConnectFull returns — breaking the
|
|
895
|
+
// "stdout is exactly one JSON object" contract F3 established the
|
|
896
|
+
// instant --build was combined with --json. Fixed by piping the
|
|
897
|
+
// child's stdio instead of inheriting it, capturing everything it
|
|
898
|
+
// writes, and re-emitting that capture on OUR OWN stderr (never
|
|
899
|
+
// stdout) — visible to a human watching the terminal, invisible to a
|
|
900
|
+
// parser reading stdout — while also attaching it to the result as
|
|
901
|
+
// `buildLog` so a --json caller gets it back structurally too.
|
|
902
|
+
//
|
|
903
|
+
// `spawnSync` (not `execFileSync`) deliberately: execFileSync's return
|
|
904
|
+
// value on a SUCCESSFUL run only ever carries stdout — anything the
|
|
905
|
+
// child wrote to stderr on a zero exit is not retrievable from it at
|
|
906
|
+
// all (only a THROWN execFileSync error carries `.stderr`, i.e. only
|
|
907
|
+
// on a non-zero exit). spawnSync instead always returns
|
|
908
|
+
// `{stdout, stderr, status}` uniformly regardless of exit code, so a
|
|
909
|
+
// future warning graph-publish.mjs writes to stderr on an otherwise
|
|
910
|
+
// successful run is captured here too, not silently dropped.
|
|
911
|
+
const spawnResult = spawnSync(process.execPath, [scriptPath], { cwd: opts.repoRoot, env: buildEnv, encoding: "utf-8" });
|
|
912
|
+
const captured = (spawnResult.stdout ?? "") + (spawnResult.stderr ?? "");
|
|
913
|
+
if (captured)
|
|
914
|
+
process.stderr.write(captured);
|
|
915
|
+
result.buildLog = captured || undefined;
|
|
916
|
+
// Round-13 review finding 7 (CONFIRMED, MAJOR): `spawnResult.status`
|
|
917
|
+
// is `null` — never `0` — when the child was terminated by a signal
|
|
918
|
+
// rather than exiting normally (Node sets `.signal` instead, and does
|
|
919
|
+
// NOT set `.error` for this case; `.error` is reserved for the SPAWN
|
|
920
|
+
// itself failing, e.g. ENOENT). The previous `(spawnResult.status ??
|
|
921
|
+
// 0) !== 0` check coalesced that `null` to `0`, so a build killed by
|
|
922
|
+
// SIGKILL/SIGTERM (OOM, a CI job timeout sending SIGTERM, an operator
|
|
923
|
+
// Ctrl-C) was reported as a SUCCESSFUL build with `buildRan = true`.
|
|
924
|
+
// Signal termination is exactly as much a failure as a non-zero exit
|
|
925
|
+
// code — checked explicitly here, never coalesced away.
|
|
926
|
+
const buildFailed = !!spawnResult.error || spawnResult.status === null || spawnResult.status !== 0;
|
|
927
|
+
if (buildFailed) {
|
|
928
|
+
const reason = spawnResult.error
|
|
929
|
+
? spawnResult.error.message
|
|
930
|
+
: spawnResult.status === null
|
|
931
|
+
? `terminated by signal ${spawnResult.signal ?? "unknown"}`
|
|
932
|
+
: `exited with status ${spawnResult.status}`;
|
|
933
|
+
// Round-13 review finding 8 (CONFIRMED, MAJOR): the token is no
|
|
934
|
+
// longer embedded in this error's `message` — see
|
|
935
|
+
// `PostMintFailureError`'s own doc (errors.ts) for why. The
|
|
936
|
+
// mint above already ran, so the token must not disappear silently
|
|
937
|
+
// just because --json mode's own success path (a single stdout
|
|
938
|
+
// object) never gets a chance to run; it is carried as structured
|
|
939
|
+
// data instead, for the caller to render deliberately.
|
|
940
|
+
throw new PostMintFailureError(`--build failed (${reason}) — see graphify output above (stderr).`, result.token, "build");
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
try {
|
|
945
|
+
execFileSync(process.execPath, [scriptPath], { cwd: opts.repoRoot, stdio: "inherit", env: buildEnv });
|
|
946
|
+
}
|
|
947
|
+
catch (e) {
|
|
948
|
+
// graph-publish.mjs already printed its own error (quarantined /
|
|
949
|
+
// 25MB-reject / build failure) to inherited stdio — surface a plain
|
|
950
|
+
// non-zero exit rather than re-wrapping and hiding that detail.
|
|
951
|
+
// execFileSync itself already throws correctly for BOTH a non-zero
|
|
952
|
+
// exit AND signal termination (round-13 review finding 7 is a
|
|
953
|
+
// spawnSync-specific bug — execFileSync's own throw-on-any-abnormal-
|
|
954
|
+
// exit behavior was never affected by it), so no separate signal
|
|
955
|
+
// check is needed on this branch.
|
|
956
|
+
const err = e;
|
|
957
|
+
const reason = err.signal ? `terminated by signal ${err.signal}` : `exited with status ${err.status}`;
|
|
958
|
+
// (Round-13 review finding 8, see above.)
|
|
959
|
+
throw new PostMintFailureError(`--build failed (${reason}) — see graphify output above.`, result.token, "build");
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
result.buildRan = true;
|
|
963
|
+
}
|
|
964
|
+
return result;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* All of `repo connect`'s console output, split out from `connectAction`
|
|
968
|
+
* (round-12 review finding 6 regression coverage) purely so it can be unit
|
|
969
|
+
* tested without a live `makeCtx`/network round-trip — `repoConnectFull`
|
|
970
|
+
* above is the piece that does real work; this is a pure function of an
|
|
971
|
+
* already-resolved result, exactly parallel to why `repoConnectFull` itself
|
|
972
|
+
* does no printing (round-11 review finding F3's doc above).
|
|
973
|
+
*/
|
|
974
|
+
export function printConnectResult(result, cfg, json) {
|
|
975
|
+
// Round-11 review finding F3 (BLOCKER, CONFIRMED): the previous revision
|
|
976
|
+
// printed the JSON summary FIRST, then unconditionally printed the
|
|
977
|
+
// plaintext token + gh-secret-set instructions (and, on --build, the
|
|
978
|
+
// graphify wrapper line) AFTER it regardless of --json — contaminating
|
|
979
|
+
// machine-readable stdout with free text and never actually including the
|
|
980
|
+
// token in the JSON object it claimed to be. Fixed: in --json mode this is
|
|
981
|
+
// the ONLY line ever printed, the token (if any) lives INSIDE this single
|
|
982
|
+
// object, and nothing follows it. Human mode is entirely separate, below.
|
|
983
|
+
if (json) {
|
|
984
|
+
console.log(JSON.stringify(result, null, 2));
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
console.log(`${result.created ? "created" : "connected"}: ${result.owner}/${result.name} (slug '${result.slug}', ` +
|
|
988
|
+
`attachment ${result.attachment_state})`);
|
|
989
|
+
// Round-15 review finding (CONFIRMED, MINOR): `result.slug` above is
|
|
990
|
+
// ALWAYS the canonical slug repo_connect actually resolved to (never the
|
|
991
|
+
// requested one echoed back verbatim — see RepoConnectRpcResult's own
|
|
992
|
+
// doc). When it differs from what was requested, `repo status
|
|
993
|
+
// <requestedSlug>` would silently find nothing — surface the resolution
|
|
994
|
+
// explicitly so the operator knows which slug to actually use.
|
|
995
|
+
if (result.requestedSlug !== result.slug) {
|
|
996
|
+
console.log(`(requested slug '${result.requestedSlug}' resolved to canonical slug '${result.slug}' — use '${result.slug}' with \`codebus repo status\`)`);
|
|
997
|
+
}
|
|
998
|
+
if (!result.is_authority) {
|
|
999
|
+
console.error("\nrepo connected, but you are not this repo's authority (workspace admin or its designated curator) — " +
|
|
1000
|
+
"cannot mint an ingest token or wire CI from here. Ask an admin to run `codebus repo connect` again, or to " +
|
|
1001
|
+
"grant you curator status, then re-run this command (it is safe to repeat).");
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
// One-time ingest token: printed here, in human mode, exactly once.
|
|
1005
|
+
// Round-12 review finding 6 (CONFIRMED, MAJOR): the previous revision
|
|
1006
|
+
// printed the SAME plaintext token a second time, inline inside the
|
|
1007
|
+
// `gh secret set --body '<token>'` command immediately below it — two
|
|
1008
|
+
// appearances in the same block, contradicting the "print once" contract
|
|
1009
|
+
// round-11 review finding F3 established (that finding was about
|
|
1010
|
+
// --json-vs-human duplication; this is a second, distinct duplication
|
|
1011
|
+
// entirely within human mode's own output). Fixed: the token is shown
|
|
1012
|
+
// exactly once, standalone, and the `gh secret set` line for it references
|
|
1013
|
+
// that value with a placeholder + a stdin pipe (`printf '%s' ... | gh
|
|
1014
|
+
// secret set ... --repo ... --env ...`, no `--body`) rather than repeating
|
|
1015
|
+
// the literal secret inline in a second command.
|
|
1016
|
+
// Round-14 review finding 1 (CONFIRMED, BLOCKER): Bragi-GmbH/CodeBus is a
|
|
1017
|
+
// PRIVATE repo, so the emitted workflow's kit checkout cannot rely on
|
|
1018
|
+
// github.token for any caller other than CodeBus itself — every operator
|
|
1019
|
+
// running --write-workflow against a foreign repo must ALSO set a
|
|
1020
|
+
// CODEBUS_KIT_TOKEN secret (a GitHub PAT/App token with read access to
|
|
1021
|
+
// Bragi-GmbH/CodeBus), or the reusable workflow's own fail-fast check
|
|
1022
|
+
// refuses the run outright. Named here alongside the other required
|
|
1023
|
+
// secrets so an operator sees the complete list in one place, not just
|
|
1024
|
+
// the subset the token-mint step itself happens to produce.
|
|
1025
|
+
console.log("\nOne-time ingest token (store it now — it cannot be retrieved again):\n" +
|
|
1026
|
+
` ${result.token}\n\n` +
|
|
1027
|
+
"Add it (and the connection details below) as GitHub Environment secrets named 'graph-publish' — pipe the " +
|
|
1028
|
+
"token shown above in rather than pasting it into another command's argv:\n" +
|
|
1029
|
+
` printf '%s' '<TOKEN-ABOVE>' | gh secret set GRAPH_INGEST_TOKEN --repo ${result.owner}/${result.name} --env graph-publish\n` +
|
|
1030
|
+
` gh secret set SUPABASE_URL --body '${cfg.url}' --repo ${result.owner}/${result.name} --env graph-publish\n` +
|
|
1031
|
+
` gh secret set SUPABASE_ANON_KEY --body '${cfg.anonKey ?? "<your anon key>"}' --repo ${result.owner}/${result.name} --env graph-publish\n\n` +
|
|
1032
|
+
"Bragi-GmbH/CodeBus is a PRIVATE repo, so the emitted workflow ALSO needs a GitHub PAT or App token with read " +
|
|
1033
|
+
"access to it (used only to check out CodeBus's own scripts/lockfiles) — set this too, or the workflow refuses to run:\n" +
|
|
1034
|
+
` gh secret set CODEBUS_KIT_TOKEN --body '<a PAT/App token that can read Bragi-GmbH/CodeBus>' --repo ${result.owner}/${result.name} --env graph-publish`);
|
|
1035
|
+
if (result.transitioned)
|
|
1036
|
+
console.log("attachment: pending -> ci_wired");
|
|
1037
|
+
if (result.workflowWritten)
|
|
1038
|
+
console.log(`wrote ${result.workflowWritten}`);
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Round-13 review finding 8 (CONFIRMED, MAJOR): the ONLY place a
|
|
1042
|
+
* `PostMintFailureError`'s token may ever be rendered — deliberately,
|
|
1043
|
+
* exactly once, matching `printConnectResult`'s own print-once discipline.
|
|
1044
|
+
* If `e` is not this specific error type, it is returned UNCHANGED for the
|
|
1045
|
+
* caller to rethrow as-is (this function performs no printing in that
|
|
1046
|
+
* case, so the generic handler in `cli/src/index.ts` remains the ONLY
|
|
1047
|
+
* thing that ever prints an ordinary error's message).
|
|
1048
|
+
*
|
|
1049
|
+
* In `--json` mode the token still lands on stdout — but wrapped in a
|
|
1050
|
+
* fresh, single JSON object (never free text), so a caller piping stdout
|
|
1051
|
+
* to a JSON parser still sees exactly one parseable object, now shaped as
|
|
1052
|
+
* an error result instead of a success result. In human mode it prints as
|
|
1053
|
+
* a clearly-marked, standalone block, exactly like the success path's own
|
|
1054
|
+
* one-time token block above.
|
|
1055
|
+
*
|
|
1056
|
+
* Round-15 review finding 2 (CONFIRMED, MINOR): the human-mode block used
|
|
1057
|
+
* to hardcode "--build failed" regardless of which post-mint step actually
|
|
1058
|
+
* threw — wrong for the transition-failure site (round-14 finding 4 added
|
|
1059
|
+
* that site; it fails before any build ever starts). The diagnostic is now
|
|
1060
|
+
* built from `e.stage`.
|
|
1061
|
+
*
|
|
1062
|
+
* Always returns a plain `CodebusError` carrying the ORIGINAL message
|
|
1063
|
+
* (never the token) for the caller to throw — that is what reaches
|
|
1064
|
+
* `index.ts`'s generic `console.error(\`error: ${e.message}\`)` handler,
|
|
1065
|
+
* which must never see the plaintext token in anything it prints.
|
|
1066
|
+
*/
|
|
1067
|
+
export function handleConnectError(e, json) {
|
|
1068
|
+
if (!(e instanceof PostMintFailureError))
|
|
1069
|
+
return e;
|
|
1070
|
+
const stageDescription = e.stage === "transition" ? "the pending -> ci_wired transition failed" : "--build failed";
|
|
1071
|
+
if (json) {
|
|
1072
|
+
console.log(JSON.stringify({ error: e.message, token: e.token, stage: e.stage }, null, 2));
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
console.log(`\n${stageDescription}, but an ingest token WAS minted for this connection before the failure — ` +
|
|
1076
|
+
"store it now, it cannot be retrieved again:\n" +
|
|
1077
|
+
` ${e.token}\n`);
|
|
1078
|
+
}
|
|
1079
|
+
return new CodebusError(e.message, e.exitCode);
|
|
1080
|
+
}
|
|
1081
|
+
async function connectAction(url, o) {
|
|
1082
|
+
const cfg = resolveConfig(o);
|
|
1083
|
+
const ctx = await makeCtx(cfg);
|
|
1084
|
+
const repoRoot = o.repoRoot ?? process.cwd();
|
|
1085
|
+
try {
|
|
1086
|
+
const result = await repoConnectFull(ctx, cfg, url, { ...o, repoRoot });
|
|
1087
|
+
printConnectResult(result, cfg, !!o.json);
|
|
1088
|
+
}
|
|
1089
|
+
catch (e) {
|
|
1090
|
+
throw handleConnectError(e, !!o.json);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
// ── repo status ─────────────────────────────────────────────────────────
|
|
1094
|
+
async function statusAction(slug, o) {
|
|
1095
|
+
const ctx = await makeCtx(resolveConfig(o));
|
|
1096
|
+
const ws = await workspaceId(ctx);
|
|
1097
|
+
const rid = await resolveRepoIdBySlug(ctx, ws, slug);
|
|
1098
|
+
if (!rid)
|
|
1099
|
+
throw new CodebusError(`unknown repo: ${slug}`);
|
|
1100
|
+
const { data: statusRows, error: statusErr } = await ctx.supabase
|
|
1101
|
+
.from("graph_status")
|
|
1102
|
+
.select("*")
|
|
1103
|
+
.eq("project_id", rid)
|
|
1104
|
+
.maybeSingle();
|
|
1105
|
+
if (statusErr)
|
|
1106
|
+
throw new CodebusError(statusErr.message);
|
|
1107
|
+
const { data: prRows, error: prErr } = await ctx.supabase
|
|
1108
|
+
.from("pull_requests")
|
|
1109
|
+
.select("status")
|
|
1110
|
+
.eq("repo_id", rid);
|
|
1111
|
+
if (prErr)
|
|
1112
|
+
throw new CodebusError(prErr.message);
|
|
1113
|
+
const prCounts = {};
|
|
1114
|
+
for (const r of prRows ?? [])
|
|
1115
|
+
prCounts[r.status] = (prCounts[r.status] ?? 0) + 1;
|
|
1116
|
+
const result = { slug, graph: statusRows ?? null, pull_requests: prCounts };
|
|
1117
|
+
if (o.json) {
|
|
1118
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
console.log(render([{
|
|
1122
|
+
slug,
|
|
1123
|
+
attachment_state: statusRows?.attachment_state ?? "(no attachment)",
|
|
1124
|
+
freshness: statusRows?.freshness ?? "(no graph)",
|
|
1125
|
+
grade: statusRows?.grade ?? "(ungraded)",
|
|
1126
|
+
prs: Object.entries(prCounts).map(([k, v]) => `${k}:${v}`).join(" ") || "(none)",
|
|
1127
|
+
}], { json: false }));
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
// ── repo list ───────────────────────────────────────────────────────────
|
|
1131
|
+
/**
|
|
1132
|
+
* Shapes one raw `repos` (+ joined `graph_attachments`) row into `repo
|
|
1133
|
+
* list`'s output row — used for BOTH --json and human-mode, since `render`
|
|
1134
|
+
* (below) feeds the exact same `rows` array to either presentation.
|
|
1135
|
+
*
|
|
1136
|
+
* Round-19 review finding (CONFIRMED, PR #55 comment 3827524459):
|
|
1137
|
+
* `remote_url` used to reach this row (and therefore a terminal or a
|
|
1138
|
+
* --json consumer) verbatim. 0070's own migration deliberately AUDITS
|
|
1139
|
+
* (reports) pre-existing userinfo-bearing remote_url rows rather than
|
|
1140
|
+
* rewriting them in place — a silent server-side rewrite of a column an
|
|
1141
|
+
* operator's own tooling may still expect verbatim was judged the worse
|
|
1142
|
+
* hazard — so its trigger only REJECTS *new* writes; a legacy
|
|
1143
|
+
* `https://token@github.com/...` row from before 0070 existed can
|
|
1144
|
+
* genuinely still be sitting in this table today. `repo list` was the one
|
|
1145
|
+
* remaining place that echoed such a row unredacted. Extracted to its own
|
|
1146
|
+
* function (rather than an inline `.map()` lambda) specifically so this
|
|
1147
|
+
* redaction has a direct unit test against a credential-bearing fixture —
|
|
1148
|
+
* `listAction` itself follows this file's usual convention of being
|
|
1149
|
+
* covered by the real-subprocess integration suite instead, since it does
|
|
1150
|
+
* nothing beyond wiring this shape to a live query and `render`.
|
|
1151
|
+
*/
|
|
1152
|
+
export function shapeRepoListRow(r) {
|
|
1153
|
+
return {
|
|
1154
|
+
slug: r.slug,
|
|
1155
|
+
remote_url: r.remote_url ? redactUrlUserinfo(r.remote_url) : "(none)",
|
|
1156
|
+
default_branch: r.default_branch,
|
|
1157
|
+
attachment_state: (Array.isArray(r.graph_attachments) ? r.graph_attachments[0]?.state : r.graph_attachments?.state) ?? "(none)",
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
async function listAction(o) {
|
|
1161
|
+
const ctx = await makeCtx(resolveConfig(o));
|
|
1162
|
+
const ws = await workspaceId(ctx);
|
|
1163
|
+
const { data, error } = await ctx.supabase
|
|
1164
|
+
.from("repos")
|
|
1165
|
+
.select("slug, remote_url, default_branch, retired_into, graph_attachments(state)")
|
|
1166
|
+
.eq("workspace_id", ws)
|
|
1167
|
+
.order("slug");
|
|
1168
|
+
if (error)
|
|
1169
|
+
throw new CodebusError(error.message);
|
|
1170
|
+
const rows = (data ?? [])
|
|
1171
|
+
.filter((r) => !r.retired_into)
|
|
1172
|
+
.map(shapeRepoListRow);
|
|
1173
|
+
console.log(render(rows, { json: !!o.json }));
|
|
1174
|
+
}
|
|
1175
|
+
// ── registration ────────────────────────────────────────────────────────
|
|
1176
|
+
export function registerRepo(program, g) {
|
|
1177
|
+
const repo = program.command("repo").description("connect and inspect GitHub repos (CB repo-intelligence)");
|
|
1178
|
+
g(repo.command("connect").argument("<github-url>", "https/ssh GitHub URL, or bare owner/repo"))
|
|
1179
|
+
.option("--slug <slug>", "repo slug within this workspace (default: the repo name)")
|
|
1180
|
+
// Round-11 review finding F6 (CONFIRMED): the plan's own file spec
|
|
1181
|
+
// (docs/plans/repo-intelligence.md A4) names this flag `--default-branch`
|
|
1182
|
+
// — matching the `p_default_branch` RPC parameter it feeds and the
|
|
1183
|
+
// `repos.default_branch` column it lands in — not `--branch`. `--branch`
|
|
1184
|
+
// is kept as a deprecated alias rather than a breaking rename; whichever
|
|
1185
|
+
// is given wins (both given: --default-branch wins), neither given
|
|
1186
|
+
// defaults to 'main' exactly as before.
|
|
1187
|
+
.option("--default-branch <branch>", "default branch (default: main)")
|
|
1188
|
+
.option("--branch <branch>", "deprecated alias for --default-branch")
|
|
1189
|
+
.option("--force", "overwrite an existing, different remote_url (requires workspace admin or curator)")
|
|
1190
|
+
.option("--write-workflow", "write .github/workflows/codebus.yml (only when cwd's origin matches) — does NOT by itself mark the attachment ci_wired; run --build (or a real successful CI ingest, once available) to earn that")
|
|
1191
|
+
.option("--build", "run graphify locally against the freshly minted token — the only thing that transitions the attachment to ci_wired")
|
|
1192
|
+
.option("--no-pr-sync", "omit the (currently disabled) pr-sync job stub from the emitted workflow")
|
|
1193
|
+
.action((url, o) => connectAction(url, { ...o, branch: o.defaultBranch ?? o.branch ?? "main" }));
|
|
1194
|
+
g(repo.command("status").argument("<slug>", "repo slug"))
|
|
1195
|
+
.action((slug, o) => statusAction(slug, o));
|
|
1196
|
+
g(repo.command("list")).action((o) => listAction(o));
|
|
1197
|
+
}
|
|
1198
|
+
//# sourceMappingURL=repo.js.map
|