@buildinternet/uploads 0.50.0 → 0.51.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/dist/cli-catalog.js +1 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.js +21 -0
- package/dist/commands/screenshot.js +17 -2
- package/dist/commands.d.ts +27 -1
- package/dist/commands.js +120 -33
- package/dist/github-gh.d.ts +25 -0
- package/dist/github-gh.js +49 -0
- package/dist/mcp/tools.js +65 -10
- package/package.json +1 -1
package/dist/cli-catalog.js
CHANGED
package/dist/client.d.ts
CHANGED
|
@@ -326,6 +326,22 @@ export interface PromoteSkip {
|
|
|
326
326
|
export interface PromoteBranchAttachmentsResult {
|
|
327
327
|
promoted: string[];
|
|
328
328
|
skipped: PromoteSkip[];
|
|
329
|
+
/** Branch-name lineage the sweep covered, current name first (issue #920).
|
|
330
|
+
* Present only when the branch was renamed at least once. */
|
|
331
|
+
lineage?: string[];
|
|
332
|
+
}
|
|
333
|
+
/** `POST /v1/workspaces/:workspace/github/branch-rename` request (server contract, issue #920). */
|
|
334
|
+
export interface RegisterBranchRenameOptions {
|
|
335
|
+
repo: string;
|
|
336
|
+
/** Previous branch name (`git branch -m <from> <to>`). */
|
|
337
|
+
from: string;
|
|
338
|
+
/** Name the branch was renamed to. */
|
|
339
|
+
to: string;
|
|
340
|
+
}
|
|
341
|
+
/** `POST /v1/workspaces/:workspace/github/branch-rename` response. `recorded:
|
|
342
|
+
* false` means the pair was already known (or the server has no such route). */
|
|
343
|
+
export interface RegisterBranchRenameResult {
|
|
344
|
+
recorded: boolean;
|
|
329
345
|
}
|
|
330
346
|
/**
|
|
331
347
|
* `POST /v1/workspaces/:workspace/github/attach` request/response (server
|
|
@@ -758,6 +774,15 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
758
774
|
* that doesn't have this route yet, as "nothing promoted").
|
|
759
775
|
*/
|
|
760
776
|
promoteBranchAttachments(opts: PromoteBranchAttachmentsOptions): Promise<PromoteBranchAttachmentsResult>;
|
|
777
|
+
/**
|
|
778
|
+
* Record one `git branch -m` step so promote sweeps the branch's whole
|
|
779
|
+
* name lineage (issue #920). Degrade-safe on the one failure that is
|
|
780
|
+
* expected in the wild: a 404 from an older/self-hosted worker without
|
|
781
|
+
* this route collapses to `{ recorded: false }` — nothing recorded, same
|
|
782
|
+
* as `promoteBranchAttachments`' callers treat a missing route. Other
|
|
783
|
+
* failures throw; the CLI's `registerRenamesBestEffort` swallows those.
|
|
784
|
+
*/
|
|
785
|
+
registerBranchRename(opts: RegisterBranchRenameOptions): Promise<RegisterBranchRenameResult>;
|
|
761
786
|
/**
|
|
762
787
|
* Attach an already-uploaded object to a PR/issue via a server-side copy
|
|
763
788
|
* (issue #702) — see `AttachExistingOptions`. Throws `UploadsError` on
|
package/dist/client.js
CHANGED
|
@@ -724,6 +724,27 @@ export function createUploadsClient(config) {
|
|
|
724
724
|
headers: { "Content-Type": "application/json" },
|
|
725
725
|
});
|
|
726
726
|
},
|
|
727
|
+
/**
|
|
728
|
+
* Record one `git branch -m` step so promote sweeps the branch's whole
|
|
729
|
+
* name lineage (issue #920). Degrade-safe on the one failure that is
|
|
730
|
+
* expected in the wild: a 404 from an older/self-hosted worker without
|
|
731
|
+
* this route collapses to `{ recorded: false }` — nothing recorded, same
|
|
732
|
+
* as `promoteBranchAttachments`' callers treat a missing route. Other
|
|
733
|
+
* failures throw; the CLI's `registerRenamesBestEffort` swallows those.
|
|
734
|
+
*/
|
|
735
|
+
async registerBranchRename(opts) {
|
|
736
|
+
try {
|
|
737
|
+
return await request("POST", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/branch-rename`, {
|
|
738
|
+
body: new TextEncoder().encode(JSON.stringify(opts)),
|
|
739
|
+
headers: { "Content-Type": "application/json" },
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
catch (err) {
|
|
743
|
+
if (err instanceof UploadsError && err.status === 404)
|
|
744
|
+
return { recorded: false };
|
|
745
|
+
throw err;
|
|
746
|
+
}
|
|
747
|
+
},
|
|
727
748
|
/**
|
|
728
749
|
* Attach an already-uploaded object to a PR/issue via a server-side copy
|
|
729
750
|
* (issue #702) — see `AttachExistingOptions`. Throws `UploadsError` on
|
|
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { extractDashValue, flagBool, flagInt, flagString, flagValues, parseCommandArgs, UsageError, } from "../cli-args.js";
|
|
4
4
|
import { writeCommandHelp } from "../cli-style.js";
|
|
5
|
-
import { branchFromFlags, derivedMetaEnabled, frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, stateAppMetaFromFlags, warnNearMissMeta, syncAttachmentsComment, commentViaSuffix, uploadPreparedImage, resolvePutStagingTarget, putStagingNoteText, resolveStageBindingWarning, mergeStagingMeta, writeReplacedNote, resolveGhPrefixSafe, resolveAutoPrTarget, resolvePutNudgeContext, putNudgeText, autoPrNoteText, } from "../commands.js";
|
|
5
|
+
import { branchFromFlags, derivedMetaEnabled, frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, stateAppMetaFromFlags, warnNearMissMeta, syncAttachmentsComment, commentViaSuffix, uploadPreparedImage, resolvePutStagingTarget, putStagingNoteText, resolveStageBindingWarning, mergeStagingMeta, registerRenamesBestEffort, writeReplacedNote, resolveGhPrefixSafe, resolveAutoPrTarget, resolvePutNudgeContext, putNudgeText, autoPrNoteText, } from "../commands.js";
|
|
6
6
|
import { resolvePutDefaults } from "../config.js";
|
|
7
7
|
import { loadDefaultsRaw, resolveScreenshotDefaults } from "../config-file.js";
|
|
8
8
|
import { resolvePutPrefix } from "../destinations.js";
|
|
@@ -351,7 +351,7 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
|
|
|
351
351
|
// maps to exactly one open PR behaves as if --pr <n> had been passed —
|
|
352
352
|
// stable key + managed comment sync — instead of the #469 auto-staging
|
|
353
353
|
// default below. Mirrors put's #700 handling exactly (resolveAutoPrTarget).
|
|
354
|
-
const
|
|
354
|
+
const autoPrMatch = ghTarget || branchArg !== undefined
|
|
355
355
|
? undefined
|
|
356
356
|
: resolveAutoPrTarget({
|
|
357
357
|
ghTarget,
|
|
@@ -365,6 +365,7 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
|
|
|
365
365
|
repoArg: flagString(parsed.flags, "--repo") ?? putDefaults.repo,
|
|
366
366
|
run,
|
|
367
367
|
});
|
|
368
|
+
const autoPrTarget = autoPrMatch?.target;
|
|
368
369
|
const effectiveGhTarget = ghTarget ?? autoPrTarget;
|
|
369
370
|
// Auto branch staging (issue #469 lever 1): mirrors bare `put`'s auto-staging
|
|
370
371
|
// (issue #403). When no --branch/--pr/--issue/--key/--ref/--prefix/--destination
|
|
@@ -428,9 +429,23 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
|
|
|
428
429
|
if (effectiveGhTarget) {
|
|
429
430
|
metadata = { ...withFacts, ...ghMetadataFromTargetWithTitle(effectiveGhTarget, run) };
|
|
430
431
|
validateMetaMap(metadata);
|
|
432
|
+
// The #700 auto-PR match suppresses staging, so the staging branch below
|
|
433
|
+
// never runs — but this capture still comes from a branch that may have
|
|
434
|
+
// been renamed while files were staged under the old name. Register the
|
|
435
|
+
// lineage here too (issue #920), reusing the branch the match already
|
|
436
|
+
// resolved. Only for the auto-PR match: an explicit --pr/--issue names
|
|
437
|
+
// no branch of its own.
|
|
438
|
+
if (autoPrMatch && !dryRun && !noUpload) {
|
|
439
|
+
await registerRenamesBestEffort(ctx.client, run, autoPrMatch.target.repo, autoPrMatch.branch);
|
|
440
|
+
}
|
|
431
441
|
}
|
|
432
442
|
else if (stagingTarget !== undefined) {
|
|
433
443
|
metadata = mergeStagingMeta(withFacts, stagingTarget);
|
|
444
|
+
// Same rename registration as `put`/`attach --branch` staging (issue
|
|
445
|
+
// #920): best-effort, never fails the capture.
|
|
446
|
+
if (!dryRun && !noUpload) {
|
|
447
|
+
await registerRenamesBestEffort(ctx.client, run, stagingTarget.repo, stagingTarget.branch);
|
|
448
|
+
}
|
|
434
449
|
}
|
|
435
450
|
else if (Object.keys(withFacts).length > 0) {
|
|
436
451
|
validateMetaMap(withFacts);
|
package/dist/commands.d.ts
CHANGED
|
@@ -394,6 +394,23 @@ export declare function uploadPuts(opts: {
|
|
|
394
394
|
width?: number;
|
|
395
395
|
concurrency?: number;
|
|
396
396
|
}): Promise<UploadBatchResult<PutUploadItem>>;
|
|
397
|
+
/**
|
|
398
|
+
* Best-effort branch-rename registration (issue #920): reads this branch's
|
|
399
|
+
* reflog for `git branch -m` steps and tells the server about each one, so a
|
|
400
|
+
* later promote sweeps the branch's whole name lineage instead of only its
|
|
401
|
+
* current name. No-op when the reflog has no rename (the common case) and
|
|
402
|
+
* when the client predates the route. Every failure is swallowed — this runs
|
|
403
|
+
* alongside staging and promote, and must never fail either. Set
|
|
404
|
+
* `UPLOADS_DEBUG=1` to see what was skipped.
|
|
405
|
+
*
|
|
406
|
+
* `opts.explicit` marks a branch the user named themselves (`--from-branch`
|
|
407
|
+
* / the MCP `fromBranch` argument): that is the manual escape hatch (PR
|
|
408
|
+
* #919), and its lineage belongs to a different name than the one we are
|
|
409
|
+
* standing on, so nothing is registered for it.
|
|
410
|
+
*/
|
|
411
|
+
export declare function registerRenamesBestEffort(client: UploadsClient, run: CommandRunner, repo: string, branch: string, opts?: {
|
|
412
|
+
explicit?: boolean;
|
|
413
|
+
}): Promise<void>;
|
|
397
414
|
export declare function runAttach(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
|
|
398
415
|
/**
|
|
399
416
|
* One source of truth for the "staged, but not going to auto-attach" advisory
|
|
@@ -465,6 +482,11 @@ export declare function resolvePutNudgeContext(opts: {
|
|
|
465
482
|
branch: string;
|
|
466
483
|
pr: number | undefined;
|
|
467
484
|
} | undefined;
|
|
485
|
+
/** A #700 auto-PR match: the PR the current branch maps to, plus that branch. */
|
|
486
|
+
export interface AutoPrMatch {
|
|
487
|
+
target: GhTarget;
|
|
488
|
+
branch: string;
|
|
489
|
+
}
|
|
468
490
|
/**
|
|
469
491
|
* Auto-PR context (issue #700): when a bare put/screenshot has no explicit
|
|
470
492
|
* destination flag at all (`--pr`/`--issue`/`--key`/`--ref`/`--prefix`/
|
|
@@ -481,6 +503,10 @@ export declare function resolvePutNudgeContext(opts: {
|
|
|
481
503
|
* default branch, or with `--no-git`; any failure (not a repo, detached
|
|
482
504
|
* HEAD, gh missing/unauthenticated/timed out, no open PR) degrades to
|
|
483
505
|
* undefined so the caller falls back to its normal staging/dated behavior.
|
|
506
|
+
*
|
|
507
|
+
* The match carries the `branch` it was resolved from, so callers that need
|
|
508
|
+
* the current branch (the #920 rename registration) reuse it instead of
|
|
509
|
+
* spawning `git rev-parse` a second time.
|
|
484
510
|
*/
|
|
485
511
|
export declare function resolveAutoPrTarget(opts: {
|
|
486
512
|
ghTarget: GhTarget | undefined;
|
|
@@ -495,7 +521,7 @@ export declare function resolveAutoPrTarget(opts: {
|
|
|
495
521
|
noAutoPr: boolean;
|
|
496
522
|
repoArg: string | undefined;
|
|
497
523
|
run: CommandRunner;
|
|
498
|
-
}):
|
|
524
|
+
}): AutoPrMatch | undefined;
|
|
499
525
|
/**
|
|
500
526
|
* Bare-put branch-staging trigger (issue #403): put on a non-default git
|
|
501
527
|
* branch stages to the branch prefix by default — the branch becomes the
|
package/dist/commands.js
CHANGED
|
@@ -15,7 +15,7 @@ import { parseMetaFlags, validateMetaMap } from "./metadata.js";
|
|
|
15
15
|
import { mergeDerivedMeta, nearMissMetaWarnings, validateStateValue } from "./metadata-vocab.js";
|
|
16
16
|
import { mergeSidecarMeta } from "./sidecar.js";
|
|
17
17
|
import { ghAttachmentKeyForMode, ghBranchAttachmentKeyForMode, ghBranchKeyPrefix, ghKeyPrefix, ghPrivateKeyPrefix, ghPrivateBranchKeyPrefix, ghMetadataFromTarget, parseGhKey, parseGhPrivateKey, ghMetadataForBranch, attachmentsCommentBody, attachmentsMarker, AUTO_RENDER_OPTIONS, GH_FALLBACK_AUTHOR_NOTE, normalizeGithubCoordinate, } from "./github.js";
|
|
18
|
-
import { resolveRepo, resolveCurrentPullRequest, resolveCurrentBranch, resolveDefaultBranch, classifyGhNumber, execRunner, timedExecRunner, ghMetadataFromTargetWithTitle, upsertAttachmentsComment, hasLinkCandidate, extractCandidateUrls, fetchAdoptionCandidateText, } from "./github-gh.js";
|
|
18
|
+
import { resolveRepo, resolveCurrentPullRequest, resolveCurrentBranch, resolveCurrentBranchSafe, resolveDefaultBranch, classifyGhNumber, execRunner, timedExecRunner, ghMetadataFromTargetWithTitle, upsertAttachmentsComment, hasLinkCandidate, extractCandidateUrls, fetchAdoptionCandidateText, renameLineageFromReflog, } from "./github-gh.js";
|
|
19
19
|
import { deriveRepoFromGit, deriveRepoSlugFromGit } from "./keys.js";
|
|
20
20
|
import { noProjectContextNudge } from "./project-context-nudge.js";
|
|
21
21
|
import { resolvePutPrefix } from "./destinations.js";
|
|
@@ -809,6 +809,14 @@ takes effect with zero files and cannot combine with --branch/--issue/
|
|
|
809
809
|
--no-promote. Promotion never applies to issues. Staged files stay findable
|
|
810
810
|
with "uploads find gh.branch=<branch>" either way.
|
|
811
811
|
|
|
812
|
+
A branch renamed with "git branch -m" is followed automatically: the rename
|
|
813
|
+
is read from the branch reflog and registered, so promotion sweeps the older
|
|
814
|
+
names too. That needs one uploads run after the rename. If the branch was
|
|
815
|
+
renamed without one, or was deleted, pass "--from-branch <old-name>" with
|
|
816
|
+
"--pr <num>". With no file arguments, this promotes the stale branch prefix
|
|
817
|
+
and refreshes the managed comment. With file or existing-key arguments, it
|
|
818
|
+
promotes the stale prefix before the normal attach flow.
|
|
819
|
+
|
|
812
820
|
Options:
|
|
813
821
|
--pr <num> Attach to this pull request
|
|
814
822
|
--issue <num> Attach to this issue
|
|
@@ -817,6 +825,8 @@ Options:
|
|
|
817
825
|
--promote No files: promote branch-staged attachments into the
|
|
818
826
|
resolved PR and refresh the comment; not with
|
|
819
827
|
--branch/--issue/--no-promote
|
|
828
|
+
--from-branch <name> Promote staged attachments from this branch instead of
|
|
829
|
+
the current branch; requires a pull-request target
|
|
820
830
|
--no-promote Skip auto-promoting branch-staged attachments (default path only)
|
|
821
831
|
--move With an already-uploaded key/URL argument: delete the source
|
|
822
832
|
object after a successful server-side copy (default: copy)
|
|
@@ -848,6 +858,7 @@ Examples:
|
|
|
848
858
|
uploads attach ./artifact.zip --issue 45 --no-comment
|
|
849
859
|
uploads attach ./shot.png --meta path=/settings --state after
|
|
850
860
|
uploads attach ./shot.png --branch
|
|
861
|
+
uploads attach --pr 123 --from-branch old/branch
|
|
851
862
|
uploads attach ./shot.png --branch feature/new-settings
|
|
852
863
|
uploads attach --promote
|
|
853
864
|
`;
|
|
@@ -1165,6 +1176,45 @@ export async function uploadPuts(opts) {
|
|
|
1165
1176
|
}
|
|
1166
1177
|
return { uploads, failures, firstError, sentMetadata };
|
|
1167
1178
|
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Best-effort branch-rename registration (issue #920): reads this branch's
|
|
1181
|
+
* reflog for `git branch -m` steps and tells the server about each one, so a
|
|
1182
|
+
* later promote sweeps the branch's whole name lineage instead of only its
|
|
1183
|
+
* current name. No-op when the reflog has no rename (the common case) and
|
|
1184
|
+
* when the client predates the route. Every failure is swallowed — this runs
|
|
1185
|
+
* alongside staging and promote, and must never fail either. Set
|
|
1186
|
+
* `UPLOADS_DEBUG=1` to see what was skipped.
|
|
1187
|
+
*
|
|
1188
|
+
* `opts.explicit` marks a branch the user named themselves (`--from-branch`
|
|
1189
|
+
* / the MCP `fromBranch` argument): that is the manual escape hatch (PR
|
|
1190
|
+
* #919), and its lineage belongs to a different name than the one we are
|
|
1191
|
+
* standing on, so nothing is registered for it.
|
|
1192
|
+
*/
|
|
1193
|
+
export async function registerRenamesBestEffort(client, run, repo, branch, opts = {}) {
|
|
1194
|
+
if (opts.explicit)
|
|
1195
|
+
return;
|
|
1196
|
+
let lineage;
|
|
1197
|
+
try {
|
|
1198
|
+
lineage = renameLineageFromReflog(run, branch);
|
|
1199
|
+
}
|
|
1200
|
+
catch {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
if (lineage.length === 0)
|
|
1204
|
+
return;
|
|
1205
|
+
for (const step of lineage) {
|
|
1206
|
+
try {
|
|
1207
|
+
await client.registerBranchRename({ repo, from: step.from, to: step.to });
|
|
1208
|
+
}
|
|
1209
|
+
catch (err) {
|
|
1210
|
+
if (process.env.UPLOADS_DEBUG === "1") {
|
|
1211
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1212
|
+
process.stderr.write(`debug: could not register branch rename ${step.from} -> ${step.to}: ${detail}\n`);
|
|
1213
|
+
}
|
|
1214
|
+
return; // one failure means the route is unavailable; don't retry the rest
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1168
1218
|
/**
|
|
1169
1219
|
* Best-effort call to `POST /v1/workspaces/:workspace/github/promote` (server contract,
|
|
1170
1220
|
* PR #310). Degrade-safe like `syncAttachmentsComment`'s bot path: an older
|
|
@@ -1180,11 +1230,20 @@ async function attemptPromoteBranch(client, target, branch) {
|
|
|
1180
1230
|
return undefined;
|
|
1181
1231
|
}
|
|
1182
1232
|
}
|
|
1183
|
-
/**
|
|
1233
|
+
/**
|
|
1234
|
+
* Human-mode note(s) for a promotion that actually promoted something. When
|
|
1235
|
+
* the server-side sweep followed a rename it adds a second line naming the
|
|
1236
|
+
* older branch names (issue #920): `lineage` is current-name-first, so the
|
|
1237
|
+
* older names are its tail.
|
|
1238
|
+
*/
|
|
1184
1239
|
function promotionNote(promotion, branch) {
|
|
1185
1240
|
const n = promotion.promoted.length;
|
|
1186
1241
|
const branchSuffix = branch ? ` from branch ${branch}` : "";
|
|
1187
|
-
|
|
1242
|
+
const promoted = `>> promoted ${n} staged attachment${n === 1 ? "" : "s"}${branchSuffix}\n`;
|
|
1243
|
+
const older = (promotion.lineage ?? []).slice(1);
|
|
1244
|
+
if (older.length === 0)
|
|
1245
|
+
return promoted;
|
|
1246
|
+
return `${promoted}>> followed rename from ${older.join(", ")}\n`;
|
|
1188
1247
|
}
|
|
1189
1248
|
export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
1190
1249
|
const parsed = parseCommandArgs(args);
|
|
@@ -1204,7 +1263,12 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
1204
1263
|
if (parsed.flags.has("--move") && typeof parsed.flags.get("--move") === "string") {
|
|
1205
1264
|
throw new UsageError("--move takes no value — place it after the file arguments");
|
|
1206
1265
|
}
|
|
1207
|
-
if (parsed.flags.has("--
|
|
1266
|
+
if (parsed.flags.has("--from-branch") && !flagString(parsed.flags, "--from-branch")) {
|
|
1267
|
+
throw new UsageError("--from-branch requires a branch name");
|
|
1268
|
+
}
|
|
1269
|
+
const fromBranch = flagString(parsed.flags, "--from-branch");
|
|
1270
|
+
if (parsed.flags.has("--promote") ||
|
|
1271
|
+
(fromBranch !== undefined && parsed.positionals.length === 0)) {
|
|
1208
1272
|
if (parsed.positionals.length > 0) {
|
|
1209
1273
|
throw new UsageError("--promote takes no file arguments — attaching a file to a PR already auto-promotes " +
|
|
1210
1274
|
"staged files; use `uploads attach <file> --pr <num>` instead");
|
|
@@ -1236,6 +1300,12 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
1236
1300
|
throw new UsageError("--branch cannot be combined with --comment");
|
|
1237
1301
|
return runAttachBranch(ctx, parsed, branchArg, run);
|
|
1238
1302
|
}
|
|
1303
|
+
if (fromBranch !== undefined && parsed.flags.has("--issue")) {
|
|
1304
|
+
throw new UsageError("--from-branch cannot be combined with --issue");
|
|
1305
|
+
}
|
|
1306
|
+
if (fromBranch !== undefined && parsed.flags.has("--no-promote")) {
|
|
1307
|
+
throw new UsageError("--from-branch cannot be combined with --no-promote");
|
|
1308
|
+
}
|
|
1239
1309
|
const explicitTarget = ghTargetFromFlags(parsed.flags, run);
|
|
1240
1310
|
const target = explicitTarget ??
|
|
1241
1311
|
resolveCurrentPullRequest(resolveRepo(flagString(parsed.flags, "--repo"), run), run);
|
|
@@ -1324,20 +1394,20 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
1324
1394
|
let promotion;
|
|
1325
1395
|
let promotedBranch;
|
|
1326
1396
|
if (target.kind === "pull" && !parsed.flags.has("--no-promote")) {
|
|
1327
|
-
|
|
1328
|
-
promotedBranch = resolveCurrentBranch(run);
|
|
1329
|
-
}
|
|
1330
|
-
catch {
|
|
1331
|
-
promotedBranch = undefined;
|
|
1332
|
-
}
|
|
1397
|
+
promotedBranch = fromBranch ?? resolveCurrentBranchSafe(run);
|
|
1333
1398
|
if (promotedBranch !== undefined) {
|
|
1399
|
+
await registerRenamesBestEffort(ctx.client, run, target.repo, promotedBranch, {
|
|
1400
|
+
explicit: fromBranch !== undefined,
|
|
1401
|
+
});
|
|
1334
1402
|
promotion = await attemptPromoteBranch(ctx.client, target, promotedBranch);
|
|
1335
1403
|
}
|
|
1336
1404
|
}
|
|
1337
1405
|
let comment;
|
|
1338
1406
|
let commentError;
|
|
1339
|
-
//
|
|
1340
|
-
|
|
1407
|
+
// Existing-key attach syncs server-side, but a stale-branch promotion runs
|
|
1408
|
+
// afterward. Refresh once more so that copy is visible in the same command.
|
|
1409
|
+
const shouldRefreshComment = uploads.length > 0 || (fromBranch !== undefined && attachedExisting.length > 0);
|
|
1410
|
+
if (!parsed.flags.has("--no-comment") && shouldRefreshComment) {
|
|
1341
1411
|
try {
|
|
1342
1412
|
comment = await syncAttachmentsComment(ctx.client, target, run, ctx.config.workspace);
|
|
1343
1413
|
}
|
|
@@ -1424,6 +1494,9 @@ async function runAttachBranch(ctx, parsed, branch, run) {
|
|
|
1424
1494
|
process.stderr.write(`>> uploading ${n} file${n === 1 ? "" : "s"} (staged for branch ${branch})\n`);
|
|
1425
1495
|
}
|
|
1426
1496
|
const target = { repo, branch };
|
|
1497
|
+
// Register any `git branch -m` steps behind this name (issue #920) so the
|
|
1498
|
+
// promote that runs when the PR opens sweeps the older names too.
|
|
1499
|
+
await registerRenamesBestEffort(ctx.client, run, repo, branch);
|
|
1427
1500
|
const { uploads, failures, firstError } = await uploadBranchAttachments({
|
|
1428
1501
|
client: ctx.client,
|
|
1429
1502
|
target,
|
|
@@ -1539,7 +1612,14 @@ async function runAttachPromoteOnly(ctx, parsed, run) {
|
|
|
1539
1612
|
const explicitTarget = ghTargetFromFlags(parsed.flags, run);
|
|
1540
1613
|
const target = explicitTarget ??
|
|
1541
1614
|
resolveCurrentPullRequest(resolveRepo(flagString(parsed.flags, "--repo"), run), run);
|
|
1542
|
-
|
|
1615
|
+
if (target.kind !== "pull") {
|
|
1616
|
+
throw new UsageError("--from-branch only promotes into a pull request");
|
|
1617
|
+
}
|
|
1618
|
+
const explicitFromBranch = flagString(parsed.flags, "--from-branch");
|
|
1619
|
+
const branch = explicitFromBranch ?? resolveCurrentBranch(run);
|
|
1620
|
+
await registerRenamesBestEffort(ctx.client, run, target.repo, branch, {
|
|
1621
|
+
explicit: explicitFromBranch !== undefined,
|
|
1622
|
+
});
|
|
1543
1623
|
const promotion = await attemptPromoteBranch(ctx.client, target, branch);
|
|
1544
1624
|
let comment;
|
|
1545
1625
|
let commentError;
|
|
@@ -1628,13 +1708,9 @@ export function resolvePutNudgeContext(opts) {
|
|
|
1628
1708
|
try {
|
|
1629
1709
|
if (deriveRepoFromGit(run) === undefined)
|
|
1630
1710
|
return undefined; // not a (usable) git repo
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
branch = resolveCurrentBranch(run);
|
|
1634
|
-
}
|
|
1635
|
-
catch {
|
|
1711
|
+
const branch = resolveCurrentBranchSafe(run);
|
|
1712
|
+
if (branch === undefined)
|
|
1636
1713
|
return undefined; // detached HEAD, or git unavailable
|
|
1637
|
-
}
|
|
1638
1714
|
const defaultBranch = resolveDefaultBranch(run);
|
|
1639
1715
|
const onDefaultBranch = defaultBranch
|
|
1640
1716
|
? branch === defaultBranch
|
|
@@ -1676,6 +1752,10 @@ export function resolvePutNudgeContext(opts) {
|
|
|
1676
1752
|
* default branch, or with `--no-git`; any failure (not a repo, detached
|
|
1677
1753
|
* HEAD, gh missing/unauthenticated/timed out, no open PR) degrades to
|
|
1678
1754
|
* undefined so the caller falls back to its normal staging/dated behavior.
|
|
1755
|
+
*
|
|
1756
|
+
* The match carries the `branch` it was resolved from, so callers that need
|
|
1757
|
+
* the current branch (the #920 rename registration) reuse it instead of
|
|
1758
|
+
* spawning `git rev-parse` a second time.
|
|
1679
1759
|
*/
|
|
1680
1760
|
export function resolveAutoPrTarget(opts) {
|
|
1681
1761
|
const { ghTarget, keyHint, refArg, prefixArg, destinationArg, branchArg, noGit, noAutoPr, repoArg, run, } = opts;
|
|
@@ -1688,13 +1768,9 @@ export function resolveAutoPrTarget(opts) {
|
|
|
1688
1768
|
try {
|
|
1689
1769
|
if (deriveRepoFromGit(run) === undefined)
|
|
1690
1770
|
return undefined; // not a (usable) git repo
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
branch = resolveCurrentBranch(run);
|
|
1694
|
-
}
|
|
1695
|
-
catch {
|
|
1771
|
+
const branch = resolveCurrentBranchSafe(run);
|
|
1772
|
+
if (branch === undefined)
|
|
1696
1773
|
return undefined; // detached HEAD, or git unavailable
|
|
1697
|
-
}
|
|
1698
1774
|
const defaultBranch = resolveDefaultBranch(run);
|
|
1699
1775
|
const onDefaultBranch = defaultBranch
|
|
1700
1776
|
? branch === defaultBranch
|
|
@@ -1705,7 +1781,7 @@ export function resolveAutoPrTarget(opts) {
|
|
|
1705
1781
|
// this must never be felt as a hang.
|
|
1706
1782
|
const timed = run === execRunner ? timedExecRunner(PUT_NUDGE_GH_TIMEOUT_MS) : run;
|
|
1707
1783
|
const repo = resolveRepo(repoArg, timed);
|
|
1708
|
-
return resolveCurrentPullRequest(repo, timed);
|
|
1784
|
+
return { target: resolveCurrentPullRequest(repo, timed), branch };
|
|
1709
1785
|
}
|
|
1710
1786
|
catch {
|
|
1711
1787
|
return undefined; // gh/git unavailable, no open PR, or repo unresolvable
|
|
@@ -1738,13 +1814,9 @@ export function resolvePutStagingTarget(opts) {
|
|
|
1738
1814
|
try {
|
|
1739
1815
|
if (deriveRepoFromGit(run) === undefined)
|
|
1740
1816
|
return undefined; // not a (usable) git repo
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
branch = resolveCurrentBranch(run);
|
|
1744
|
-
}
|
|
1745
|
-
catch {
|
|
1817
|
+
const branch = resolveCurrentBranchSafe(run);
|
|
1818
|
+
if (branch === undefined)
|
|
1746
1819
|
return undefined; // detached HEAD, or git unavailable
|
|
1747
|
-
}
|
|
1748
1820
|
const defaultBranch = resolveDefaultBranch(run);
|
|
1749
1821
|
const onDefaultBranch = defaultBranch
|
|
1750
1822
|
? branch === defaultBranch
|
|
@@ -1965,7 +2037,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1965
2037
|
// resolveAutoPrTarget. Supersedes both the #403 staging default and the
|
|
1966
2038
|
// #393 nudge for this case; computed before the gh.* metadata resolution
|
|
1967
2039
|
// below since it takes over that resolution entirely.
|
|
1968
|
-
const
|
|
2040
|
+
const autoPrMatch = ghTarget
|
|
1969
2041
|
? undefined
|
|
1970
2042
|
: resolveAutoPrTarget({
|
|
1971
2043
|
ghTarget,
|
|
@@ -1978,6 +2050,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1978
2050
|
repoArg: flagString(parsed.flags, "--repo") ?? defaults.repo,
|
|
1979
2051
|
run,
|
|
1980
2052
|
});
|
|
2053
|
+
const autoPrTarget = autoPrMatch?.target;
|
|
1981
2054
|
const effectiveGhTarget = ghTarget ?? autoPrTarget;
|
|
1982
2055
|
// Comment sync runs by default with --pr/--issue (matches `attach`); opt
|
|
1983
2056
|
// out with --no-comment. --comment is accepted as a redundant no-op for
|
|
@@ -2109,9 +2182,23 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
2109
2182
|
validateMetaMap(merged); // enforce 24-key/8KB caps on the merged map (matches attach)
|
|
2110
2183
|
metadata = merged;
|
|
2111
2184
|
attachedRef = merged["gh.ref"];
|
|
2185
|
+
// Auto-PR (#700) suppresses staging, so the staging branch below never
|
|
2186
|
+
// runs — but this put still comes from a branch that may have been
|
|
2187
|
+
// renamed while files were staged under the old name. Register the
|
|
2188
|
+
// lineage here too (issue #920), reusing the branch the match already
|
|
2189
|
+
// resolved. Only for the auto-PR match: an explicit --pr/--issue names
|
|
2190
|
+
// no branch of its own.
|
|
2191
|
+
if (autoPrMatch && !dryRun) {
|
|
2192
|
+
await registerRenamesBestEffort(ctx.client, run, autoPrMatch.target.repo, autoPrMatch.branch);
|
|
2193
|
+
}
|
|
2112
2194
|
}
|
|
2113
2195
|
else if (stagingTarget) {
|
|
2114
2196
|
metadata = mergeStagingMeta(userMeta, stagingTarget);
|
|
2197
|
+
// Branch staging: register any rename behind this name (issue #920) so
|
|
2198
|
+
// the PR-time promote finds files staged under the older names too.
|
|
2199
|
+
if (!dryRun) {
|
|
2200
|
+
await registerRenamesBestEffort(ctx.client, run, stagingTarget.repo, stagingTarget.branch);
|
|
2201
|
+
}
|
|
2115
2202
|
}
|
|
2116
2203
|
else {
|
|
2117
2204
|
// gh.* additionally needs git, which the shared derived gate ignores.
|
package/dist/github-gh.d.ts
CHANGED
|
@@ -19,6 +19,31 @@ export declare function resolveRepo(explicit: string | undefined, run?: CommandR
|
|
|
19
19
|
export declare function resolveCurrentPullRequest(repo: string, run?: CommandRunner): GhTarget;
|
|
20
20
|
/** Resolve the current git branch (`--branch` with no value). Throws UsageError on detached HEAD or outside a git repo. */
|
|
21
21
|
export declare function resolveCurrentBranch(run?: CommandRunner): string;
|
|
22
|
+
/**
|
|
23
|
+
* `resolveCurrentBranch` for best-effort callers: undefined instead of a
|
|
24
|
+
* throw when the branch can't be determined (detached HEAD, not a git repo,
|
|
25
|
+
* git unavailable). Use it wherever a missing branch means "skip this step",
|
|
26
|
+
* not "fail the command".
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveCurrentBranchSafe(run?: CommandRunner): string | undefined;
|
|
29
|
+
/** One `git branch -m` step read out of a branch's reflog: `from` was renamed to `to`. */
|
|
30
|
+
export interface BranchRenameStep {
|
|
31
|
+
from: string;
|
|
32
|
+
to: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Rename lineage for `branch`, read from its own reflog (issue #920). `git
|
|
36
|
+
* branch -m old new` writes `Branch: renamed refs/heads/old to
|
|
37
|
+
* refs/heads/new` into the NEW branch's reflog, and chained renames
|
|
38
|
+
* accumulate there, newest entry first — so the parsed pairs are reversed
|
|
39
|
+
* into oldest-first order, which is the order the server wants them
|
|
40
|
+
* registered.
|
|
41
|
+
*
|
|
42
|
+
* Best-effort by construction: any git failure (not a repo, no reflog for
|
|
43
|
+
* the branch, git missing) and any non-rename reflog line yield an empty
|
|
44
|
+
* array. Callers must treat "no lineage" as the normal case.
|
|
45
|
+
*/
|
|
46
|
+
export declare function renameLineageFromReflog(run?: CommandRunner, branch?: string): BranchRenameStep[];
|
|
22
47
|
/**
|
|
23
48
|
* Best-effort default-branch name via the local `origin/HEAD` ref (no
|
|
24
49
|
* network call — just reads the ref git already cached from the last
|
package/dist/github-gh.js
CHANGED
|
@@ -114,6 +114,55 @@ export function resolveCurrentBranch(run = execRunner) {
|
|
|
114
114
|
}
|
|
115
115
|
return branch;
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* `resolveCurrentBranch` for best-effort callers: undefined instead of a
|
|
119
|
+
* throw when the branch can't be determined (detached HEAD, not a git repo,
|
|
120
|
+
* git unavailable). Use it wherever a missing branch means "skip this step",
|
|
121
|
+
* not "fail the command".
|
|
122
|
+
*/
|
|
123
|
+
export function resolveCurrentBranchSafe(run = execRunner) {
|
|
124
|
+
try {
|
|
125
|
+
return resolveCurrentBranch(run);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Rename lineage for `branch`, read from its own reflog (issue #920). `git
|
|
133
|
+
* branch -m old new` writes `Branch: renamed refs/heads/old to
|
|
134
|
+
* refs/heads/new` into the NEW branch's reflog, and chained renames
|
|
135
|
+
* accumulate there, newest entry first — so the parsed pairs are reversed
|
|
136
|
+
* into oldest-first order, which is the order the server wants them
|
|
137
|
+
* registered.
|
|
138
|
+
*
|
|
139
|
+
* Best-effort by construction: any git failure (not a repo, no reflog for
|
|
140
|
+
* the branch, git missing) and any non-rename reflog line yield an empty
|
|
141
|
+
* array. Callers must treat "no lineage" as the normal case.
|
|
142
|
+
*/
|
|
143
|
+
export function renameLineageFromReflog(run = execRunner, branch) {
|
|
144
|
+
if (!branch)
|
|
145
|
+
return [];
|
|
146
|
+
let out;
|
|
147
|
+
try {
|
|
148
|
+
out = run("git", ["reflog", "show", "--format=%gs", `refs/heads/${branch}`]);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return []; // no reflog, not a repo, git unavailable — nothing to follow
|
|
152
|
+
}
|
|
153
|
+
const steps = [];
|
|
154
|
+
for (const line of out.split("\n")) {
|
|
155
|
+
const match = /^Branch: renamed refs\/heads\/(.+) to refs\/heads\/(.+)$/.exec(line.trim());
|
|
156
|
+
if (!match)
|
|
157
|
+
continue;
|
|
158
|
+
const from = match[1].trim();
|
|
159
|
+
const to = match[2].trim();
|
|
160
|
+
if (!from || !to || from === to)
|
|
161
|
+
continue;
|
|
162
|
+
steps.push({ from, to });
|
|
163
|
+
}
|
|
164
|
+
return steps.reverse(); // reflog is newest-first; register oldest-first
|
|
165
|
+
}
|
|
117
166
|
/**
|
|
118
167
|
* Best-effort default-branch name via the local `origin/HEAD` ref (no
|
|
119
168
|
* network call — just reads the ref git already cached from the last
|
package/dist/mcp/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createUploadsClient } from "../client.js";
|
|
2
|
-
import { buildDoctorReport, ghListPrefixes, ghMergedList, makeGhTarget, mergeStagingMeta, resolveAutoPrTarget, resolveGhPrefixSafe, resolvePutNudgeContext, resolvePutStagingTarget, resolveStaged, syncAttachmentsComment, autoPrNoteText, putNudgeText, uploadAttachments, uploadPreparedImage, uploadPuts, } from "../commands.js";
|
|
1
|
+
import { createUploadsClient, } from "../client.js";
|
|
2
|
+
import { buildDoctorReport, ghListPrefixes, ghMergedList, makeGhTarget, mergeStagingMeta, resolveAutoPrTarget, resolveGhPrefixSafe, resolvePutNudgeContext, registerRenamesBestEffort, resolvePutStagingTarget, resolveStaged, syncAttachmentsComment, autoPrNoteText, putNudgeText, uploadAttachments, uploadPreparedImage, uploadPuts, } from "../commands.js";
|
|
3
3
|
import { resolveFrameId } from "../frame.js";
|
|
4
4
|
import { resolveConfig, resolvePutDefaults, } from "../config.js";
|
|
5
5
|
import { resolvePutPrefix } from "../destinations.js";
|
|
@@ -9,7 +9,7 @@ import { safeCaptureFacts } from "../capture-facts.js";
|
|
|
9
9
|
import { deriveRepoSlugFromGit } from "../keys.js";
|
|
10
10
|
import { validateMetaMap } from "../metadata.js";
|
|
11
11
|
import { mergeDerivedMeta } from "../metadata-vocab.js";
|
|
12
|
-
import { execRunner, ghMetadataFromTargetWithTitle, resolveCurrentBranch, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
|
|
12
|
+
import { execRunner, ghMetadataFromTargetWithTitle, resolveCurrentBranch, resolveCurrentBranchSafe, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
|
|
13
13
|
import { appProp, canonicalMetaFromArgs, METADATA_PATH_CUE, metadataArgWithCanonical, metadataProp, optBool, optPosInt, optString, optStringArray, optStringRecord, stateProp, usage, } from "./args.js";
|
|
14
14
|
import { batchFailureMessage, mcpDestroyPublic, mcpNoAuth, mcpOAuthAny, mcpOAuthDelete, mcpOAuthRead, mcpOAuthWrite, mcpRead, mcpWriteInternal, mcpWritePublic, stdioOutputSchemas, withOutputSchemas, ToolBatchError, } from "./server.js";
|
|
15
15
|
import { attachmentFromText, buildReportPayload, parseReportType, REPORT_TYPES, submitReport, validateReportMessage, } from "../report.js";
|
|
@@ -131,6 +131,25 @@ export function createUploadsMcpTools(opts) {
|
|
|
131
131
|
}
|
|
132
132
|
return { comment, commentError };
|
|
133
133
|
};
|
|
134
|
+
/**
|
|
135
|
+
* Best-effort branch promote for the stdio `attach` tool (CLI `attach --pr`
|
|
136
|
+
* parity, issue #920). Registers any `git branch -m` steps first so the
|
|
137
|
+
* sweep follows renames, then promotes. Never throws: a failure surfaces as
|
|
138
|
+
* `promoteError` so the uploads themselves still return.
|
|
139
|
+
*/
|
|
140
|
+
const attemptPromote = async (client, target, branch) => {
|
|
141
|
+
try {
|
|
142
|
+
const promotion = await client.promoteBranchAttachments({
|
|
143
|
+
repo: target.repo,
|
|
144
|
+
num: target.num,
|
|
145
|
+
branch,
|
|
146
|
+
});
|
|
147
|
+
return { promotion };
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
return { promoteError: err instanceof Error ? err.message : String(err) };
|
|
151
|
+
}
|
|
152
|
+
};
|
|
134
153
|
const tools = [
|
|
135
154
|
{
|
|
136
155
|
name: "gallery_create",
|
|
@@ -466,7 +485,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
466
485
|
// had been passed (stable key + managed comment sync) instead of
|
|
467
486
|
// the #403 staging default below. Never throws — see
|
|
468
487
|
// resolveAutoPrTarget.
|
|
469
|
-
const autoPrTarget = target
|
|
488
|
+
const autoPrTarget = (target
|
|
470
489
|
? undefined
|
|
471
490
|
: resolveAutoPrTarget({
|
|
472
491
|
ghTarget: target,
|
|
@@ -478,7 +497,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
478
497
|
noAutoPr,
|
|
479
498
|
repoArg: optString(args, "repo") ?? defaults.repo,
|
|
480
499
|
run,
|
|
481
|
-
});
|
|
500
|
+
}))?.target;
|
|
482
501
|
const effectiveTarget = target ?? autoPrTarget;
|
|
483
502
|
// Bare-put branch staging (issue #403): local stdio MCP put mirrors
|
|
484
503
|
// the CLI default — no pr/issue/key/ref/prefix/destination, not
|
|
@@ -858,7 +877,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
858
877
|
// behaves as if `pr` had been passed (stable key + managed comment
|
|
859
878
|
// sync) instead of the #469 auto-staging default below. Never
|
|
860
879
|
// throws — see resolveAutoPrTarget.
|
|
861
|
-
const autoPrTarget = target
|
|
880
|
+
const autoPrTarget = (target
|
|
862
881
|
? undefined
|
|
863
882
|
: resolveAutoPrTarget({
|
|
864
883
|
ghTarget: target,
|
|
@@ -870,7 +889,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
870
889
|
noAutoPr,
|
|
871
890
|
repoArg: optString(args, "repo") ?? defaults.repo,
|
|
872
891
|
run,
|
|
873
|
-
});
|
|
892
|
+
}))?.target;
|
|
874
893
|
const effectiveTarget = target ?? autoPrTarget;
|
|
875
894
|
// Auto branch staging (issue #469 lever 1): mirrors the CLI screenshot
|
|
876
895
|
// command and the put tool above (issue #403) — no pr/issue/key/ref/
|
|
@@ -1064,7 +1083,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
1064
1083
|
title: "Attach to GitHub",
|
|
1065
1084
|
annotations: mcpDestroyPublic,
|
|
1066
1085
|
securitySchemes: mcpOAuthWrite,
|
|
1067
|
-
description: "Upload one or more files as stable PR/issue attachments (in parallel) and maintain a managed GitHub comment. Returns `uploads` and `failures` (one bad file does not abort the batch). Each success has `url`, `embedUrl`, and `markdown` (prefer embedUrl for GitHub). With no pr/issue, targets the current branch PR. Attachments are public and keys are predictable; upload only non-sensitive media.",
|
|
1086
|
+
description: "Upload one or more files as stable PR/issue attachments (in parallel) and maintain a managed GitHub comment. Returns `uploads` and `failures` (one bad file does not abort the batch). Each success has `url`, `embedUrl`, and `markdown` (prefer embedUrl for GitHub). With no pr/issue, targets the current branch PR, and files already staged for that branch are promoted into it (`promotion`/`promoteError`; renamed branches are followed automatically, or name the old branch with fromBranch). Attachments are public and keys are predictable; upload only non-sensitive media.",
|
|
1068
1087
|
inputSchema: {
|
|
1069
1088
|
type: "object",
|
|
1070
1089
|
properties: {
|
|
@@ -1078,6 +1097,14 @@ export function createUploadsMcpTools(opts) {
|
|
|
1078
1097
|
type: "boolean",
|
|
1079
1098
|
description: "Upload only; don't create/update the managed comment.",
|
|
1080
1099
|
},
|
|
1100
|
+
noPromote: {
|
|
1101
|
+
type: "boolean",
|
|
1102
|
+
description: "Skip promoting files staged for the current branch into the PR.",
|
|
1103
|
+
},
|
|
1104
|
+
fromBranch: {
|
|
1105
|
+
type: "string",
|
|
1106
|
+
description: "Promote files staged under this branch name instead of the current branch (for a branch renamed before the PR opened).",
|
|
1107
|
+
},
|
|
1081
1108
|
contentType: {
|
|
1082
1109
|
type: "string",
|
|
1083
1110
|
description: "Override the Content-Type (applied to every file; ignored when optimize rewrites).",
|
|
@@ -1109,6 +1136,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
1109
1136
|
examples: [
|
|
1110
1137
|
{ files: ["./after.png"], pr: 12, state: "after" },
|
|
1111
1138
|
{ files: ["./before.png", "./after.png"], pr: 12 },
|
|
1139
|
+
{ files: ["./after.png"], pr: 12, fromBranch: "old-branch-name" },
|
|
1112
1140
|
],
|
|
1113
1141
|
},
|
|
1114
1142
|
async handler(args) {
|
|
@@ -1118,6 +1146,13 @@ export function createUploadsMcpTools(opts) {
|
|
|
1118
1146
|
const explicitTarget = ghTargetFromArgs(args, run);
|
|
1119
1147
|
const target = explicitTarget ??
|
|
1120
1148
|
resolveCurrentPullRequest(resolveRepo(optString(args, "repo"), run), run);
|
|
1149
|
+
const fromBranch = optString(args, "fromBranch");
|
|
1150
|
+
if (fromBranch !== undefined && optBool(args, "noPromote")) {
|
|
1151
|
+
usage("fromBranch cannot be combined with noPromote");
|
|
1152
|
+
}
|
|
1153
|
+
if (fromBranch !== undefined && target.kind !== "pull") {
|
|
1154
|
+
usage("fromBranch only promotes into a pull request");
|
|
1155
|
+
}
|
|
1121
1156
|
const { config, client } = await clientFor(args);
|
|
1122
1157
|
const contentType = optString(args, "contentType");
|
|
1123
1158
|
const defaults = resolvePutDefaults({ envFile: globals.envFile });
|
|
@@ -1154,10 +1189,30 @@ export function createUploadsMcpTools(opts) {
|
|
|
1154
1189
|
failures,
|
|
1155
1190
|
});
|
|
1156
1191
|
}
|
|
1192
|
+
// Auto-promote (CLI `attach --pr` parity, issue #920): before the
|
|
1193
|
+
// comment sync, best-effort promote this workspace's branch-staged
|
|
1194
|
+
// files into the PR, following any `git branch -m` rename. Never for
|
|
1195
|
+
// issues, never with noPromote, and silently skipped when the current
|
|
1196
|
+
// branch can't be resolved — this must not fail the attach.
|
|
1197
|
+
let promotion;
|
|
1198
|
+
let promoteError;
|
|
1199
|
+
if (target.kind === "pull" && !optBool(args, "noPromote")) {
|
|
1200
|
+
const branch = fromBranch ?? resolveCurrentBranchSafe(run);
|
|
1201
|
+
if (branch !== undefined) {
|
|
1202
|
+
await registerRenamesBestEffort(client, run, target.repo, branch, {
|
|
1203
|
+
explicit: fromBranch !== undefined,
|
|
1204
|
+
});
|
|
1205
|
+
({ promotion, promoteError } = await attemptPromote(client, target, branch));
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const promoteFields = {
|
|
1209
|
+
...(promotion ? { promotion } : {}),
|
|
1210
|
+
...(promoteError ? { promoteError } : {}),
|
|
1211
|
+
};
|
|
1157
1212
|
if (optBool(args, "noComment"))
|
|
1158
|
-
return { target, uploads, failures };
|
|
1213
|
+
return { target, uploads, failures, ...promoteFields };
|
|
1159
1214
|
const { comment, commentError } = await syncComment(client, target, config.workspace);
|
|
1160
|
-
return { target, uploads, failures, comment, commentError };
|
|
1215
|
+
return { target, uploads, failures, ...promoteFields, comment, commentError };
|
|
1161
1216
|
},
|
|
1162
1217
|
},
|
|
1163
1218
|
{
|