Job Needs Rule Overview #
This rule validates job dependencies in GitHub Actions workflows by checking the needs: configuration. It detects duplicate dependencies, references to undefined jobs, and cyclic dependency chains that would cause workflow failures.
Key Features: #
- Duplicate Detection: Identifies repeated job IDs in the
needs:section - Undefined Job Detection: Catches references to jobs that don’t exist in the workflow
- Cyclic Dependency Detection: Finds circular dependency chains using DAG (Directed Acyclic Graph) analysis
- Job ID Collision Detection: Warns when the same job ID is defined multiple times
Security Impact #
Severity: Low (3/10)
While not a direct security issue, invalid job dependencies can lead to:
- Workflow Failures: Workflows with invalid dependencies won’t run, potentially blocking CI/CD pipelines
- Skipped Security Checks: If security-related jobs have broken dependencies, they may be silently skipped
- Pipeline Bypass: Attackers might exploit misconfigured dependencies to skip mandatory checks
- Deployment Without Testing: Broken dependency chains can lead to deployments without proper validation
Understanding Job Dependencies #
GitHub Actions uses the needs: keyword to define job dependencies. Jobs run in parallel by default, but needs: creates explicit execution order.
Basic Dependency Syntax #
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm build
test:
needs: build # Runs after 'build' completes
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: [build, test] # Runs after both 'build' and 'test' complete
runs-on: ubuntu-latest
steps:
- run: npm deploy
Example Vulnerable Workflow #
Common dependency misconfigurations:
name: CI Pipeline
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm build
test:
needs: [build, build] # ❌ Duplicate: 'build' appears twice
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: release # ❌ Undefined: 'release' job doesn't exist
runs-on: ubuntu-latest
steps:
- run: npm deploy
# ❌ Cyclic dependency: a -> b -> c -> a
job-a:
needs: job-c
runs-on: ubuntu-latest
steps:
- run: echo "A"
job-b:
needs: job-a
runs-on: ubuntu-latest
steps:
- run: echo "B"
job-c:
needs: job-b
runs-on: ubuntu-latest
steps:
- run: echo "C"
What the Rule Detects #
1. Duplicate Job IDs in Needs #
When the same job ID appears multiple times in a needs: array:
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: make build
test:
needs: [build, lint, build] # ❌ 'build' duplicated
runs-on: ubuntu-latest
steps:
- run: make test
Error Output:
workflow.yml:10:5: job ID "build" duplicates in needs section [needs]
2. References to Undefined Jobs #
When needs: references a job that doesn’t exist:
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: make build
deploy:
needs: testing # ❌ 'testing' job doesn't exist
runs-on: ubuntu-latest
steps:
- run: make deploy
Error Output:
workflow.yml:9:5: job ID "deploy" needs job "testing" is not defined [needs]
3. Duplicate Job Definitions #
When the same job ID is defined multiple times:
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: make build
build: # ❌ Duplicate job ID
runs-on: ubuntu-latest
steps:
- run: npm build
Error Output:
workflow.yml:10:3: job ID "build" is already defined at line:4,col:3 [needs]
4. Cyclic Dependencies #
When jobs form a circular dependency chain:
jobs:
job-a:
needs: job-c
runs-on: ubuntu-latest
steps:
- run: echo "A"
job-b:
needs: job-a
runs-on: ubuntu-latest
steps:
- run: echo "B"
job-c:
needs: job-b # ❌ Creates cycle: a -> b -> c -> a
runs-on: ubuntu-latest
steps:
- run: echo "C"
Error Output:
workflow.yml:4:3: cyclic dependency in needs section found: "job-a" -> "job-c", "job-b" -> "job-a", "job-c" -> "job-b" is detected cycle [needs]
Safe Patterns #
Pattern 1: Linear Dependency Chain #
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm ci
- run: npm run build
test:
needs: build
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- run: npm run deploy
Pattern 2: Fan-Out Pattern #
Multiple jobs depending on a single job:
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
unit-tests:
needs: build
runs-on: ubuntu-latest
steps:
- run: npm run test:unit
integration-tests:
needs: build
runs-on: ubuntu-latest
steps:
- run: npm run test:integration
e2e-tests:
needs: build
runs-on: ubuntu-latest
steps:
- run: npm run test:e2e
Pattern 3: Fan-In Pattern #
Single job depending on multiple jobs:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- run: npm test
security-scan:
runs-on: ubuntu-latest
steps:
- run: npm audit
deploy:
needs: [lint, test, security-scan] # Waits for all three
runs-on: ubuntu-latest
steps:
- run: npm run deploy
Pattern 4: Diamond Pattern #
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
lint:
needs: build
runs-on: ubuntu-latest
steps:
- run: npm run lint
test:
needs: build
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: [lint, test] # Waits for both branches
runs-on: ubuntu-latest
steps:
- run: npm run deploy
Technical Detection Mechanism #
The rule uses a Directed Acyclic Graph (DAG) algorithm to detect cyclic dependencies:
// Check cyclic dependency using DAG traversal
func CheckCyclicDependency(nodes map[string]*jobNode) *edge {
for _, v := range nodes {
if v.status == nodeStatusNew {
if e := CheckCyclicNode(v); e != nil {
return e
}
}
}
return nil
}
func CheckCyclicNode(v *jobNode) *edge {
v.status = nodeStatusActive
for _, w := range v.resolved {
switch w.status {
case nodeStatusActive:
return &edge{v, w} // Cycle detected
case nodeStatusNew:
if e := CheckCyclicNode(w); e != nil {
return e
}
}
}
v.status = nodeStatusInactive
return nil
}
Best Practices #
1. Keep Dependencies Explicit and Minimal #
Only add dependencies that are truly necessary:
# Good: Explicit, minimal dependencies
jobs:
build:
runs-on: ubuntu-latest
steps: [...]
test:
needs: build # Only depends on build
steps: [...]
# Avoid: Unnecessary transitive dependencies
jobs:
test:
needs: [build, lint, setup] # If 'lint' already needs 'build', don't repeat
2. Document Complex Dependencies #
Add comments for non-obvious dependency relationships:
jobs:
deploy:
# Requires both security scan and tests to pass before deployment
needs: [security-scan, integration-tests]
runs-on: ubuntu-latest
3. Use Conditional Dependencies Carefully #
jobs:
deploy:
needs: test
if: needs.test.result == 'success' # Only deploy if tests passed
runs-on: ubuntu-latest
4. Visualize Dependencies #
For complex workflows, document the dependency graph:
# Dependency Graph:
#
# build
# / \
# lint test
# \ /
# deploy
jobs:
build: ...
lint:
needs: build
test:
needs: build
deploy:
needs: [lint, test]
Common Mistakes #
Mistake 1: Typos in Job References #
# ❌ Wrong: Typo in job name
jobs:
build:
runs-on: ubuntu-latest
steps: [...]
test:
needs: buidl # Typo: should be 'build'
Mistake 2: Case Sensitivity Confusion #
Job IDs are case-insensitive for dependency resolution:
# This works but can be confusing
jobs:
Build:
runs-on: ubuntu-latest
steps: [...]
test:
needs: build # Works, but lowercase doesn't match definition
Mistake 3: Self-Reference #
# ❌ Wrong: Job cannot depend on itself
jobs:
build:
needs: build # Invalid self-reference
runs-on: ubuntu-latest
Relationship to Other Rules #
- id: Invalid job IDs will cause dependency resolution to fail
- workflow-call: Reusable workflows have their own dependency considerations
Detection Example #
Running sisakulint on a workflow with dependency issues:
$ sisakulint .github/workflows/ci.yml
.github/workflows/ci.yml:10:5: job ID "build" duplicates in needs section [needs]
10 👈| needs: [build, build]
.github/workflows/ci.yml:15:5: job ID "deploy" needs job "release" is not defined [needs]
15 👈| needs: release
.github/workflows/ci.yml:20:3: cyclic dependency in needs section found: "job-a" -> "job-c", "job-b" -> "job-a", "job-c" -> "job-b" is detected cycle [needs]
20 👈| job-a:
References #
Testing #
To test this rule:
# Detect dependency issues
sisakulint .github/workflows/*.yml
# Focus on needs rule only
sisakulint -ignore ".*" .github/workflows/*.yml 2>&1 | grep needs
Configuration #
This rule is enabled by default. To disable it:
sisakulint -ignore needs
Disabling this rule is not recommended as invalid job dependencies will cause workflow failures on GitHub.