@henryqw/pi-pr 4.0.3 → 4.0.5

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 CHANGED
@@ -72,9 +72,9 @@ Each footer entry is one linked `PR #number` plus one plain-language status: `N
72
72
  | External check or commit status failed | Show `CI failed` as a no-action blocker. |
73
73
  | Changes requested or unresolved review threads | Run the package comment sweep when the same local prerequisite holds. |
74
74
  | No-action state | Report the state without taking action. |
75
- | Merge-ready pull request | Ask for final confirmation, recheck fresh state, and merge directly if confirmed. |
75
+ | Merge-ready pull request | Ask for final confirmation, recheck fresh state, and squash-merge if confirmed. |
76
76
 
77
- `pi-pr-create` selects its base in this order: the leading `/pr --base BRANCH`, one `branch.<branch>.gh-merge-base` value, then the default branch of validated `origin`. It captures the selected base OID and merge-base. Creation requires at least one committed change ahead. Dirty work alone does not enable creation.
77
+ `pi-pr-create` selects its base in this order: the leading `/pr --base BRANCH`, one `branch.<branch>.gh-merge-base` value, then the default branch of validated `origin`. It captures the selected base OID and merge-base. Creation requires at least one committed change ahead. Dirty work alone does not enable creation. If the current branch is the selected base, pi-pr stays silent because GitHub cannot create a pull request from a ref to itself.
78
78
 
79
79
  The base always comes from validated `origin`. The head may use that repository or a fork with the same GitHub source. Base and head must use the same GitHub host. Other fork relationships stop before mutation.
80
80
 
@@ -154,8 +154,8 @@ The GitHub response must match the observed URL, host, repository, head ref, hea
154
154
  - It does not rebase the local branch, overwrite concurrent remote updates, delete branches, or clean up worktrees. Creation uses exact leases plus ancestry checks; an empty lease is only an atomic absence check.
155
155
  - Creation, discovery, and comment-sweep pushes require one unambiguous push URL for the configured destination.
156
156
  - Presentation fetches use that exact push URL and exact advertised OID. They do not use shared fetch state.
157
- - Strict status checks in legacy branch protection or applicable repository rulesets require a base update.
158
- - Applicable ruleset restrictions intersect repository-wide merge methods. An empty intersection stops the workflow.
157
+ - A pull request that GitHub reports as behind requires a base update.
158
+ - Direct merges always use squash. GitHub rejects the mutation if repository policy does not allow it.
159
159
  - Before merge, `/pr` fetches the exact head OID from the validated push URL without shared fetch state.
160
160
  - A merge, rebase, cherry-pick, revert, or sequencer state blocks direct merge, even when `git status` is empty.
161
161
  - A branch update resolves the base repository ref directly. It stops if that ref moves before merge or push.
@@ -149,7 +149,6 @@ function cloneAuthority(value: CurrentPullRequest): CurrentPullRequest {
149
149
  base: { ...value.base },
150
150
  head: { ...value.head },
151
151
  target: { ...value.target },
152
- merge: value.merge ? { ...value.merge, allowedMergeMethods: [...value.merge.allowedMergeMethods] } : null,
153
152
  };
154
153
  }
155
154
 
@@ -2,10 +2,7 @@ import type {
2
2
  ExtensionAPI,
3
3
  ExtensionCommandContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
- import {
6
- executeGitHubMerge,
7
- selectMergeMethod,
8
- } from "./pr-merge.ts";
5
+ import { executeGitHubMerge } from "./pr-merge.ts";
9
6
  import {
10
7
  linkInferredPullRequest,
11
8
  loadCurrentPullRequest,
@@ -184,11 +181,9 @@ async function mergePullRequest(
184
181
  current: CurrentPullRequest,
185
182
  load: typeof loadCurrentPullRequest,
186
183
  ): Promise<boolean> {
187
- if (!current.merge) throw new Error(`PR #${current.number} merge failed: merge capabilities are unavailable`);
188
- const method = selectMergeMethod(current.merge);
189
184
  const confirmed = await ctx.ui.confirm(
190
185
  `Merge PR #${current.number}?`,
191
- `Method: ${method}.`,
186
+ "Method: squash.",
192
187
  );
193
188
  if (!confirmed) return false;
194
189
 
@@ -204,8 +199,6 @@ async function mergePullRequest(
204
199
  expectedHead: current.head.oid,
205
200
  expectedBase: current.base,
206
201
  headFetchSource: current.headFetchSource,
207
- allowedMergeMethods: current.merge.allowedMergeMethods,
208
- viewerDefaultMergeMethod: current.merge.viewerDefaultMergeMethod,
209
202
  revalidateReadiness: async (local) => {
210
203
  const discovery = await load(pi, ctx, local);
211
204
  if (discovery.kind !== "current") {
@@ -218,11 +211,6 @@ async function mergePullRequest(
218
211
  if (deriveNextStep(discovery) !== "merge") {
219
212
  throw new Error(`PR #${fresh.number} merge cancelled: pull request is no longer merge-ready`);
220
213
  }
221
- if (!fresh.merge) throw new Error(`PR #${fresh.number} merge failed: merge capabilities are unavailable`);
222
- const freshMethod = selectMergeMethod(fresh.merge);
223
- if (freshMethod !== method) {
224
- throw new Error(`PR #${fresh.number} merge cancelled: merge method changed from ${method} to ${freshMethod}`);
225
- }
226
214
  },
227
215
  });
228
216
  return true;
@@ -42,10 +42,6 @@ import {
42
42
  type ExecOptions,
43
43
  } from "./pr-execution.ts";
44
44
 
45
- export const PR_COMMENT_SWEEP_ACTIONS = [
46
- "start", "resume", "show", "record", "publish", "refresh", "resolve", "finalize",
47
- ] as const;
48
- export type PrCommentSweepAction = (typeof PR_COMMENT_SWEEP_ACTIONS)[number];
49
45
  export const SWEEP_RECOVERY_MAX_BYTES = 1024 * 1024;
50
46
 
51
47
  const STATE_VERSION = 1;
@@ -140,16 +136,6 @@ export type PullRequestCommentSweepOptions = {
140
136
  newRunId?: () => string;
141
137
  pause?: (milliseconds: number) => Promise<void>;
142
138
  };
143
- export type PrCommentSweepRequest =
144
- | { action: "start" }
145
- | { action: "resume" }
146
- | { action: "show"; guard: SweepRunGuard; id: string }
147
- | { action: "record"; guard: SweepRunGuard; ledger: SweepLedgerEntry[]; ownedPaths?: string[] }
148
- | { action: "publish"; guard: SweepRunGuard }
149
- | { action: "refresh"; guard: SweepRunGuard }
150
- | { action: "resolve"; guard: SweepRunGuard; threadIds: string[] }
151
- | { action: "finalize"; guard: SweepRunGuard; projection: SweepFinalProjection; checks: SweepCheck[] };
152
-
153
139
  function isRecord(value: unknown): value is Record<string, unknown> {
154
140
  return value !== null && typeof value === "object" && !Array.isArray(value);
155
141
  }
@@ -1086,17 +1072,4 @@ export class PullRequestCommentSweep {
1086
1072
  return { kind: "finalized", pullRequestUrl: state.authority.url, head: state.publicationHead, checks: checks.length };
1087
1073
  }, { agentDir: this.agentDir, signal: this.signal });
1088
1074
  }
1089
-
1090
- async action(request: PrCommentSweepRequest): Promise<SweepStatus | FeedbackItem | { kind: "finalized"; pullRequestUrl: string; head: string; checks: number }> {
1091
- switch (request.action) {
1092
- case "start": return await this.start();
1093
- case "resume": return await this.resume();
1094
- case "show": return await this.show(request.guard, request.id);
1095
- case "record": return await this.record(request.guard, request.ledger, request.ownedPaths);
1096
- case "publish": return await this.publish(request.guard);
1097
- case "refresh": return await this.refresh(request.guard);
1098
- case "resolve": return await this.resolve(request.guard, request.threadIds);
1099
- case "finalize": return await this.finalize(request.guard, request.projection, request.checks);
1100
- }
1101
- }
1102
1075
  }
@@ -6,14 +6,11 @@ import {
6
6
  loadCurrentPullRequest,
7
7
  loadPullRequestPublication,
8
8
  preflightPullRequestCreation,
9
- readBranchUpstreamConfiguration,
10
9
  readPullRequestBaseRefOid,
11
10
  readTrackingOid,
12
11
  readValidatedRemoteAuthority,
13
- restoreBranchUpstreamConfiguration,
14
12
  setBranchUpstream,
15
13
  verifyBranchUpstream,
16
- type BranchUpstreamConfiguration,
17
14
  type BranchUpstreamTarget,
18
15
  type PullRequestLoadContext,
19
16
  type PullRequestPublication,
@@ -31,7 +28,6 @@ import {
31
28
  runChecked,
32
29
  spawnBounded,
33
30
  withWorktreeLock,
34
- type AttemptState,
35
31
  type Exec,
36
32
  type ExecOptions,
37
33
  } from "./pr-execution.ts";
@@ -61,22 +57,11 @@ type CrossRepositoryCreateAuthority = {
61
57
 
62
58
  export type CreatePullRequestState = {
63
59
  phase: CreatePhase;
64
- attempts: {
65
- fetchBase: AttemptState;
66
- merge: AttemptState;
67
- stage: AttemptState;
68
- continueMerge: AttemptState;
69
- push: AttemptState;
70
- fetchTracking: AttemptState;
71
- setUpstream: AttemptState;
72
- pullRequest: AttemptState;
73
- };
74
60
  base?: CreateBaseAuthority;
75
61
  createAuthority?: CrossRepositoryCreateAuthority;
76
62
  mergeHead?: string;
77
63
  publicationHead?: string;
78
64
  conflict?: { paths: string[]; statusBaseline: string; originalHead: string };
79
- originalUpstream?: BranchUpstreamConfiguration;
80
65
  url?: string;
81
66
  };
82
67
 
@@ -102,11 +87,6 @@ function sameTarget(left: PullRequestTarget, right: PullRequestTarget, expectedR
102
87
  left.fetchSource === right.fetchSource && right.remoteOid === expectedRemoteOid;
103
88
  }
104
89
 
105
- function sameUpstreamConfiguration(left: BranchUpstreamConfiguration, right: BranchUpstreamConfiguration): boolean {
106
- return left.remote.length === right.remote.length && left.remote.every((value, index) => value === right.remote[index]) &&
107
- left.merge.length === right.merge.length && left.merge.every((value, index) => value === right.merge[index]);
108
- }
109
-
110
90
  function line(output: string, label: string): string {
111
91
  const normalized = output.replace(/\r\n/g, "\n");
112
92
  const values = normalized.endsWith("\n") ? normalized.slice(0, -1).split("\n") : normalized.split("\n");
@@ -198,17 +178,12 @@ function resolvedPaths(paths: readonly string[], expected: readonly string[]): s
198
178
  }
199
179
 
200
180
  export class PullRequestCreator {
201
- readonly state: CreatePullRequestState = {
202
- phase: "unprepared",
203
- attempts: {
204
- fetchBase: "none", merge: "none", stage: "none", continueMerge: "none", push: "none",
205
- fetchTracking: "none", setUpstream: "none", pullRequest: "none",
206
- },
207
- };
181
+ readonly state: CreatePullRequestState = { phase: "unprepared" };
208
182
 
209
183
  private readonly cwd: string;
210
184
  private readonly target: PullRequestTarget;
211
185
  private readonly noTarget: boolean;
186
+ private noTargetUpstreamConfigured = false;
212
187
  private readonly signal?: AbortSignal;
213
188
  private readonly agentDir?: string;
214
189
  private readonly exec: Exec;
@@ -283,7 +258,7 @@ export class PullRequestCreator {
283
258
  }
284
259
 
285
260
  async prepare(explicitBase?: string): Promise<CreatePullRequestResult> {
286
- if (this.state.phase !== "unprepared" || this.state.attempts.fetchBase !== "none") {
261
+ if (this.state.phase !== "unprepared") {
287
262
  throw new Error("PR creation prepare action was already consumed");
288
263
  }
289
264
  return await withWorktreeLock(this.cwd, async () => {
@@ -306,17 +281,11 @@ export class PullRequestCreator {
306
281
  oid: base.oid,
307
282
  fetchSource: base.fetchSource,
308
283
  };
309
- this.state.attempts.fetchBase = "attempting";
310
- try {
311
- await runChecked(this.exec, "git", [
312
- "fetch", "--no-write-fetch-head", "--no-tags", "--no-recurse-submodules", base.fetchSource, base.oid,
313
- ], this.options());
314
- await runChecked(this.exec, "git", ["cat-file", "-e", `${base.oid}^{commit}`], this.options());
315
- this.state.attempts.fetchBase = "applied";
316
- } catch (error) {
317
- this.state.attempts.fetchBase = "unknown";
318
- throw error;
319
- }
284
+ this.state.phase = "blocked";
285
+ await runChecked(this.exec, "git", [
286
+ "fetch", "--no-write-fetch-head", "--no-tags", "--no-recurse-submodules", base.fetchSource, base.oid,
287
+ ], this.options());
288
+ await runChecked(this.exec, "git", ["cat-file", "-e", `${base.oid}^{commit}`], this.options());
320
289
  await this.freshNone();
321
290
  if (await readHead(this.exec, this.options()) !== preflight.head) {
322
291
  throw new Error("PR creation cancelled: local HEAD changed during prepare");
@@ -358,7 +327,7 @@ export class PullRequestCreator {
358
327
  }
359
328
 
360
329
  async merge(): Promise<CreatePullRequestResult> {
361
- if (this.state.phase !== "prepared" || !this.state.base || this.state.attempts.merge !== "none") {
330
+ if (this.state.phase !== "prepared" || !this.state.base) {
362
331
  throw new Error("PR creation is not prepared for merge");
363
332
  }
364
333
  return await withWorktreeLock(this.cwd, async () => {
@@ -370,35 +339,17 @@ export class PullRequestCreator {
370
339
  this.state.mergeHead = originalHead;
371
340
  return { kind: "verified", head: originalHead, fastForward: false };
372
341
  }
373
- this.state.attempts.merge = "attempting";
374
- let result;
375
- try {
376
- result = await this.exec("git", ["merge", "--no-edit", this.state.base!.oid], this.options());
377
- } catch (error) {
378
- this.state.attempts.merge = "unknown";
379
- throw error;
380
- }
381
- if (result.killed) {
382
- this.state.attempts.merge = "unknown";
383
- throw new Error("git merge was killed; its outcome is unknown");
384
- }
342
+ this.state.phase = "blocked";
343
+ const result = await this.exec("git", ["merge", "--no-edit", this.state.base!.oid], this.options());
344
+ if (result.killed) throw new Error("git merge was killed; its outcome is unknown");
385
345
  if (result.code === 0) {
386
- try {
387
- const verified = await this.verifyMerge(originalHead);
388
- this.state.attempts.merge = "applied";
389
- return { kind: "verified", ...verified };
390
- } catch (error) {
391
- this.state.attempts.merge = "unknown";
392
- throw error;
393
- }
346
+ const verified = await this.verifyMerge(originalHead);
347
+ return { kind: "verified", ...verified };
394
348
  }
395
349
  try {
396
350
  const paths = await this.captureConflict(originalHead);
397
- this.state.attempts.merge = "applied";
398
351
  return { kind: "conflict", paths };
399
352
  } catch (error) {
400
- this.state.attempts.merge = "blocked";
401
- this.state.phase = "blocked";
402
353
  throw new Error(`git merge failed: ${result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`}; ${error instanceof Error ? error.message : String(error)}`);
403
354
  }
404
355
  }, { agentDir: this.agentDir, signal: this.signal });
@@ -408,9 +359,6 @@ export class PullRequestCreator {
408
359
  if (this.state.phase !== "conflict-awaiting-user" || !this.state.conflict || !this.state.base) {
409
360
  throw new Error("PR creation has no conflict awaiting continuation");
410
361
  }
411
- if (this.state.attempts.stage !== "none" || this.state.attempts.continueMerge !== "none") {
412
- throw new Error("PR creation conflict continuation was already consumed");
413
- }
414
362
  const paths = resolvedPaths(pathsInput, this.state.conflict.paths);
415
363
  return await withWorktreeLock(this.cwd, async () => {
416
364
  await this.freshNone();
@@ -420,94 +368,34 @@ export class PullRequestCreator {
420
368
  if (mergeHead !== this.state.base!.oid) throw new Error("PR creation merge context changed");
421
369
  const status = await runChecked(this.exec, "git", ["status", "--porcelain=v2", "-z", "--untracked-files=all"], this.options());
422
370
  assertOnlyDeclaredStatusChanged(this.state.conflict!.statusBaseline, status.stdout, paths);
423
- this.state.attempts.stage = "attempting";
424
- try {
425
- await runChecked(this.exec, "git", ["add", "--", ...paths], this.options());
426
- this.state.attempts.stage = "applied";
427
- } catch (error) {
428
- this.state.attempts.stage = "unknown";
429
- throw error;
430
- }
371
+ this.state.phase = "blocked";
372
+ await runChecked(this.exec, "git", ["add", "--", ...paths], this.options());
431
373
  const unmerged = parseNulPaths((await runChecked(this.exec, "git", ["diff", "--name-only", "-z", "--diff-filter=U"], this.options())).stdout, "Unmerged paths");
432
374
  if (unmerged.length) throw new Error(`Conflict paths remain unresolved: ${unmerged.join(", ")}`);
433
- this.state.attempts.continueMerge = "attempting";
434
- try {
435
- await runChecked(this.exec, "git", ["-c", "core.editor=true", "merge", "--continue"], this.options());
436
- const verified = await this.verifyMerge(this.state.conflict!.originalHead);
437
- this.state.attempts.continueMerge = "applied";
438
- return { kind: "verified", ...verified };
439
- } catch (error) {
440
- this.state.attempts.continueMerge = "unknown";
441
- throw error;
442
- }
375
+ await runChecked(this.exec, "git", ["-c", "core.editor=true", "merge", "--continue"], this.options());
376
+ const verified = await this.verifyMerge(this.state.conflict!.originalHead);
377
+ return { kind: "verified", ...verified };
443
378
  }, { agentDir: this.agentDir, signal: this.signal });
444
379
  }
445
380
 
446
- private upstreamTarget(head: string): BranchUpstreamTarget {
447
- return {
381
+ private async configureNoTargetUpstream(head: string): Promise<void> {
382
+ const target: BranchUpstreamTarget = {
448
383
  branch: this.target.branch,
449
384
  remote: this.target.remote,
450
385
  ref: this.target.ref,
451
386
  fetchSource: this.target.fetchSource,
452
387
  remoteOid: head,
453
388
  };
454
- }
455
-
456
- private async captureNoTargetUpstream(): Promise<void> {
457
- if (!this.noTarget || this.state.originalUpstream !== undefined) return;
458
- this.state.originalUpstream = await readBranchUpstreamConfiguration(this.pi(), this.context(), this.target.branch);
459
- }
460
-
461
- private async fetchNoTargetTracking(head: string): Promise<void> {
462
- if (this.state.attempts.fetchTracking === "applied") return;
463
- if (this.state.attempts.fetchTracking !== "none" && this.state.attempts.fetchTracking !== "unknown") {
464
- throw new Error("PR creation tracking fetch cannot be retried");
465
- }
466
- const target = this.upstreamTarget(head);
467
- this.state.attempts.fetchTracking = "attempting";
468
- try {
469
- await fetchBranchTrackingRef(this.pi(), this.context(), target);
470
- if (await readTrackingOid(this.pi(), this.context(), branchTrackingRef(target)) !== head) {
471
- throw new Error("Fetched tracking ref did not match published HEAD");
472
- }
473
- this.state.attempts.fetchTracking = "applied";
474
- } catch (error) {
475
- this.state.attempts.fetchTracking = "unknown";
476
- throw error;
477
- }
478
- }
479
-
480
- private async configureNoTargetUpstream(head: string): Promise<void> {
481
- const original = this.state.originalUpstream;
482
- if (!original) throw new Error("PR creation original branch upstream is unavailable");
483
- if (!sameUpstreamConfiguration(
484
- await readBranchUpstreamConfiguration(this.pi(), this.context(), this.target.branch),
485
- original,
486
- )) {
487
- throw new Error("PR creation cancelled: branch upstream changed before setup");
488
- }
489
- await this.fetchNoTargetTracking(head);
490
- const target = this.upstreamTarget(head);
491
- this.state.attempts.setUpstream = "attempting";
492
- try {
493
- await setBranchUpstream(this.pi(), this.context(), target);
494
- await verifyBranchUpstream(this.pi(), this.context(), target);
495
- this.state.attempts.setUpstream = "applied";
496
- } catch (error) {
497
- this.state.attempts.setUpstream = "unknown";
498
- const rollbackContext = { cwd: this.cwd, signal: new AbortController().signal };
499
- try {
500
- await restoreBranchUpstreamConfiguration(this.pi(), rollbackContext, target, original);
501
- } catch {
502
- this.state.phase = "blocked";
503
- throw new Error("PR creation upstream setup failed and rollback was incomplete");
504
- }
505
- throw error;
389
+ await fetchBranchTrackingRef(this.pi(), this.context(), target);
390
+ if (await readTrackingOid(this.pi(), this.context(), branchTrackingRef(target)) !== head) {
391
+ throw new Error("Fetched tracking ref did not match published HEAD");
506
392
  }
393
+ await setBranchUpstream(this.pi(), this.context(), target);
394
+ await verifyBranchUpstream(this.pi(), this.context(), target);
507
395
  }
508
396
 
509
397
  async push(): Promise<CreatePullRequestResult> {
510
- if (this.state.phase !== "verified" || !this.state.base || this.state.attempts.push !== "none") {
398
+ if (this.state.phase !== "verified" || !this.state.base) {
511
399
  throw new Error("PR creation is not ready to push");
512
400
  }
513
401
  return await withWorktreeLock(this.cwd, async () => {
@@ -525,31 +413,27 @@ export class PullRequestCreator {
525
413
  await this.freshNone();
526
414
  if (await this.liveBase() !== this.state.base!.oid) throw new Error("PR creation cancelled: frozen base moved");
527
415
  if (await this.requireCleanHead() !== head) throw new Error("PR creation cancelled: local HEAD changed before push");
528
- if (this.noTarget) await this.captureNoTargetUpstream();
529
416
  this.state.publicationHead = head;
530
- this.state.attempts.push = "attempting";
531
- try {
532
- await runChecked(this.exec, "git", [
533
- "push", "--porcelain", `--force-with-lease=refs/heads/${this.target.ref}:${original ?? ""}`,
534
- "--recurse-submodules=no", "--", this.target.fetchSource, `${head}:refs/heads/${this.target.ref}`,
535
- ], this.options());
536
- if (await readRemoteOid(this.exec, this.options(), this.target.fetchSource, this.target.ref) !== head) {
537
- throw new Error("Published remote ref did not match captured HEAD");
538
- }
539
- this.state.attempts.push = "applied";
540
- } catch (error) {
541
- this.state.attempts.push = "unknown";
542
- throw error;
417
+ this.state.phase = "blocked";
418
+ await runChecked(this.exec, "git", [
419
+ "push", "--porcelain", `--force-with-lease=refs/heads/${this.target.ref}:${original ?? ""}`,
420
+ "--recurse-submodules=no", "--", this.target.fetchSource, `${head}:refs/heads/${this.target.ref}`,
421
+ ], this.options());
422
+ if (await readRemoteOid(this.exec, this.options(), this.target.fetchSource, this.target.ref) !== head) {
423
+ throw new Error("Published remote ref did not match captured HEAD");
543
424
  }
544
425
  this.state.phase = "pushed";
545
- if (this.noTarget) await this.fetchNoTargetTracking(head);
426
+ if (this.noTarget) {
427
+ await this.configureNoTargetUpstream(head);
428
+ this.noTargetUpstreamConfigured = true;
429
+ }
546
430
  return { kind: "pushed", head };
547
431
  }, { agentDir: this.agentDir, signal: this.signal });
548
432
  }
549
433
 
550
434
  private async publishedAuthority(): Promise<void> {
551
435
  const head = this.state.publicationHead!;
552
- if (this.noTarget && this.state.attempts.setUpstream !== "applied") {
436
+ if (this.noTarget && !this.noTargetUpstreamConfigured) {
553
437
  const branch = line((await runChecked(this.exec, "git", ["branch", "--show-current"], this.options())).stdout, "current branch");
554
438
  const authority = await readValidatedRemoteAuthority(this.pi(), this.context(), this.target.remote);
555
439
  if (branch !== this.target.branch || authority.host !== this.target.host ||
@@ -587,16 +471,9 @@ export class PullRequestCreator {
587
471
  }
588
472
 
589
473
  async publish(titleInput: string, body: string): Promise<CreatePullRequestResult> {
590
- const retryingUpstream = this.noTarget && this.state.attempts.pullRequest === "applied";
591
- if (
592
- this.state.phase !== "pushed" || !this.state.base || !this.state.publicationHead ||
593
- (this.state.attempts.pullRequest !== "none" && !retryingUpstream)
594
- ) {
474
+ if (this.state.phase !== "pushed" || !this.state.base || !this.state.publicationHead) {
595
475
  throw new Error("PR creation is not ready to publish metadata");
596
476
  }
597
- if (this.noTarget && !this.state.originalUpstream) {
598
- throw new Error("PR creation original branch upstream is unavailable");
599
- }
600
477
  const title = requiredText(titleInput, "pull request title");
601
478
  if (Buffer.byteLength(title, "utf8") > MAX_TITLE_BYTES) throw new Error(`Pull request title exceeds ${MAX_TITLE_BYTES} bytes`);
602
479
  if (typeof body !== "string" || body.includes("\0") || Buffer.byteLength(body, "utf8") > MAX_BODY_BYTES) {
@@ -609,63 +486,42 @@ export class PullRequestCreator {
609
486
  if (before && (before.base.repository.toLowerCase() !== this.state.base!.repository.toLowerCase() || before.base.ref !== this.state.base!.ref)) {
610
487
  throw new Error("Exact-head pull request targets a different base");
611
488
  }
612
- let after: PullRequestPublication;
613
- if (retryingUpstream) {
614
- if (!before || before.title !== title || before.body !== body) {
615
- throw new Error("Published pull request changed before upstream completion");
616
- }
617
- after = before;
618
- } else {
619
- const repository = `${this.state.base!.host}/${this.state.base!.repository}`;
620
- const sameRepository = this.state.base!.repository.toLowerCase() === this.target.repository.toLowerCase();
621
- const organizationAuthority = !sameRepository && this.state.createAuthority?.headOwnerType === "Organization"
622
- ? this.state.createAuthority
623
- : undefined;
624
- if (!sameRepository && !this.state.createAuthority) {
625
- throw new Error("Cross-repository PR creation was not preflighted before push");
626
- }
627
- const args = before
628
- ? ["pr", "edit", String(before.number), "--repo", repository, "--title", title, "--body-file", "-"]
629
- : organizationAuthority
630
- ? [
631
- "api", "graphql", "--hostname", this.state.base!.host,
632
- "-f", `query=${CREATE_PULL_REQUEST_MUTATION}`,
633
- "-f", `repositoryId=${organizationAuthority.baseRepositoryId}`,
634
- "-f", `baseRefName=${this.state.base!.ref}`,
635
- "-f", `headRepositoryId=${organizationAuthority.headRepositoryId}`,
636
- "-f", `headRefName=${this.target.ref}`,
637
- "-f", `title=${title}`,
638
- "-f", `body=${body}`,
639
- ]
640
- : [
641
- "pr", "create", "--repo", repository, "--head",
642
- sameRepository ? this.target.ref : `${this.target.repository.split("/")[0]}:${this.target.ref}`,
643
- "--base", this.state.base!.ref, "--title", title, "--body-file", "-",
644
- ];
645
- this.state.attempts.pullRequest = "attempting";
646
- try {
647
- const created = await runChecked(this.exec, "gh", args, this.options({ stdin: organizationAuthority ? undefined : body }));
648
- if (!before && organizationAuthority) {
649
- parseCreatedUrl(created.stdout, this.state.base!.host, this.state.base!.repository);
650
- }
651
- } catch (error) {
652
- this.state.attempts.pullRequest = "unknown";
653
- throw error;
654
- }
655
- try {
656
- const refreshed = await this.exactCandidate();
657
- if (!refreshed || refreshed.base.repository.toLowerCase() !== this.state.base!.repository.toLowerCase() ||
658
- refreshed.base.ref !== this.state.base!.ref || refreshed.title !== title || refreshed.body !== body) {
659
- throw new Error("Published pull request did not retain canonical identity, title, and body");
660
- }
661
- after = refreshed;
662
- this.state.attempts.pullRequest = "applied";
663
- } catch (error) {
664
- this.state.attempts.pullRequest = "unknown";
665
- throw error;
666
- }
489
+ const repository = `${this.state.base!.host}/${this.state.base!.repository}`;
490
+ const sameRepository = this.state.base!.repository.toLowerCase() === this.target.repository.toLowerCase();
491
+ const organizationAuthority = !sameRepository && this.state.createAuthority?.headOwnerType === "Organization"
492
+ ? this.state.createAuthority
493
+ : undefined;
494
+ if (!sameRepository && !this.state.createAuthority) {
495
+ throw new Error("Cross-repository PR creation was not preflighted before push");
496
+ }
497
+ const args = before
498
+ ? ["pr", "edit", String(before.number), "--repo", repository, "--title", title, "--body-file", "-"]
499
+ : organizationAuthority
500
+ ? [
501
+ "api", "graphql", "--hostname", this.state.base!.host,
502
+ "-f", `query=${CREATE_PULL_REQUEST_MUTATION}`,
503
+ "-f", `repositoryId=${organizationAuthority.baseRepositoryId}`,
504
+ "-f", `baseRefName=${this.state.base!.ref}`,
505
+ "-f", `headRepositoryId=${organizationAuthority.headRepositoryId}`,
506
+ "-f", `headRefName=${this.target.ref}`,
507
+ "-f", `title=${title}`,
508
+ "-f", `body=${body}`,
509
+ ]
510
+ : [
511
+ "pr", "create", "--repo", repository, "--head",
512
+ sameRepository ? this.target.ref : `${this.target.repository.split("/")[0]}:${this.target.ref}`,
513
+ "--base", this.state.base!.ref, "--title", title, "--body-file", "-",
514
+ ];
515
+ this.state.phase = "blocked";
516
+ const created = await runChecked(this.exec, "gh", args, this.options({ stdin: organizationAuthority ? undefined : body }));
517
+ if (!before && organizationAuthority) {
518
+ parseCreatedUrl(created.stdout, this.state.base!.host, this.state.base!.repository);
519
+ }
520
+ const after = await this.exactCandidate();
521
+ if (!after || after.base.repository.toLowerCase() !== this.state.base!.repository.toLowerCase() ||
522
+ after.base.ref !== this.state.base!.ref || after.title !== title || after.body !== body) {
523
+ throw new Error("Published pull request did not retain canonical identity, title, and body");
667
524
  }
668
- if (this.noTarget) await this.configureNoTargetUpstream(this.state.publicationHead!);
669
525
  this.state.phase = "published";
670
526
  this.state.url = after.url.href;
671
527
  return { kind: "published", url: after.url.href };