@@ -60,6 +60,12 @@ struct StepOutcome {
6060 /// `None` only for steps short-circuited because a predecessor failed
6161 /// or the build was cancelled before they could run.
6262 summary : Option < StepResultSummary > ,
63+ /// Set when this step did not complete successfully — it failed, timed
64+ /// out, was cancelled, or was itself skipped. Descendants gate on this
65+ /// (not on `exit_code`) so a skip propagates transitively: a skipped
66+ /// step reports `exit_code == 0`, so the exit code alone cannot
67+ /// distinguish "passed" from "skipped" and the cascade would break.
68+ failed_or_skipped : bool ,
6369}
6470
6571type StepFuture = futures:: future:: Shared < BoxFuture < ' static , StepOutcome > > ;
@@ -186,8 +192,12 @@ pub(crate) async fn run(
186192 let pred_outcomes: Vec < StepOutcome > =
187193 join_all ( preds. iter ( ) . map ( |( _, f) | f. clone ( ) ) ) . await ;
188194
189- // Early exit if any predecessor failed or the build was cancelled.
190- if cancel. is_cancelled ( ) || pred_outcomes. iter ( ) . any ( |o| o. exit_code != 0 ) {
195+ // Early exit if any predecessor failed/was skipped, or the build
196+ // was cancelled. Gating on `failed_or_skipped` (not `exit_code`)
197+ // is what makes the skip propagate transitively: a skipped
198+ // predecessor reports `exit_code == 0`, so an exit-code-only gate
199+ // would let a skipped step's descendants run anyway.
200+ if cancel. is_cancelled ( ) || pred_outcomes. iter ( ) . any ( |o| o. failed_or_skipped ) {
191201 let status = if cancel. is_cancelled ( ) {
192202 StepStatus :: Canceled
193203 } else {
@@ -203,6 +213,7 @@ pub(crate) async fn run(
203213 exit_code : None ,
204214 duration_ms : 0 ,
205215 } ) ,
216+ failed_or_skipped : true ,
206217 } ;
207218 }
208219
@@ -249,6 +260,7 @@ pub(crate) async fn run(
249260 exit_code : Some ( 1 ) ,
250261 duration_ms : 0 ,
251262 } ) ,
263+ failed_or_skipped : true ,
252264 }
253265 }
254266 }
@@ -285,6 +297,22 @@ pub(crate) async fn run(
285297 let outcomes: Vec < StepOutcome > = join_all ( pending) . await ;
286298 let any_failed = outcomes. iter ( ) . any ( |o| o. exit_code != 0 ) ;
287299
300+ // Reap ephemeral leaf snapshots. Uncached steps commit an `ephemeral:*`
301+ // image for downstream container lineage; the cache registry never tracks
302+ // them, so once the run is over nothing else will. Collect every such
303+ // snapshot the steps produced and ask the default runner to remove them
304+ // (best-effort — failures are logged, not fatal).
305+ let ephemeral: Vec < SnapshotRef > = outcomes
306+ . iter ( )
307+ . filter_map ( |o| o. snapshot . clone ( ) )
308+ . filter ( |s| s. 0 . starts_with ( "ephemeral:" ) )
309+ . collect ( ) ;
310+ if !ephemeral. is_empty ( )
311+ && let Some ( runner) = runner_registry. resolve ( None )
312+ {
313+ runner. reap_snapshots ( ephemeral) . await ;
314+ }
315+
288316 // Derive the overall verdict. Timeout wins (it also fired cancellation);
289317 // then cancellation; then any failed step; otherwise the build passed.
290318 let status = if timed_out {
@@ -363,10 +391,13 @@ async fn execute_step(
363391 let step_key = step_wire. key . clone ( ) ;
364392 let display_name = step_wire. label . clone ( ) . unwrap_or_else ( || {
365393 let cmd = step_wire. cmd . trim ( ) ;
366- if cmd. len ( ) <= 40 {
394+ if cmd. chars ( ) . count ( ) <= 40 {
367395 cmd. to_owned ( )
368396 } else {
369- format ! ( "{}…" , & cmd[ ..39 ] )
397+ // Truncate on a char boundary, not a byte offset: `&cmd[..39]`
398+ // panics if byte 39 falls inside a multibyte UTF-8 sequence.
399+ let truncated: String = cmd. chars ( ) . take ( 39 ) . collect ( ) ;
400+ format ! ( "{truncated}…" )
370401 }
371402 } ) ;
372403 let env_map = transition. env ;
@@ -476,6 +507,7 @@ async fn execute_step(
476507 exit_code : Some ( 124 ) ,
477508 duration_ms : dur_ms,
478509 } ) ,
510+ failed_or_skipped : true ,
479511 } ) ;
480512 }
481513 }
@@ -522,6 +554,7 @@ async fn execute_step(
522554 exit_code : Some ( sr. exit_code ) ,
523555 duration_ms : dur_ms,
524556 } ) ,
557+ failed_or_skipped : sr. exit_code != 0 ,
525558 } )
526559 }
527560 Err ( e) => {
0 commit comments