Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/jig/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ func engineExecutor(
Cancelled: signalCtx.Done(),
FreshenLease: execution.FreshenLease,
})
// A direct run never publishes, so it never repairs CI either.
if outcome.Continuation != nil {
outcome.Continuation.Release()
}
return controlplane.DirectOutcome{
State: outcome.State,
Result: outcome.Result,
Expand Down
29 changes: 24 additions & 5 deletions cmd/jig/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,23 +272,37 @@ func workerAttemptRunner(
return worker.Outcome{State: protocol.AttemptFailed, Error: err.Error()}
}
// Belt to the engine's braces: the seeded credentials in the
// ephemeral HOME die with the attempt on every exit path (KTD11).
defer func() {
// ephemeral HOME die with the attempt on every exit path (KTD11) —
// unless a CI repair continuation still needs the scratch, in which
// case they die when it is released.
kept := false
removeScratch := func() {
if err := os.RemoveAll(filepath.Join(scratchRoot, attemptID)); err != nil {
logger.Warn("attempt_scratch_destroy_failed", "attempt_id", attemptID, "error", err)
}
}
defer func() {
if !kept {
removeScratch()
}
}()
trace, err := host().OpenTrace(prepared)
if err != nil {
return infraFail(err)
}
// The drain runs on a detached context: an interrupted attempt still
// has events worth delivering, and a dead context would drop the tail
// of exactly the run an operator most wants to read.
defer func() {
// of exactly the run an operator most wants to read. A continuation's
// repair rounds emit into the same stream, so it stays open for them.
closeTrace := func() {
if err := trace.Close(context.WithoutCancel(ctx)); err != nil {
logger.Warn("attempt_trace_close_failed", "attempt_id", attemptID, "error", err)
}
}
defer func() {
if !kept {
closeTrace()
}
}()

runner, err := engine.New(engine.Config{
Expand All @@ -308,14 +322,19 @@ func workerAttemptRunner(
// (which rides the heartbeat response) and the operator's Ctrl-C. The
// engine watches one channel, so they are merged rather than the
// second one being dropped.
return runner.Execute(context.WithoutCancel(ctx), engine.Attempt{
outcome := runner.Execute(context.WithoutCancel(ctx), engine.Attempt{
Claim: prepared.Claim,
WorktreePath: prepared.WorktreePath,
Branch: prepared.Branch,
BaseSHA: prepared.BaseSHA,
Cancelled: anyClosed(signalCtx.Done(), prepared.Cancelled()),
FreshenLease: prepared.FreshenLease,
})
kept = worker.DeferToContinuation(&outcome, func() {
closeTrace()
removeScratch()
})
return outcome
})
}

Expand Down
17 changes: 9 additions & 8 deletions docs/plans/2026-09-27-001-feat-ci-repair-plan.md

Large diffs are not rendered by default.

Loading
Loading