@@ -21,6 +21,28 @@ import (
2121)
2222
2323var (
24+ daemonFlags = []string {
25+ `(?:^|\s)--daemon\b` ,
26+ `(?:^|\s)--daemonize\b` ,
27+ `(?:^|\s)--detach\b` ,
28+ `(?:^|\s)-daemon\b` ,
29+ }
30+
31+ redirPatterns = []string {
32+ `>\s*\S+` ,
33+ `>>\s*\S+` ,
34+ `2>\s*\S+` ,
35+ `2>>\s*\S+` ,
36+ `&>\s*\S+` ,
37+ `&>>\s*\S+` ,
38+ `>\s*\S+.*2>&1` ,
39+ `2>&1.*>\s*\S+` ,
40+ `>\s*/dev/null` ,
41+ `2>\s*/dev/null` ,
42+ `&>\s*/dev/null` ,
43+ `\d+>&\d+` ,
44+ }
45+
2446 reValidSHA256 = regexp .MustCompile (`^[a-fA-F0-9]{64}$` )
2547 reValidSHA512 = regexp .MustCompile (`^[a-fA-F0-9]{128}$` )
2648 reValidSHA1 = regexp .MustCompile (`^[a-fA-F0-9]{40}$` )
4365 hostEditDistanceExceptions = map [string ]string {
4466 "www.libssh.org" : "www.libssh2.org" ,
4567 }
68+
69+ // Detect background processes (commands ending with '&' or '& sleep ...') or daemonized commands
70+ // reBackgroundProcess detects background processes (commands ending with '&' or '& sleep ...')
71+ // We explicitly avoid matching '&&' which is commonly used for command chaining.
72+ reBackgroundProcess = regexp .MustCompile (`(?:^|[^&])&(?:\s*$|\s+sleep\b)` ) // matches 'cmd &' or 'cmd & sleep'
73+ reDaemonProcess = regexp .MustCompile (`.*(?:` + strings .Join (daemonFlags , "|" ) + `).*` )
74+ // Detect output redirection in shell commands
75+ reOutputRedirect = regexp .MustCompile (strings .Join (redirPatterns , "|" ))
4676)
4777
4878const gitCheckout = "git-checkout"
@@ -456,6 +486,47 @@ var AllRules = func(l *Linter) Rules { //nolint:gocyclo
456486 return fmt .Errorf ("auto-update is disabled but no reason is provided" )
457487 },
458488 },
489+ {
490+ Name : "background-process-without-redirect" ,
491+ Description : "test steps should redirect output when running background processes" ,
492+ Severity : SeverityWarning ,
493+ LintFunc : func (c config.Configuration ) error {
494+ checkSteps := func (steps []config.Pipeline ) error {
495+ for _ , s := range steps {
496+ if s .Runs == "" {
497+ continue
498+ }
499+ lines := strings .Split (s .Runs , "\n " )
500+ for i , line := range lines {
501+ checkLine := line
502+ if strings .Contains (line , "&" ) && i + 1 < len (lines ) {
503+ checkLine += "\n " + lines [i + 1 ]
504+ }
505+
506+ needsRedirect := reBackgroundProcess .MatchString (checkLine ) || reDaemonProcess .MatchString (line )
507+ if needsRedirect && ! reOutputRedirect .MatchString (line ) {
508+ return fmt .Errorf ("background process missing output redirect: %s" , strings .TrimSpace (line ))
509+ }
510+ }
511+ }
512+ return nil
513+ }
514+
515+ if c .Test != nil {
516+ if err := checkSteps (c .Test .Pipeline ); err != nil {
517+ return err
518+ }
519+ }
520+ for _ , sp := range c .Subpackages {
521+ if sp .Test != nil {
522+ if err := checkSteps (sp .Test .Pipeline ); err != nil {
523+ return err
524+ }
525+ }
526+ }
527+ return nil
528+ },
529+ },
459530 {
460531 Name : "valid-update-schedule" ,
461532 Description : "update schedule config should contain a valid period" ,
0 commit comments