@@ -40,6 +40,7 @@ const DEFAULT_LANE_TIMEOUT_MS = 120 * 60 * 1000;
4040const DEFAULT_LANE_START_STAGGER_MS = 2_000 ;
4141const DEFAULT_STATUS_INTERVAL_MS = 30_000 ;
4242const DEFAULT_PREFLIGHT_RUN_TIMEOUT_MS = 60_000 ;
43+ const CLEANUP_SMOKE_NAME = "cleanup-smoke" ;
4344export const SHELL_CAPTURE_MAX_CHARS = 1024 * 1024 ;
4445export const LOG_TAIL_MAX_BYTES = 1024 * 1024 ;
4546const DEFAULT_TIMINGS_FILE = path . join ( ROOT_DIR , ".artifacts/docker-tests/lane-timings.json" ) ;
@@ -456,8 +457,10 @@ async function writeFailureIndex(logDir, summary) {
456457 const failures = Array . isArray ( summary . failures )
457458 ? summary . failures
458459 : ( summary . lanes ?? [ ] ) . filter ( ( lane ) => lane . status !== 0 ) ;
460+ const workflowRerunFailures = failures . filter ( ( failure ) => failure . targetable !== false ) ;
459461 const lanes = failures . map ( ( failure ) => ( {
460- ghWorkflowCommand : githubWorkflowRerunCommand ( [ failure . name ] , ref ) ,
462+ ghWorkflowCommand :
463+ failure . targetable === false ? undefined : githubWorkflowRerunCommand ( [ failure . name ] , ref ) ,
461464 image : failure . image ,
462465 imageKind : failure . imageKind ,
463466 lane : failure . name ,
@@ -466,13 +469,14 @@ async function writeFailureIndex(logDir, summary) {
466469 noOutputTimedOut : failure . noOutputTimedOut ,
467470 rerunCommand : failure . rerunCommand ,
468471 status : failure . status ,
472+ targetable : failure . targetable ,
469473 timedOut : failure . timedOut ,
470474 } ) ) ;
471475 const failureIndex = {
472476 combinedGhWorkflowCommand :
473- lanes . length > 0
477+ workflowRerunFailures . length > 0
474478 ? githubWorkflowRerunCommand (
475- lanes . map ( ( lane ) => lane . lane ) ,
479+ workflowRerunFailures . map ( ( failure ) => failure . name ) ,
476480 ref ,
477481 )
478482 : undefined ,
@@ -712,6 +716,85 @@ async function runForeground(label, command, env) {
712716 }
713717}
714718
719+ async function recordCleanupSmokeFailure ( error , baseEnv , logDir , command , startedAtMs ) {
720+ const status = 1 ;
721+ const logFile = path . join ( logDir , `${ CLEANUP_SMOKE_NAME } .log` ) ;
722+ const message = error instanceof Error ? error . message : String ( error ) ;
723+ await fs . promises . writeFile (
724+ logFile ,
725+ [
726+ `==> [${ CLEANUP_SMOKE_NAME } ] command: ${ command } ` ,
727+ `==> [${ CLEANUP_SMOKE_NAME } ] status: ${ status } ` ,
728+ `==> [${ CLEANUP_SMOKE_NAME } ] error: ${ message } ` ,
729+ ]
730+ . filter ( Boolean )
731+ . join ( "\n" ) ,
732+ ) ;
733+ return {
734+ command,
735+ attempts : [
736+ {
737+ attempt : 1 ,
738+ elapsedSeconds : phaseElapsedSeconds ( startedAtMs ) ,
739+ finishedAt : new Date ( ) . toISOString ( ) ,
740+ noOutputTimedOut : false ,
741+ startedAt : new Date ( startedAtMs ) . toISOString ( ) ,
742+ status,
743+ timedOut : false ,
744+ } ,
745+ ] ,
746+ elapsedSeconds : phaseElapsedSeconds ( startedAtMs ) ,
747+ finishedAt : new Date ( ) . toISOString ( ) ,
748+ image : baseEnv . OPENCLAW_DOCKER_E2E_IMAGE ,
749+ logFile,
750+ name : CLEANUP_SMOKE_NAME ,
751+ noOutputTimedOut : false ,
752+ rerunCommand : command ,
753+ startedAt : new Date ( startedAtMs ) . toISOString ( ) ,
754+ status,
755+ targetable : false ,
756+ timedOut : false ,
757+ } ;
758+ }
759+
760+ async function runCleanupSmoke ( baseEnv , logDir , command , startedAtMs ) {
761+ const logFile = path . join ( logDir , `${ CLEANUP_SMOKE_NAME } .log` ) ;
762+ const result = await runShellCommand ( {
763+ command,
764+ env : baseEnv ,
765+ label : CLEANUP_SMOKE_NAME ,
766+ logFile,
767+ } ) ;
768+ if ( result . status === 0 ) {
769+ return undefined ;
770+ }
771+ return {
772+ command,
773+ attempts : [
774+ {
775+ attempt : 1 ,
776+ elapsedSeconds : phaseElapsedSeconds ( startedAtMs ) ,
777+ finishedAt : new Date ( ) . toISOString ( ) ,
778+ noOutputTimedOut : result . noOutputTimedOut ,
779+ startedAt : new Date ( startedAtMs ) . toISOString ( ) ,
780+ status : result . status ,
781+ timedOut : result . timedOut ,
782+ } ,
783+ ] ,
784+ elapsedSeconds : phaseElapsedSeconds ( startedAtMs ) ,
785+ finishedAt : new Date ( ) . toISOString ( ) ,
786+ image : baseEnv . OPENCLAW_DOCKER_E2E_IMAGE ,
787+ logFile,
788+ name : CLEANUP_SMOKE_NAME ,
789+ noOutputTimedOut : result . noOutputTimedOut ,
790+ rerunCommand : command ,
791+ startedAt : new Date ( startedAtMs ) . toISOString ( ) ,
792+ status : result . status ,
793+ targetable : false ,
794+ timedOut : result . timedOut ,
795+ } ;
796+ }
797+
715798async function runForegroundGroup ( entries , env ) {
716799 const failures = [ ] ;
717800 for ( const entry of entries ) {
@@ -1469,17 +1552,58 @@ async function main() {
14691552 }
14701553
14711554 if ( profile === DEFAULT_PROFILE && selectedLaneNames . length === 0 ) {
1472- await runPhase ( phases , "cleanup-smoke" , { } , async ( ) => {
1473- await runForeground (
1474- "Run cleanup smoke after parallel lanes" ,
1475- "pnpm test:docker:cleanup" ,
1555+ const cleanupSmokeCommand = "pnpm test:docker:cleanup" ;
1556+ const cleanupStartedAtMs = Date . now ( ) ;
1557+ let cleanupFailure ;
1558+ try {
1559+ await runPhase ( phases , CLEANUP_SMOKE_NAME , { } , async ( ) => {
1560+ cleanupFailure = await runCleanupSmoke (
1561+ baseEnv ,
1562+ logDir ,
1563+ cleanupSmokeCommand ,
1564+ cleanupStartedAtMs ,
1565+ ) ;
1566+ if ( cleanupFailure ) {
1567+ throw new Error (
1568+ `Run cleanup smoke after parallel lanes failed with status ${ cleanupFailure . status } ` ,
1569+ ) ;
1570+ }
1571+ } ) ;
1572+ } catch ( error ) {
1573+ cleanupFailure ??= await recordCleanupSmokeFailure (
1574+ error ,
14761575 baseEnv ,
1576+ logDir ,
1577+ cleanupSmokeCommand ,
1578+ cleanupStartedAtMs ,
14771579 ) ;
1478- } ) ;
1580+ }
1581+ if ( cleanupFailure ) {
1582+ failures . push ( cleanupFailure ) ;
1583+ }
14791584 } else {
14801585 console . log ( "==> Cleanup smoke after parallel lanes: skipped for selected/release lanes" ) ;
14811586 }
14821587 await writeTimingStore ( timingStore , allResults ) ;
1588+ if ( failures . length > 0 ) {
1589+ await writeRunSummary ( logDir , {
1590+ chunk : releaseChunk || undefined ,
1591+ failures,
1592+ image : baseEnv . OPENCLAW_DOCKER_E2E_IMAGE ,
1593+ images : {
1594+ bare : baseEnv . OPENCLAW_DOCKER_E2E_BARE_IMAGE ,
1595+ functional : baseEnv . OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE ,
1596+ } ,
1597+ lanes : allResults ,
1598+ phases,
1599+ profile,
1600+ selectedLanes : selectedLaneNames . length > 0 ? selectedLaneNames : undefined ,
1601+ startedAt : runStartedAt ,
1602+ status : "failed" ,
1603+ } ) ;
1604+ await printFailureSummary ( failures , tailLines ) ;
1605+ process . exit ( 1 ) ;
1606+ }
14831607 await writeRunSummary ( logDir , {
14841608 chunk : releaseChunk || undefined ,
14851609 failures,
0 commit comments