@buildinternet/uploads 0.15.0 → 0.16.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 +11 -0
- package/dist/cli-catalog.js +4 -1
- package/dist/client.d.ts +39 -2
- package/dist/client.js +18 -0
- package/dist/commands.d.ts +15 -1
- package/dist/commands.js +179 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,6 +83,17 @@ infers the pull request for the current branch via `gh`, uploads stable URLs, an
|
|
|
83
83
|
or updates one marker-owned GitHub comment. It keeps loose `gh/...` attachments and linked public galleries in distinct sections, shows up to three available gallery images inline, and updates that same comment in place on every sync. Use `--pr`, `--issue`, and `--repo` to select
|
|
84
84
|
the target explicitly, or `--no-comment` to upload without changing GitHub comments.
|
|
85
85
|
|
|
86
|
+
**Branch staging (pre-PR):** `attach <files> --branch [name]` (also on
|
|
87
|
+
`screenshot`) stages files against a git branch before any PR exists — the
|
|
88
|
+
working mode for coding agents capturing as they go. Staged files live under
|
|
89
|
+
`gh/<owner>/<repo>/branch/<branch>/…` and are promoted into the PR's
|
|
90
|
+
attachments when one opens: automatically via the
|
|
91
|
+
[GitHub App](https://uploads.sh/docs/github-app) webhook, or on the first
|
|
92
|
+
`attach` after the PR exists (`--promote` forces it with no new files,
|
|
93
|
+
`--no-promote` opts out). `uploads github link` inspects or claims the
|
|
94
|
+
workspace↔repo binding the webhook path uses. Promoted staging is cleaned up
|
|
95
|
+
server-side after ~7 days (~30 for branches that never got a PR).
|
|
96
|
+
|
|
86
97
|
**Screenshot capture:** `uploads screenshot <url|file.html>` renders a page to a
|
|
87
98
|
hosted image in one step — no separate browser tooling needed. `--via auto`
|
|
88
99
|
(default) drives a Chrome/Chromium already on the machine (`playwright-core`
|
package/dist/cli-catalog.js
CHANGED
|
@@ -136,7 +136,10 @@ export const ROOT_COMMANDS = [
|
|
|
136
136
|
{
|
|
137
137
|
name: "github",
|
|
138
138
|
summary: "Claim/inspect this workspace's binding to a GitHub repo",
|
|
139
|
-
subcommands: [
|
|
139
|
+
subcommands: [
|
|
140
|
+
{ name: "link", summary: "Claim or inspect the repo binding" },
|
|
141
|
+
{ name: "doctor", summary: "Check the GitHub App's webhook event subscriptions" },
|
|
142
|
+
],
|
|
140
143
|
},
|
|
141
144
|
{
|
|
142
145
|
name: "list",
|
package/dist/client.d.ts
CHANGED
|
@@ -172,8 +172,16 @@ export interface FindGalleriesByReferenceOptions {
|
|
|
172
172
|
limit?: number;
|
|
173
173
|
cursor?: string;
|
|
174
174
|
}
|
|
175
|
-
/**
|
|
176
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Reasons the bot did not post. The CLI falls back to the local `gh` path
|
|
177
|
+
* for all of these except `not_authorized` (issue #297 baseline control):
|
|
178
|
+
* the target repo is bound to a different workspace (or is unbound and the
|
|
179
|
+
* caller is the communal `default` workspace, which can't claim new repos).
|
|
180
|
+
* Falling back to `gh` there would let the human's own credentials post
|
|
181
|
+
* anyway, defeating the point of the server-side gate, so the CLI surfaces
|
|
182
|
+
* the decline instead.
|
|
183
|
+
*/
|
|
184
|
+
export type GithubCommentDeclineReason = "app_unconfigured" | "not_installed" | "forbidden" | "not_authorized" | "unavailable";
|
|
177
185
|
export type GithubCommentResult = {
|
|
178
186
|
posted: true;
|
|
179
187
|
action: "created" | "updated" | "skipped";
|
|
@@ -212,9 +220,24 @@ export interface GithubLinkResult {
|
|
|
212
220
|
export interface GithubLinkClaimResult extends GithubLinkResult {
|
|
213
221
|
claimed: boolean;
|
|
214
222
|
}
|
|
223
|
+
/** `DELETE /v1/:workspace/github/link` result (issue #318, self-serve unlink). */
|
|
224
|
+
export interface GithubLinkUnlinkResult {
|
|
225
|
+
repo: string;
|
|
226
|
+
unlinked: boolean;
|
|
227
|
+
reason?: "not_linked";
|
|
228
|
+
}
|
|
215
229
|
export interface HealthResult {
|
|
216
230
|
ok: boolean;
|
|
217
231
|
}
|
|
232
|
+
/** `GET /v1/:workspace/github/health` result (issue #293 follow-up). */
|
|
233
|
+
export interface GithubHealthResult {
|
|
234
|
+
configured: boolean;
|
|
235
|
+
ok: boolean;
|
|
236
|
+
events: string[] | null;
|
|
237
|
+
missingEvents: string[];
|
|
238
|
+
requiredEvents: string[];
|
|
239
|
+
hint?: string;
|
|
240
|
+
}
|
|
218
241
|
export interface UsageResult {
|
|
219
242
|
workspace: string;
|
|
220
243
|
bytes: number;
|
|
@@ -462,6 +485,20 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
462
485
|
* server without this route.
|
|
463
486
|
*/
|
|
464
487
|
githubLinkClaim(repo: string): Promise<GithubLinkClaimResult>;
|
|
488
|
+
/**
|
|
489
|
+
* GitHub App configuration + webhook event subscription check. Throws
|
|
490
|
+
* `UploadsError` (status 404) on an older/self-hosted server without
|
|
491
|
+
* this route — callers treat that as "unknown", not "broken".
|
|
492
|
+
*/
|
|
493
|
+
githubHealth(): Promise<GithubHealthResult>;
|
|
494
|
+
/**
|
|
495
|
+
* Self-serve unlink (issue #318): removes `repo`'s binding, but only if
|
|
496
|
+
* this workspace currently owns it. Throws `UploadsError` (status 403)
|
|
497
|
+
* when a different workspace owns the binding — never steals or
|
|
498
|
+
* overwrites another workspace's claim. Throws (status 404) on an
|
|
499
|
+
* older/self-hosted server without this route.
|
|
500
|
+
*/
|
|
501
|
+
githubLinkUnlink(repo: string): Promise<GithubLinkUnlinkResult>;
|
|
465
502
|
health(): Promise<HealthResult>;
|
|
466
503
|
/** Workspace storage / upload counters (+ limits when configured). */
|
|
467
504
|
usage(): Promise<UsageResult>;
|
package/dist/client.js
CHANGED
|
@@ -489,6 +489,24 @@ export function createUploadsClient(config) {
|
|
|
489
489
|
headers: { "Content-Type": "application/json" },
|
|
490
490
|
});
|
|
491
491
|
},
|
|
492
|
+
/**
|
|
493
|
+
* GitHub App configuration + webhook event subscription check. Throws
|
|
494
|
+
* `UploadsError` (status 404) on an older/self-hosted server without
|
|
495
|
+
* this route — callers treat that as "unknown", not "broken".
|
|
496
|
+
*/
|
|
497
|
+
async githubHealth() {
|
|
498
|
+
return request("GET", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/health`);
|
|
499
|
+
},
|
|
500
|
+
/**
|
|
501
|
+
* Self-serve unlink (issue #318): removes `repo`'s binding, but only if
|
|
502
|
+
* this workspace currently owns it. Throws `UploadsError` (status 403)
|
|
503
|
+
* when a different workspace owns the binding — never steals or
|
|
504
|
+
* overwrites another workspace's claim. Throws (status 404) on an
|
|
505
|
+
* older/self-hosted server without this route.
|
|
506
|
+
*/
|
|
507
|
+
async githubLinkUnlink(repo) {
|
|
508
|
+
return request("DELETE", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/link?repo=${encodeURIComponent(repo)}`);
|
|
509
|
+
},
|
|
492
510
|
async health() {
|
|
493
511
|
return request("GET", `${config.apiUrl}/health`, { auth: false });
|
|
494
512
|
},
|
package/dist/commands.d.ts
CHANGED
|
@@ -33,7 +33,10 @@ export declare function ghTargetFromFlags(flags: CommandFlags["flags"], run: Com
|
|
|
33
33
|
* the current git branch (`resolveCurrentBranch`); `--branch feature/x` uses
|
|
34
34
|
* the given name verbatim. Returns undefined when the flag is absent at all
|
|
35
35
|
* (distinct from an empty/whitespace value, which is rejected). Throws
|
|
36
|
-
* UsageError if `--branch` is given more than once
|
|
36
|
+
* UsageError if `--branch` is given more than once, or if the value looks
|
|
37
|
+
* like a filename accidentally swallowed by the optional-value lookahead
|
|
38
|
+
* (e.g. `uploads attach --branch shot.png` with no other file args) — see
|
|
39
|
+
* `looksLikeFileNotBranch`.
|
|
37
40
|
*/
|
|
38
41
|
export declare function branchFromFlags(flags: CommandFlags["flags"], run: CommandRunner): string | undefined;
|
|
39
42
|
/** Shared put/attach optimize flags + UPLOADS_NO_OPTIMIZE default. */
|
|
@@ -112,6 +115,17 @@ export interface AttachmentsCommentResult {
|
|
|
112
115
|
}
|
|
113
116
|
/** Human-mode suffix noting who posted the managed comment. */
|
|
114
117
|
export declare function commentViaSuffix(via: AttachmentsCommentResult["via"]): string;
|
|
118
|
+
/**
|
|
119
|
+
* Thrown by `syncAttachmentsComment` when the server declines with
|
|
120
|
+
* `not_authorized` (issue #297 baseline control) — this repo is bound to a
|
|
121
|
+
* different workspace, or unbound and unclaimable by the communal `default`
|
|
122
|
+
* workspace. Deliberately not caught by the generic "bot endpoint
|
|
123
|
+
* unreachable" fallback below: falling back to gh here would let the
|
|
124
|
+
* human's own credentials post anyway, defeating the point of the
|
|
125
|
+
* server-side gate.
|
|
126
|
+
*/
|
|
127
|
+
export declare class GithubCommentAuthorizationError extends Error {
|
|
128
|
+
}
|
|
115
129
|
export declare function syncAttachmentsComment(client: UploadsClient, target: GhTarget, run: CommandRunner, workspace?: string): Promise<AttachmentsCommentResult>;
|
|
116
130
|
export type AttachUploadItem = PutResult & {
|
|
117
131
|
file: string;
|
package/dist/commands.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { basename } from "node:path";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, extname } from "node:path";
|
|
3
3
|
import { mapBounded } from "./async.js";
|
|
4
4
|
import { createUploadsClient, } from "./client.js";
|
|
5
5
|
import { parseCommandArgs, flagString, flagBool, flagInt, flagValues, UsageError, } from "./cli-args.js";
|
|
@@ -130,12 +130,53 @@ export function makeGhTarget(pr, issue, repoArg, run) {
|
|
|
130
130
|
export function ghTargetFromFlags(flags, run) {
|
|
131
131
|
return makeGhTarget(flagInt(flags, "--pr", "--pr"), flagInt(flags, "--issue", "--issue"), flagString(flags, "--repo"), run);
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Extensions that mark a `--branch` value as almost certainly a filename that
|
|
135
|
+
* got swallowed by the optional-value lookahead (e.g. `--branch shot.png`
|
|
136
|
+
* with no other file args). Branch names legitimately contain dots (e.g.
|
|
137
|
+
* `release/1.2`, `v1.2.3`), so this only matches known media/document
|
|
138
|
+
* extensions, never bare dotted segments.
|
|
139
|
+
*/
|
|
140
|
+
const BRANCH_LIKE_FILE_EXTENSIONS = new Set([
|
|
141
|
+
".png",
|
|
142
|
+
".jpg",
|
|
143
|
+
".jpeg",
|
|
144
|
+
".gif",
|
|
145
|
+
".webp",
|
|
146
|
+
".bmp",
|
|
147
|
+
".svg",
|
|
148
|
+
".ico",
|
|
149
|
+
".tif",
|
|
150
|
+
".tiff",
|
|
151
|
+
".heic",
|
|
152
|
+
".avif",
|
|
153
|
+
".mp4",
|
|
154
|
+
".mov",
|
|
155
|
+
".avi",
|
|
156
|
+
".webm",
|
|
157
|
+
".mkv",
|
|
158
|
+
".pdf",
|
|
159
|
+
]);
|
|
160
|
+
/**
|
|
161
|
+
* True when `value` looks like a filename that was mistakenly consumed as the
|
|
162
|
+
* `--branch` value: it names a file that exists on disk, or its extension is
|
|
163
|
+
* a known media/document type. Ordinary branch names (including dotted ones
|
|
164
|
+
* like `v1.2` or `release/1.2`) never match either check.
|
|
165
|
+
*/
|
|
166
|
+
function looksLikeFileNotBranch(value) {
|
|
167
|
+
if (existsSync(value))
|
|
168
|
+
return true;
|
|
169
|
+
return BRANCH_LIKE_FILE_EXTENSIONS.has(extname(value).toLowerCase());
|
|
170
|
+
}
|
|
133
171
|
/**
|
|
134
172
|
* Reads `--branch [name]` — an optional-value flag: `--branch` alone resolves
|
|
135
173
|
* the current git branch (`resolveCurrentBranch`); `--branch feature/x` uses
|
|
136
174
|
* the given name verbatim. Returns undefined when the flag is absent at all
|
|
137
175
|
* (distinct from an empty/whitespace value, which is rejected). Throws
|
|
138
|
-
* UsageError if `--branch` is given more than once
|
|
176
|
+
* UsageError if `--branch` is given more than once, or if the value looks
|
|
177
|
+
* like a filename accidentally swallowed by the optional-value lookahead
|
|
178
|
+
* (e.g. `uploads attach --branch shot.png` with no other file args) — see
|
|
179
|
+
* `looksLikeFileNotBranch`.
|
|
139
180
|
*/
|
|
140
181
|
export function branchFromFlags(flags, run) {
|
|
141
182
|
if (!flags.has("--branch"))
|
|
@@ -145,8 +186,14 @@ export function branchFromFlags(flags, run) {
|
|
|
145
186
|
throw new UsageError("--branch may only be given once");
|
|
146
187
|
if (raw === true)
|
|
147
188
|
return resolveCurrentBranch(run);
|
|
148
|
-
if (typeof raw === "string" && raw.trim().length > 0)
|
|
189
|
+
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
190
|
+
if (looksLikeFileNotBranch(raw)) {
|
|
191
|
+
throw new UsageError(`"${raw}" looks like a file, not a branch name — did you mean ` +
|
|
192
|
+
`"uploads attach ${raw} --branch" (auto-detect the current branch), ` +
|
|
193
|
+
`or "uploads attach --branch <name> ${raw}" (explicit branch name)?`);
|
|
194
|
+
}
|
|
149
195
|
return raw;
|
|
196
|
+
}
|
|
150
197
|
throw new UsageError("--branch requires a non-empty branch name");
|
|
151
198
|
}
|
|
152
199
|
/**
|
|
@@ -297,15 +344,39 @@ export function frameOptionsFromFlags(flags) {
|
|
|
297
344
|
export function commentViaSuffix(via) {
|
|
298
345
|
return via === "bot" ? " (uploads-sh[bot])" : " (via gh)";
|
|
299
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Thrown by `syncAttachmentsComment` when the server declines with
|
|
349
|
+
* `not_authorized` (issue #297 baseline control) — this repo is bound to a
|
|
350
|
+
* different workspace, or unbound and unclaimable by the communal `default`
|
|
351
|
+
* workspace. Deliberately not caught by the generic "bot endpoint
|
|
352
|
+
* unreachable" fallback below: falling back to gh here would let the
|
|
353
|
+
* human's own credentials post anyway, defeating the point of the
|
|
354
|
+
* server-side gate.
|
|
355
|
+
*/
|
|
356
|
+
export class GithubCommentAuthorizationError extends Error {
|
|
357
|
+
}
|
|
300
358
|
export async function syncAttachmentsComment(client, target, run, workspace) {
|
|
359
|
+
let bot;
|
|
301
360
|
try {
|
|
302
|
-
|
|
361
|
+
bot = await client.upsertGithubComment({
|
|
303
362
|
repo: target.repo,
|
|
304
363
|
num: target.num,
|
|
305
364
|
kind: target.kind,
|
|
306
365
|
});
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// Endpoint absent/unreachable (self-hosted, network, older worker) — fall
|
|
369
|
+
// through to the gh path below.
|
|
370
|
+
bot = undefined;
|
|
371
|
+
}
|
|
372
|
+
if (bot) {
|
|
307
373
|
if (bot.posted)
|
|
308
374
|
return { action: bot.action, count: bot.count, via: "bot" };
|
|
375
|
+
if (bot.reason === "not_authorized") {
|
|
376
|
+
throw new GithubCommentAuthorizationError(`${bot.message ?? `${target.repo} is not authorized for this workspace.`}\n` +
|
|
377
|
+
`Run \`uploads github link --status --repo ${target.repo}\` to see who owns the ` +
|
|
378
|
+
`binding, use that workspace instead, or post the comment manually with gh.`);
|
|
379
|
+
}
|
|
309
380
|
// Installed-but-unapproved is a fixable misconfiguration, not a silent
|
|
310
381
|
// degrade: tell the user (and how to fix it) before falling back to gh.
|
|
311
382
|
if (bot.reason === "forbidden" && bot.message) {
|
|
@@ -313,10 +384,6 @@ export async function syncAttachmentsComment(client, target, run, workspace) {
|
|
|
313
384
|
`Posting via local gh in the meantime.\n`);
|
|
314
385
|
}
|
|
315
386
|
}
|
|
316
|
-
catch {
|
|
317
|
-
// Endpoint absent/unreachable (self-hosted, network, older worker) — fall
|
|
318
|
-
// through to the gh path below.
|
|
319
|
-
}
|
|
320
387
|
// gh fallback: gather from this workspace's own data and post via local `gh`.
|
|
321
388
|
// Note (issue #304): this CLI process has no server-side WorkspaceRecord in
|
|
322
389
|
// scope, so it cannot honor a workspace's githubCommentLinkToFilePage=false
|
|
@@ -664,11 +731,15 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
664
731
|
throw new UsageError("--promote cannot be combined with --no-promote");
|
|
665
732
|
return runAttachPromoteOnly(ctx, parsed, run);
|
|
666
733
|
}
|
|
734
|
+
// Validate --branch (including the filename-lookahead guard) before the
|
|
735
|
+
// zero-positionals bailout below — otherwise `uploads attach --branch
|
|
736
|
+
// shot.png` (where shot.png is swallowed as the branch value, leaving no
|
|
737
|
+
// file args) would silently print help instead of a clear UsageError.
|
|
738
|
+
const branchArg = branchFromFlags(parsed.flags, run);
|
|
667
739
|
if (parsed.positionals.length === 0) {
|
|
668
740
|
writeCommandHelp(ATTACH_HELP);
|
|
669
741
|
return 2;
|
|
670
742
|
}
|
|
671
|
-
const branchArg = branchFromFlags(parsed.flags, run);
|
|
672
743
|
if (branchArg !== undefined) {
|
|
673
744
|
if (parsed.flags.has("--pr"))
|
|
674
745
|
throw new UsageError("--branch cannot be combined with --pr");
|
|
@@ -1623,6 +1694,11 @@ App is installed on the repo; otherwise via your local gh auth. Finds its own
|
|
|
1623
1694
|
prior comment via a hidden marker and edits it in place; never touches other
|
|
1624
1695
|
comments or the description.
|
|
1625
1696
|
|
|
1697
|
+
If this repo is bound to a different workspace (or unbound and you're on the
|
|
1698
|
+
communal "default" workspace), the bot post is declined and this command
|
|
1699
|
+
fails rather than silently falling back to gh — see \`uploads github link
|
|
1700
|
+
--status\`.
|
|
1701
|
+
|
|
1626
1702
|
Examples:
|
|
1627
1703
|
uploads --env-file .env comment --pr 123
|
|
1628
1704
|
uploads comment --issue 45 --repo buildinternet/uploads
|
|
@@ -1650,38 +1726,73 @@ export async function runComment(ctx, args, help = false, run = execRunner) {
|
|
|
1650
1726
|
}
|
|
1651
1727
|
// --- github link ---
|
|
1652
1728
|
const GITHUB_HELP = `uploads github link [--repo <owner/name>] [--status] [--workspace <name>]
|
|
1729
|
+
uploads github unlink [--repo <owner/name>] [--workspace <name>]
|
|
1730
|
+
uploads github doctor [--workspace <name>]
|
|
1653
1731
|
|
|
1654
|
-
Claim or
|
|
1655
|
-
attachments comment / webhook auto-promotion, which use this
|
|
1656
|
-
First-claim-wins: claiming an already-bound repo never steals it
|
|
1657
|
-
whichever workspace claimed it first — the command reports who owns it
|
|
1658
|
-
instead.
|
|
1732
|
+
Claim, inspect, or release this workspace's binding to a GitHub repo (see the
|
|
1733
|
+
managed attachments comment / webhook auto-promotion, which use this
|
|
1734
|
+
binding). First-claim-wins: claiming an already-bound repo never steals it
|
|
1735
|
+
from whichever workspace claimed it first — the command reports who owns it,
|
|
1736
|
+
and how to get it released, instead.
|
|
1659
1737
|
|
|
1660
1738
|
--repo defaults the same way as --pr/--issue elsewhere (gh repo view, then
|
|
1661
1739
|
the git remote). --status only inspects the current binding (files:read);
|
|
1662
|
-
without it,
|
|
1740
|
+
without it, "link" claims the repo (files:write). "unlink" releases a
|
|
1741
|
+
binding this workspace owns — it 403s (via the server) if another workspace
|
|
1742
|
+
owns it; an operator can reassign or remove that binding instead.
|
|
1743
|
+
|
|
1744
|
+
\`doctor\` checks the GitHub App itself: whether it's configured on the
|
|
1745
|
+
server, and whether it's subscribed to the webhook events uploads.sh's
|
|
1746
|
+
handler needs (issues, pull_request — see docs/github-app). A missing
|
|
1747
|
+
subscription is the classic silent failure: the App's ping stays green
|
|
1748
|
+
while webhook auto-promotion and title-cache invalidation quietly do
|
|
1749
|
+
nothing.
|
|
1663
1750
|
|
|
1664
1751
|
Examples:
|
|
1665
1752
|
uploads github link
|
|
1666
1753
|
uploads github link --repo buildinternet/uploads
|
|
1667
1754
|
uploads github link --status
|
|
1755
|
+
uploads github unlink --repo buildinternet/uploads
|
|
1756
|
+
uploads github doctor
|
|
1668
1757
|
`;
|
|
1758
|
+
function formatGithubDoctor(result) {
|
|
1759
|
+
if (!result.configured) {
|
|
1760
|
+
return `github app: not configured on this server${result.hint ? ` — ${result.hint}` : ""}\n`;
|
|
1761
|
+
}
|
|
1762
|
+
if (result.events === null) {
|
|
1763
|
+
return `github app: configured, but health check failed${result.hint ? ` — ${result.hint}` : ""}\n`;
|
|
1764
|
+
}
|
|
1765
|
+
if (result.ok) {
|
|
1766
|
+
return `github app: ok — subscribed to ${result.requiredEvents.join(", ")}\n`;
|
|
1767
|
+
}
|
|
1768
|
+
return (`github app: missing webhook event subscription(s): ${result.missingEvents.join(", ")}\n` +
|
|
1769
|
+
(result.hint ? ` ${result.hint}\n` : ""));
|
|
1770
|
+
}
|
|
1669
1771
|
function formatGithubLink(repo, result) {
|
|
1670
1772
|
return result.workspace
|
|
1671
1773
|
? `${repo} is bound to workspace "${result.workspace}"${result.source ? ` (${result.source})` : ""}\n`
|
|
1672
1774
|
: `${repo} is not bound to any workspace\n`;
|
|
1673
1775
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
writeCommandHelp(GITHUB_HELP);
|
|
1679
|
-
return help || parsed.help ? 0 : 2;
|
|
1776
|
+
async function runGithubDoctor(ctx) {
|
|
1777
|
+
let result;
|
|
1778
|
+
try {
|
|
1779
|
+
result = await ctx.client.githubHealth();
|
|
1680
1780
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1781
|
+
catch (err) {
|
|
1782
|
+
if (err instanceof UploadsError && err.status === 404) {
|
|
1783
|
+
throw new UsageError("server does not support the GitHub App health check yet (404) — upgrade the uploads.sh API/self-hosted worker");
|
|
1784
|
+
}
|
|
1785
|
+
throw err;
|
|
1786
|
+
}
|
|
1787
|
+
if (ctx.json) {
|
|
1788
|
+
await writeJson(result);
|
|
1789
|
+
}
|
|
1790
|
+
else {
|
|
1791
|
+
await writeStdout(formatGithubDoctor(result));
|
|
1792
|
+
}
|
|
1793
|
+
return result.ok ? 0 : 1;
|
|
1794
|
+
}
|
|
1795
|
+
async function runGithubLink(ctx, repo, statusOnly) {
|
|
1685
1796
|
let result;
|
|
1686
1797
|
try {
|
|
1687
1798
|
result = statusOnly
|
|
@@ -1699,11 +1810,52 @@ export async function runGithub(ctx, args, help = false, run = execRunner) {
|
|
|
1699
1810
|
return 0;
|
|
1700
1811
|
}
|
|
1701
1812
|
if (!statusOnly && result.claimed === false) {
|
|
1702
|
-
process.stderr.write(`note: ${repo} is already bound to a different workspace ("${result.workspace}") — first-claim-wins, not overwritten
|
|
1813
|
+
process.stderr.write(`note: ${repo} is already bound to a different workspace ("${result.workspace}") — first-claim-wins, not overwritten. Run "uploads github unlink --repo ${repo}" from that workspace, or ask an operator to reassign it.\n`);
|
|
1703
1814
|
}
|
|
1704
1815
|
await writeStdout(formatGithubLink(repo, result));
|
|
1705
1816
|
return 0;
|
|
1706
1817
|
}
|
|
1818
|
+
async function runGithubUnlink(ctx, repo) {
|
|
1819
|
+
let result;
|
|
1820
|
+
try {
|
|
1821
|
+
result = await ctx.client.githubLinkUnlink(repo);
|
|
1822
|
+
}
|
|
1823
|
+
catch (err) {
|
|
1824
|
+
if (err instanceof UploadsError && err.status === 404) {
|
|
1825
|
+
throw new UsageError("server does not support repo bindings yet (404) — upgrade the uploads.sh API/self-hosted worker");
|
|
1826
|
+
}
|
|
1827
|
+
if (err instanceof UploadsError && err.status === 403) {
|
|
1828
|
+
throw new UsageError(`${repo} is bound to a different workspace — ask an operator to reassign or remove it (${err.message})`);
|
|
1829
|
+
}
|
|
1830
|
+
throw err;
|
|
1831
|
+
}
|
|
1832
|
+
if (ctx.json) {
|
|
1833
|
+
await writeJson(result);
|
|
1834
|
+
return 0;
|
|
1835
|
+
}
|
|
1836
|
+
await writeStdout(result.unlinked
|
|
1837
|
+
? `unlinked ${repo}\n`
|
|
1838
|
+
: `${repo} was not bound to any workspace — nothing to unlink\n`);
|
|
1839
|
+
return 0;
|
|
1840
|
+
}
|
|
1841
|
+
export async function runGithub(ctx, args, help = false, run = execRunner) {
|
|
1842
|
+
const parsed = parseCommandArgs(args);
|
|
1843
|
+
const action = parsed.positionals[0];
|
|
1844
|
+
if (help || parsed.help || !action) {
|
|
1845
|
+
writeCommandHelp(GITHUB_HELP);
|
|
1846
|
+
return help || parsed.help ? 0 : 2;
|
|
1847
|
+
}
|
|
1848
|
+
if (action !== "link" && action !== "unlink" && action !== "doctor") {
|
|
1849
|
+
throw new UsageError(`unknown github subcommand: ${action}`);
|
|
1850
|
+
}
|
|
1851
|
+
if (action === "doctor")
|
|
1852
|
+
return runGithubDoctor(ctx);
|
|
1853
|
+
const repo = resolveRepo(flagString(parsed.flags, "--repo"), run);
|
|
1854
|
+
if (action === "unlink")
|
|
1855
|
+
return runGithubUnlink(ctx, repo);
|
|
1856
|
+
const statusOnly = flagBool(parsed.flags, "--status");
|
|
1857
|
+
return runGithubLink(ctx, repo, statusOnly);
|
|
1858
|
+
}
|
|
1707
1859
|
// --- usage / reconcile / purge ---
|
|
1708
1860
|
const USAGE_HELP = `uploads usage [--workspace <name>]
|
|
1709
1861
|
|