Skip to content

Launch a ticket stacked on another ticket's PR - #204

Open
thrawn01 wants to merge 1 commit into
mainfrom
thrawn01/eng-594-launch-a-ticket-stacked-on-another-tickets-pr
Open

thrawn01 wants to merge 1 commit into
mainfrom
thrawn01/eng-594-launch-a-ticket-stacked-on-another-tickets-pr

Conversation

@thrawn01

Copy link
Copy Markdown
Contributor

Purpose

When one ticket builds on another, its worker can't start until the other merges, because a worker's branch always starts from the default branch. This gives rules the capability to start a worker from another ticket's unmerged PR, and to remember where that branch began. That start point is the one fact needed later to move the branch onto the default branch. It decides nothing about when anyone should stack. With no rule using stackOn, nothing changes.

whenever({ name: "stack-on-blocker", each: "ticket",
  if: (t, ctx) => /* ... */ ctx.pr.of(ctx.ticket.blockers(t)[0].ticket) !== undefined,
  then: (t, ctx) => ctx.worker.queueLaunch(t, {
    stackOn: ctx.pr.of(ctx.ticket.blockers(t)[0].ticket),
    kickoff: `/dispatch:build ${t.id} --base ${ctx.ticket.blockers(t)[0].ticket.branch}`,
    once: ["stack"],
  }) });

The launch receipt reads ok: workspace w2, stacked on ENG-1 at 0adc9f1 (thrawn01/eng-1), and t.stack reads {parent: "ENG-1", forkPoint: "<full sha>"} from then on. This is the stacked-launch piece of ENG-568's blueprint. pr.conflict, exempt and rules/stacking.ts are later pieces.

Implementation

  • internal/stackrecord owns the stack record, two keys in the clone's git config (branch.<b>.dispatchStackParent and dispatchStackForkPoint), and the both-or-neither rule. Read matches git's lowercased key names. A partial record reads as partial.
  • The launcher takes Request.Stack (stack parent ticket, its branch, the head). On a brand-new branch only, it fetches the parent branch, writes the record, then creates the branch at the head. A failed write or switch is checkout_failed with the lease returned. A relaunch or an existing branch never writes or changes a record.
  • The world reads each repo's records once per local read and saves them, and any read error, in history, so replay and explain see the same t.stack.
  • Rule API: t.stack, t.branch, Blocker.ticket, ctx.pr.of(t) and queueLaunch's stackOn. ctx.pr.of is undefined when dispatch read the PR and found none, and unknown when it didn't read it this cycle. Generated types, the scenario fixtures (ticket(id, { branch, stack }), unread("stack")), docs, the glossary entries and ADR 0004 are included.
  • dispatch:build step 6 passes a kickoff's --base <branch> to gh pr create.
  • rules/stack.test.ts and stack_test.go cover the acceptance criteria through dispatch tick, explain and replay with real git.

Changes from build

  • Design: a failed switch clears the record. Shipped: it clears it only when the branch doesn't exist. A switch that fails after git created the branch (a locked HEAD reproduces it) left a stacked branch with no record for good, because the next launch sees an existing branch. Not in the design file, which is a Linear document, so only noted here and on the ticket.
  • Design: Request carries the record beside Base. Shipped: Request.Stack holds the parent ticket, its branch and the head, and overrides Base, since the launcher needs the branch to fetch it and the record doesn't store it.
  • Design: unknown when the read fails or the record is partial. Shipped also: unknown for a repo with no entry in saved history, so history from before this change replays as unread rather than "no record".
  • The executor refuses a stackOn missing its ticket, branch or head, where the design says nothing.

Verification

make ci passes (vet, go test -tags tests_slow ./..., shellcheck), and the slow suite passed twice more with -count=1. I disabled the record clear, and separately the lease return and the keep-the-record check, and each made its test fail.

Checked by: a test-review agent against the acceptance criteria, three code-review agents (correctness, fit with the ticket, next reader), a skill review of skills/build/SKILL.md, and a second correctness round on the fixes, which found nothing needing a change. Not run: make test-drift (it needs the installed treehouse, gh, herdr and Claude) and any eval, which the ticket says to skip. Left as is after the second round:

  • another process creating the branch between dispatch's check and its switch would be switched onto instead of refused;
  • a stacked launch on a branch that exists only on origin comes up unstacked without saying so;
  • a fetch that fails for an origin-only branch creates it locally from the stack parent's head, as it did from the default branch before.

Fixes ENG-594

A rule can queue a launch with stackOn: pr. The launcher starts the
ticket's branch at that PR's head and writes a stack record into the
clone's git config, which rules read as t.stack. Rules also get t.branch,
Blocker.ticket and ctx.pr.of(t), and dispatch:build opens its PR against
a --base branch the kickoff names.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant