@@ -51,6 +51,8 @@ type CommandReport struct {
5151 // "No module named pytest" is not a broken module, and a reader must be able
5252 // to see that without rerunning anything.
5353 Diagnostic string `json:"diagnostic,omitempty"`
54+ // A failed cleanup must not be followed by workspace deletion or another lane.
55+ CleanupPending bool `json:"cleanup_pending,omitempty"`
5456}
5557
5658const defaultModuleCommandTimeout = 10 * time .Minute
@@ -176,13 +178,13 @@ func (services *Services) runModuleLanes(
176178 modulePath string ,
177179 plan moduleworkflow.VerificationPlan ,
178180 timeout time.Duration ,
179- ) (ModuleVerification , []domain.Finding ) {
180- report : = ModuleVerification {
181+ ) (report ModuleVerification , findings []domain.Finding ) {
182+ report = ModuleVerification {
181183 GitmodulesName : plan .GitmodulesName , Path : plan .Path ,
182184 GitlinkOID : plan .GitlinkOID , RepositoryID : plan .RepositoryID ,
183185 Lanes : []LaneReport {},
184186 }
185- findings : = []domain.Finding {}
187+ findings = []domain.Finding {}
186188
187189 workspace , err := os .MkdirTemp ("" , "gds-module-verify-" )
188190 if err != nil {
@@ -192,8 +194,27 @@ func (services *Services) runModuleLanes(
192194 Evidence : map [string ]any {"gitmodules_name" : plan .GitmodulesName },
193195 })
194196 }
195- defer os .RemoveAll (workspace )
196197 checkout := filepath .Join (workspace , "checkout" )
198+ registered , preserve := false , false
199+ defer func () {
200+ if preserve {
201+ return
202+ }
203+ cleanupCtx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
204+ defer cancel ()
205+ var cleanupErr error
206+ if registered {
207+ cleanupErr = services .GitMutations .RemoveWorktree (cleanupCtx , modulePath , checkout )
208+ }
209+ if cleanupErr == nil {
210+ cleanupErr = os .RemoveAll (workspace )
211+ }
212+ if cleanupErr != nil {
213+ findings = append (findings , domain.Finding {Code : "GDS_MODULE_VERIFICATION_CLEANUP_NOT_PROVEN" , Severity : domain .SeverityHigh ,
214+ Message : "Verification workspace cleanup failed; retained state requires inspection." ,
215+ Evidence : map [string ]any {"workspace" : workspace , "error" : cleanupErr .Error ()}})
216+ }
217+ }()
197218
198219 if err := services .GitMutations .AddDetachedWorktree (
199220 ctx , modulePath , checkout , plan .GitlinkOID ,
@@ -206,16 +227,22 @@ func (services *Services) runModuleLanes(
206227 },
207228 })
208229 }
209- defer func () {
210- _ = services .GitMutations .RemoveWorktree (ctx , modulePath , checkout )
211- }()
230+ registered = true
212231
213232 for _ , lane := range plan .Lanes {
214233 laneReport := LaneReport {Lane : lane .Lane , Commands : []CommandReport {}}
215234 failed := false
216235 for _ , declared := range lane .Commands {
217236 result := runDeclaredCommand (ctx , checkout , declared , timeout )
218237 laneReport .Commands = append (laneReport .Commands , result )
238+ if result .CleanupPending {
239+ preserve = true
240+ report .Lanes = append (report .Lanes , laneReport )
241+ findings = append (findings , domain.Finding {Code : "GDS_MODULE_VERIFICATION_CLEANUP_NOT_PROVEN" , Severity : domain .SeverityHigh ,
242+ Message : "Command descendants may still own the verification workspace; no later lane was started." ,
243+ Evidence : map [string ]any {"workspace" : workspace , "command" : declared , "diagnostic" : result .Diagnostic }})
244+ return report , findings
245+ }
219246 if result .Status == "passed" {
220247 continue
221248 }
@@ -275,6 +302,10 @@ func runDeclaredCommand(
275302 command := exec .CommandContext (bounded , "bash" , "-euo" , "pipefail" , "-c" , declared )
276303 command .Dir = directory
277304 command .Stdin = nil
305+ stop , configureErr := configureModuleProcess (command )
306+ if configureErr != nil {
307+ return CommandReport {Command : declared , Status : "failed" , ExitCode : - 1 , Diagnostic : configureErr .Error ()}
308+ }
278309 // This selector belongs to the controller operation. Module commands prove
279310 // their own source checkout, and must not silently select its consumer's
280311 // estate. A declared command can still explicitly select an estate itself.
@@ -291,14 +322,23 @@ func runDeclaredCommand(
291322 command .Stdout = diagnostic
292323 command .Stderr = diagnostic
293324 err := command .Run ()
325+ leftover , cleanupErr := stop ()
326+ if err == nil && leftover {
327+ err = errors .New ("declared command exited with unjoined descendants" )
328+ }
329+ err = errors .Join (err , bounded .Err (), cleanupErr )
294330 report := CommandReport {
295331 Command : declared , Status : "passed" ,
296- DurationMS : time .Since (started ).Milliseconds (),
332+ DurationMS : time .Since (started ).Milliseconds (),
333+ CleanupPending : cleanupErr != nil ,
297334 }
298335 if err == nil {
299336 return report
300337 }
301338 report .Diagnostic = boundedDiagnostic (diagnostic .String ())
339+ if cleanupErr != nil {
340+ report .Diagnostic = boundedDiagnostic (report .Diagnostic + "\n " + cleanupErr .Error ())
341+ }
302342 if report .Diagnostic == "" {
303343 report .Diagnostic = boundedDiagnostic (err .Error ())
304344 }
0 commit comments