@henryqw/pi-pr 4.0.2 → 4.0.4

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.
@@ -5,6 +5,7 @@ import {
5
5
  findExactHeadPullRequests,
6
6
  loadCurrentPullRequest,
7
7
  loadPullRequestPublication,
8
+ preflightPullRequestCreation,
8
9
  readPullRequestBaseRefOid,
9
10
  readTrackingOid,
10
11
  readValidatedRemoteAuthority,
@@ -24,17 +25,13 @@ import {
24
25
  readRemoteOid,
25
26
  requiredOid,
26
27
  requiredText,
27
- resolveRepositoryFetchSource,
28
28
  runChecked,
29
29
  spawnBounded,
30
30
  withWorktreeLock,
31
- type AttemptState,
32
31
  type Exec,
33
32
  type ExecOptions,
34
33
  } from "./pr-execution.ts";
35
34
 
36
- const BASE_RERUN = "Rerun /pr --base=<host>/<owner>/<repository>:<ref>";
37
- const MAX_REMOTE_BRANCHES = 128;
38
35
  const MAX_TITLE_BYTES = 256;
39
36
  const MAX_BODY_BYTES = 64 * 1024;
40
37
  const CREATE_AUTHORITY_QUERY = "query($baseOwner:String!,$baseName:String!,$headOwner:String!,$headName:String!){base:repository(owner:$baseOwner,name:$baseName){id nameWithOwner}head:repository(owner:$headOwner,name:$headName){id nameWithOwner owner{__typename}}createInput:__type(name:\"CreatePullRequestInput\"){inputFields{name}}}";
@@ -60,16 +57,6 @@ type CrossRepositoryCreateAuthority = {
60
57
 
61
58
  export type CreatePullRequestState = {
62
59
  phase: CreatePhase;
63
- attempts: {
64
- fetchBase: AttemptState;
65
- merge: AttemptState;
66
- stage: AttemptState;
67
- continueMerge: AttemptState;
68
- push: AttemptState;
69
- fetchTracking: AttemptState;
70
- setUpstream: AttemptState;
71
- pullRequest: AttemptState;
72
- };
73
60
  base?: CreateBaseAuthority;
74
61
  createAuthority?: CrossRepositoryCreateAuthority;
75
62
  mergeHead?: string;
@@ -107,16 +94,6 @@ function line(output: string, label: string): string {
107
94
  return values[0];
108
95
  }
109
96
 
110
- function configuredValues(output: string): string[] {
111
- if (output === "") return [];
112
- const normalized = output.replace(/\r\n/g, "\n");
113
- const values = normalized.endsWith("\n") ? normalized.slice(0, -1).split("\n") : normalized.split("\n");
114
- if (values.some((value) => !value) || new Set(values).size !== values.length) {
115
- throw new Error("Git configuration returned invalid values");
116
- }
117
- return values;
118
- }
119
-
120
97
  function isRecord(value: unknown): value is Record<string, unknown> {
121
98
  return typeof value === "object" && value !== null && !Array.isArray(value);
122
99
  }
@@ -191,14 +168,6 @@ function parseCreatedUrl(output: string, host: string, baseRepository: string):
191
168
  return url;
192
169
  }
193
170
 
194
- function parseExplicitBase(value: string): Omit<CreateBaseAuthority, "oid" | "fetchSource"> {
195
- const match = /^([a-z0-9](?:[a-z0-9.-]*[a-z0-9])?)\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+):(.+)$/.exec(value);
196
- if (!match || match[2] === "." || match[2] === ".." || match[3] === "." || match[3] === "..") {
197
- throw new Error(`Invalid base authority. ${BASE_RERUN}`);
198
- }
199
- return { host: match[1]!.toLowerCase(), repository: `${match[2]}/${match[3]}`, ref: requiredText(match[4], "base ref") };
200
- }
201
-
202
171
  function resolvedPaths(paths: readonly string[], expected: readonly string[]): string[] {
203
172
  if (!Array.isArray(paths)) throw new TypeError("resolvedPaths must be an array");
204
173
  const parsed = parseNulPaths(`${paths.join("\0")}${paths.length ? "\0" : ""}`, "Resolved conflict paths");
@@ -209,21 +178,17 @@ function resolvedPaths(paths: readonly string[], expected: readonly string[]): s
209
178
  }
210
179
 
211
180
  export class PullRequestCreator {
212
- readonly state: CreatePullRequestState = {
213
- phase: "unprepared",
214
- attempts: {
215
- fetchBase: "none", merge: "none", stage: "none", continueMerge: "none", push: "none",
216
- fetchTracking: "none", setUpstream: "none", pullRequest: "none",
217
- },
218
- };
181
+ readonly state: CreatePullRequestState = { phase: "unprepared" };
219
182
 
220
183
  private readonly cwd: string;
221
184
  private readonly target: PullRequestTarget;
222
185
  private readonly noTarget: boolean;
186
+ private noTargetUpstreamConfigured = false;
223
187
  private readonly signal?: AbortSignal;
224
188
  private readonly agentDir?: string;
225
189
  private readonly exec: Exec;
226
190
  private readonly load: Load;
191
+ private explicitBase?: string;
227
192
 
228
193
  constructor(options: CreatePullRequestOptions) {
229
194
  if (!options.target || options.target.remoteOid !== null && !requiredOid(options.target.remoteOid, "remote OID")) {
@@ -257,7 +222,7 @@ export class PullRequestCreator {
257
222
  }
258
223
 
259
224
  private async freshNone(): Promise<void> {
260
- const discovery = await this.load(this.pi(), this.context());
225
+ const discovery = await this.load(this.pi(), this.context(), undefined, undefined, this.explicitBase);
261
226
  if (discovery.kind !== "none" || !sameTarget(this.target, discovery.creationTarget)) {
262
227
  throw new Error("PR creation cancelled: fresh complete discovery is no longer none");
263
228
  }
@@ -265,71 +230,6 @@ export class PullRequestCreator {
265
230
  if (branch !== this.target.branch) throw new Error("PR creation cancelled: current branch changed");
266
231
  }
267
232
 
268
- private async originAuthority() {
269
- return await readValidatedRemoteAuthority(this.pi(), this.context(), "origin");
270
- }
271
-
272
- private async inferBaseRef(): Promise<{ authority: Awaited<ReturnType<PullRequestCreator["originAuthority"]>>; ref: string }> {
273
- const authority = await this.originAuthority();
274
- await runChecked(this.exec, "git", [
275
- "fetch", "--prune", "--no-write-fetch-head", "--no-tags", "--no-recurse-submodules",
276
- authority.fetchSource, "+refs/heads/*:refs/remotes/origin/*",
277
- ], this.options());
278
- const refs = await runChecked(this.exec, "git", [
279
- "for-each-ref", "--format=%(refname)%09%(objectname)%09%(symref)", "refs/remotes/origin",
280
- ], this.options());
281
- const rows = configuredValues(refs.stdout);
282
- if (rows.length > MAX_REMOTE_BRANCHES) throw new Error(`${BASE_RERUN}; more than ${MAX_REMOTE_BRANCHES} base candidates exist`);
283
- const originOwnsTarget = authority.host === this.target.host &&
284
- authority.repository.toLowerCase() === this.target.repository.toLowerCase();
285
- const candidates: Array<{ ref: string; score: number }> = [];
286
- for (const row of rows) {
287
- const parts = row.split("\t");
288
- if (parts.length !== 3) throw new Error(`Base inference is unsafe. ${BASE_RERUN}`);
289
- const prefix = "refs/remotes/origin/";
290
- if (!parts[0]!.startsWith(prefix)) throw new Error(`Base inference is unsafe. ${BASE_RERUN}`);
291
- const ref = parts[0]!.slice(prefix.length);
292
- if (ref === "HEAD" || originOwnsTarget && ref === this.target.ref) continue;
293
- if (!ref || parts[2] !== "") throw new Error(`Base inference is unsafe. ${BASE_RERUN}`);
294
- const oid = requiredOid(parts[1], "remote base OID");
295
- const distance = line((await runChecked(this.exec, "git", ["rev-list", "--left-right", "--count", `HEAD...${oid}`], this.options())).stdout, "base distance");
296
- const counts = /^(\d+)\s+(\d+)$/.exec(distance);
297
- if (!counts) throw new Error(`Base inference is unsafe. ${BASE_RERUN}`);
298
- const score = Number(counts[1]) + Number(counts[2]);
299
- if (!Number.isSafeInteger(score)) throw new Error(`Base inference is unsafe. ${BASE_RERUN}`);
300
- candidates.push({ ref, score });
301
- }
302
- if (!candidates.length) throw new Error(`Base cannot be inferred. ${BASE_RERUN}`);
303
- const minimum = Math.min(...candidates.map(({ score }) => score));
304
- const nearest = candidates.filter(({ score }) => score === minimum);
305
- if (nearest.length !== 1) throw new Error(`Base is ambiguous. ${BASE_RERUN}`);
306
- return { authority, ref: nearest[0]!.ref };
307
- }
308
-
309
- private async resolveBase(explicit?: string): Promise<Omit<CreateBaseAuthority, "oid">> {
310
- if (explicit !== undefined) {
311
- const parsed = parseExplicitBase(explicit);
312
- if (parsed.host !== this.target.host.toLowerCase()) {
313
- throw new Error("PR creation base and head must use the same GitHub host");
314
- }
315
- await runChecked(this.exec, "git", ["check-ref-format", "--branch", parsed.ref], this.options());
316
- return { ...parsed, fetchSource: await resolveRepositoryFetchSource(this.exec, this.options(), parsed) };
317
- }
318
- const configured = await runChecked(this.exec, "git", [
319
- "config", "--get-all", `branch.${this.target.branch}.gh-merge-base`,
320
- ], this.options(), [0, 1]);
321
- const values = configured.code === 1 && configured.stdout === "" ? [] : configuredValues(configured.stdout);
322
- if (values.length > 1) throw new Error(`Configured base is ambiguous. ${BASE_RERUN}`);
323
- if (values.length === 1) {
324
- await runChecked(this.exec, "git", ["check-ref-format", "--branch", values[0]!], this.options());
325
- const authority = await this.originAuthority();
326
- return { ...authority, ref: values[0]! };
327
- }
328
- const inferred = await this.inferBaseRef();
329
- await runChecked(this.exec, "git", ["check-ref-format", "--branch", inferred.ref], this.options());
330
- return { ...inferred.authority, ref: inferred.ref };
331
- }
332
-
333
233
  private async liveBase(): Promise<string> {
334
234
  if (!this.state.base) throw new Error("PR creation base is unavailable");
335
235
  return await readPullRequestBaseRefOid(this.pi(), this.context(), this.state.base);
@@ -358,40 +258,48 @@ export class PullRequestCreator {
358
258
  }
359
259
 
360
260
  async prepare(explicitBase?: string): Promise<CreatePullRequestResult> {
361
- if (this.state.phase !== "unprepared" || this.state.attempts.fetchBase !== "none") {
261
+ if (this.state.phase !== "unprepared") {
362
262
  throw new Error("PR creation prepare action was already consumed");
363
263
  }
364
264
  return await withWorktreeLock(this.cwd, async () => {
265
+ this.explicitBase = explicitBase;
365
266
  await this.freshNone();
366
- const base = await this.resolveBase(explicitBase);
367
- if (base.host !== this.target.host.toLowerCase()) {
368
- throw new Error("PR creation base and head must use the same GitHub host");
369
- }
370
- const oid = await readPullRequestBaseRefOid(this.pi(), this.context(), base);
371
- this.state.base = { ...base, oid };
372
- this.state.attempts.fetchBase = "attempting";
373
- try {
374
- await runChecked(this.exec, "git", [
375
- "fetch", "--no-write-fetch-head", "--no-tags", "--no-recurse-submodules", base.fetchSource, oid,
376
- ], this.options());
377
- await runChecked(this.exec, "git", ["cat-file", "-e", `${oid}^{commit}`], this.options());
378
- this.state.attempts.fetchBase = "applied";
379
- } catch (error) {
380
- this.state.attempts.fetchBase = "unknown";
381
- throw error;
267
+ const preflight = await preflightPullRequestCreation(
268
+ this.pi(),
269
+ this.context(),
270
+ this.target,
271
+ this.explicitBase,
272
+ );
273
+ if (preflight.ahead === 0) {
274
+ throw new Error("PR creation requires at least one commit ahead of the selected base");
382
275
  }
276
+ const { base } = preflight;
277
+ this.state.base = {
278
+ host: base.host,
279
+ repository: base.repository,
280
+ ref: base.ref,
281
+ oid: base.oid,
282
+ fetchSource: base.fetchSource,
283
+ };
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());
383
289
  await this.freshNone();
384
- if (await this.liveBase() !== oid) throw new Error("PR creation cancelled: base ref moved during prepare");
385
- const mergeBase = requiredOid(line((await runChecked(this.exec, "git", ["merge-base", "HEAD", oid], this.options())).stdout, "merge base"), "merge base");
290
+ if (await readHead(this.exec, this.options()) !== preflight.head) {
291
+ throw new Error("PR creation cancelled: local HEAD changed during prepare");
292
+ }
293
+ if (await this.liveBase() !== base.oid) throw new Error("PR creation cancelled: base ref moved during prepare");
386
294
  this.state.phase = "prepared";
387
- return { kind: "prepared", base: { ...this.state.base }, mergeBase };
295
+ return { kind: "prepared", base: { ...this.state.base }, mergeBase: base.mergeBase };
388
296
  }, { agentDir: this.agentDir, signal: this.signal });
389
297
  }
390
298
 
391
299
  private async verifyMerge(originalHead: string): Promise<{ head: string; fastForward: boolean }> {
392
300
  const base = this.state.base!;
393
301
  const head = await readHead(this.exec, this.options());
394
- const commits = line((await runChecked(this.exec, "git", ["rev-list", "--parents", "-n", "1", "HEAD"], this.options())).stdout, "merge parents")
302
+ const commits = line((await runChecked(this.exec, "git", ["rev-list", "--parents", "-n", "1", head], this.options())).stdout, "merge parents")
395
303
  .split(" ").map((value, index) => requiredOid(value, index ? "merge parent" : "merged HEAD"));
396
304
  if (commits[0] !== head) throw new Error("PR creation merge verification returned a different HEAD");
397
305
  let fastForward = false;
@@ -419,7 +327,7 @@ export class PullRequestCreator {
419
327
  }
420
328
 
421
329
  async merge(): Promise<CreatePullRequestResult> {
422
- if (this.state.phase !== "prepared" || !this.state.base || this.state.attempts.merge !== "none") {
330
+ if (this.state.phase !== "prepared" || !this.state.base) {
423
331
  throw new Error("PR creation is not prepared for merge");
424
332
  }
425
333
  return await withWorktreeLock(this.cwd, async () => {
@@ -431,35 +339,17 @@ export class PullRequestCreator {
431
339
  this.state.mergeHead = originalHead;
432
340
  return { kind: "verified", head: originalHead, fastForward: false };
433
341
  }
434
- this.state.attempts.merge = "attempting";
435
- let result;
436
- try {
437
- result = await this.exec("git", ["merge", "--no-edit", this.state.base!.oid], this.options());
438
- } catch (error) {
439
- this.state.attempts.merge = "unknown";
440
- throw error;
441
- }
442
- if (result.killed) {
443
- this.state.attempts.merge = "unknown";
444
- throw new Error("git merge was killed; its outcome is unknown");
445
- }
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");
446
345
  if (result.code === 0) {
447
- try {
448
- const verified = await this.verifyMerge(originalHead);
449
- this.state.attempts.merge = "applied";
450
- return { kind: "verified", ...verified };
451
- } catch (error) {
452
- this.state.attempts.merge = "unknown";
453
- throw error;
454
- }
346
+ const verified = await this.verifyMerge(originalHead);
347
+ return { kind: "verified", ...verified };
455
348
  }
456
349
  try {
457
350
  const paths = await this.captureConflict(originalHead);
458
- this.state.attempts.merge = "applied";
459
351
  return { kind: "conflict", paths };
460
352
  } catch (error) {
461
- this.state.attempts.merge = "blocked";
462
- this.state.phase = "blocked";
463
353
  throw new Error(`git merge failed: ${result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`}; ${error instanceof Error ? error.message : String(error)}`);
464
354
  }
465
355
  }, { agentDir: this.agentDir, signal: this.signal });
@@ -469,9 +359,6 @@ export class PullRequestCreator {
469
359
  if (this.state.phase !== "conflict-awaiting-user" || !this.state.conflict || !this.state.base) {
470
360
  throw new Error("PR creation has no conflict awaiting continuation");
471
361
  }
472
- if (this.state.attempts.stage !== "none" || this.state.attempts.continueMerge !== "none") {
473
- throw new Error("PR creation conflict continuation was already consumed");
474
- }
475
362
  const paths = resolvedPaths(pathsInput, this.state.conflict.paths);
476
363
  return await withWorktreeLock(this.cwd, async () => {
477
364
  await this.freshNone();
@@ -481,26 +368,13 @@ export class PullRequestCreator {
481
368
  if (mergeHead !== this.state.base!.oid) throw new Error("PR creation merge context changed");
482
369
  const status = await runChecked(this.exec, "git", ["status", "--porcelain=v2", "-z", "--untracked-files=all"], this.options());
483
370
  assertOnlyDeclaredStatusChanged(this.state.conflict!.statusBaseline, status.stdout, paths);
484
- this.state.attempts.stage = "attempting";
485
- try {
486
- await runChecked(this.exec, "git", ["add", "--", ...paths], this.options());
487
- this.state.attempts.stage = "applied";
488
- } catch (error) {
489
- this.state.attempts.stage = "unknown";
490
- throw error;
491
- }
371
+ this.state.phase = "blocked";
372
+ await runChecked(this.exec, "git", ["add", "--", ...paths], this.options());
492
373
  const unmerged = parseNulPaths((await runChecked(this.exec, "git", ["diff", "--name-only", "-z", "--diff-filter=U"], this.options())).stdout, "Unmerged paths");
493
374
  if (unmerged.length) throw new Error(`Conflict paths remain unresolved: ${unmerged.join(", ")}`);
494
- this.state.attempts.continueMerge = "attempting";
495
- try {
496
- await runChecked(this.exec, "git", ["-c", "core.editor=true", "merge", "--continue"], this.options());
497
- const verified = await this.verifyMerge(this.state.conflict!.originalHead);
498
- this.state.attempts.continueMerge = "applied";
499
- return { kind: "verified", ...verified };
500
- } catch (error) {
501
- this.state.attempts.continueMerge = "unknown";
502
- throw error;
503
- }
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 };
504
378
  }, { agentDir: this.agentDir, signal: this.signal });
505
379
  }
506
380
 
@@ -512,30 +386,16 @@ export class PullRequestCreator {
512
386
  fetchSource: this.target.fetchSource,
513
387
  remoteOid: head,
514
388
  };
515
- this.state.attempts.fetchTracking = "attempting";
516
- try {
517
- await fetchBranchTrackingRef(this.pi(), this.context(), target);
518
- if (await readTrackingOid(this.pi(), this.context(), branchTrackingRef(target)) !== head) {
519
- throw new Error("Fetched tracking ref did not match published HEAD");
520
- }
521
- this.state.attempts.fetchTracking = "applied";
522
- } catch (error) {
523
- this.state.attempts.fetchTracking = "unknown";
524
- throw error;
525
- }
526
- this.state.attempts.setUpstream = "attempting";
527
- try {
528
- await setBranchUpstream(this.pi(), this.context(), target);
529
- await verifyBranchUpstream(this.pi(), this.context(), target);
530
- this.state.attempts.setUpstream = "applied";
531
- } catch (error) {
532
- this.state.attempts.setUpstream = "unknown";
533
- 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");
534
392
  }
393
+ await setBranchUpstream(this.pi(), this.context(), target);
394
+ await verifyBranchUpstream(this.pi(), this.context(), target);
535
395
  }
536
396
 
537
397
  async push(): Promise<CreatePullRequestResult> {
538
- if (this.state.phase !== "verified" || !this.state.base || this.state.attempts.push !== "none") {
398
+ if (this.state.phase !== "verified" || !this.state.base) {
539
399
  throw new Error("PR creation is not ready to push");
540
400
  }
541
401
  return await withWorktreeLock(this.cwd, async () => {
@@ -554,29 +414,26 @@ export class PullRequestCreator {
554
414
  if (await this.liveBase() !== this.state.base!.oid) throw new Error("PR creation cancelled: frozen base moved");
555
415
  if (await this.requireCleanHead() !== head) throw new Error("PR creation cancelled: local HEAD changed before push");
556
416
  this.state.publicationHead = head;
557
- this.state.attempts.push = "attempting";
558
- try {
559
- await runChecked(this.exec, "git", [
560
- "push", "--porcelain", `--force-with-lease=refs/heads/${this.target.ref}:${original ?? ""}`,
561
- "--recurse-submodules=no", "--", this.target.fetchSource, `${head}:refs/heads/${this.target.ref}`,
562
- ], this.options());
563
- if (await readRemoteOid(this.exec, this.options(), this.target.fetchSource, this.target.ref) !== head) {
564
- throw new Error("Published remote ref did not match captured HEAD");
565
- }
566
- this.state.attempts.push = "applied";
567
- } catch (error) {
568
- this.state.attempts.push = "unknown";
569
- 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");
570
424
  }
571
425
  this.state.phase = "pushed";
572
- if (this.noTarget) await this.configureNoTargetUpstream(head);
426
+ if (this.noTarget) {
427
+ await this.configureNoTargetUpstream(head);
428
+ this.noTargetUpstreamConfigured = true;
429
+ }
573
430
  return { kind: "pushed", head };
574
431
  }, { agentDir: this.agentDir, signal: this.signal });
575
432
  }
576
433
 
577
434
  private async publishedAuthority(): Promise<void> {
578
435
  const head = this.state.publicationHead!;
579
- if (this.noTarget && this.state.attempts.setUpstream !== "applied") {
436
+ if (this.noTarget && !this.noTargetUpstreamConfigured) {
580
437
  const branch = line((await runChecked(this.exec, "git", ["branch", "--show-current"], this.options())).stdout, "current branch");
581
438
  const authority = await readValidatedRemoteAuthority(this.pi(), this.context(), this.target.remote);
582
439
  if (branch !== this.target.branch || authority.host !== this.target.host ||
@@ -587,7 +444,7 @@ export class PullRequestCreator {
587
444
  }
588
445
  return;
589
446
  }
590
- const discovery = await this.load(this.pi(), this.context());
447
+ const discovery = await this.load(this.pi(), this.context(), undefined, undefined, this.explicitBase);
591
448
  if (discovery.kind === "none") {
592
449
  if (!sameTarget(this.target, discovery.creationTarget, head)) throw new Error("Published target authority changed");
593
450
  return;
@@ -614,7 +471,7 @@ export class PullRequestCreator {
614
471
  }
615
472
 
616
473
  async publish(titleInput: string, body: string): Promise<CreatePullRequestResult> {
617
- if (this.state.phase !== "pushed" || !this.state.base || !this.state.publicationHead || this.state.attempts.pullRequest !== "none") {
474
+ if (this.state.phase !== "pushed" || !this.state.base || !this.state.publicationHead) {
618
475
  throw new Error("PR creation is not ready to publish metadata");
619
476
  }
620
477
  const title = requiredText(titleInput, "pull request title");
@@ -655,30 +512,19 @@ export class PullRequestCreator {
655
512
  sameRepository ? this.target.ref : `${this.target.repository.split("/")[0]}:${this.target.ref}`,
656
513
  "--base", this.state.base!.ref, "--title", title, "--body-file", "-",
657
514
  ];
658
- this.state.attempts.pullRequest = "attempting";
659
- try {
660
- const created = await runChecked(this.exec, "gh", args, this.options({ stdin: organizationAuthority ? undefined : body }));
661
- if (!before && organizationAuthority) {
662
- parseCreatedUrl(created.stdout, this.state.base!.host, this.state.base!.repository);
663
- }
664
- } catch (error) {
665
- this.state.attempts.pullRequest = "unknown";
666
- throw error;
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);
667
519
  }
668
- try {
669
- const after = await this.exactCandidate();
670
- if (!after || after.base.repository.toLowerCase() !== this.state.base!.repository.toLowerCase() ||
671
- after.base.ref !== this.state.base!.ref || after.title !== title || after.body !== body) {
672
- throw new Error("Published pull request did not retain canonical identity, title, and body");
673
- }
674
- this.state.attempts.pullRequest = "applied";
675
- this.state.phase = "published";
676
- this.state.url = after.url.href;
677
- return { kind: "published", url: after.url.href };
678
- } catch (error) {
679
- this.state.attempts.pullRequest = "unknown";
680
- throw error;
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");
681
524
  }
525
+ this.state.phase = "published";
526
+ this.state.url = after.url.href;
527
+ return { kind: "published", url: after.url.href };
682
528
  }, { agentDir: this.agentDir, signal: this.signal });
683
529
  }
684
530
  }