feat(linter): context-scoped needs:/dependencies: cross-checks
When glint check runs in single-context mode (--branch, --tag, --source, or --var), jobs that resolve to JobSkipped against that context are now excluded from needs: and dependencies: cross-job checks (GL027-GL031). This eliminates false-positive errors for jobs intentionally gated to specific pipeline events (e.g. a deploy job with rules:if: CI_COMMIT_TAG no longer triggers GL027 on branch pipelines). linter.Lint now accepts a skipped map[string]bool (nil = check all jobs); checkNeeds and checkDependencies skip jobs present in the map. Multi-context mode (--context) passes nil, so all jobs are checked regardless of context. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+14
-1
@@ -341,6 +341,8 @@ Examples:
|
||||
}
|
||||
}
|
||||
|
||||
var skipped map[string]bool // jobs excluded from cross-job lint checks
|
||||
|
||||
if len(contexts) > 0 {
|
||||
// Multi-context mode: build one context per --context flag, then print a comparison table.
|
||||
ctxList := make([]*cicontext.Context, 0, len(contexts))
|
||||
@@ -368,6 +370,17 @@ Examples:
|
||||
if !enrichContext(ctx, p) {
|
||||
fmt.Fprintf(os.Stderr, "%s: [warning] workflow:rules: pipeline would not start for this context\n", path)
|
||||
}
|
||||
// Build skipped set: jobs statically unreachable in this context are
|
||||
// excluded from needs:/dependencies: cross-checks to avoid false positives.
|
||||
skipped = make(map[string]bool)
|
||||
for name, job := range p.Jobs {
|
||||
if cicontext.EvalJob(job, ctx) == cicontext.JobSkipped {
|
||||
skipped[name] = true
|
||||
}
|
||||
}
|
||||
if len(skipped) == 0 {
|
||||
skipped = nil
|
||||
}
|
||||
}
|
||||
if *listVars {
|
||||
printVars(p, ctx)
|
||||
@@ -379,7 +392,7 @@ Examples:
|
||||
}
|
||||
}
|
||||
|
||||
findings := linter.Lint(p)
|
||||
findings := linter.Lint(p, skipped)
|
||||
findings = applyConfig(findings, glintCfg, p.Suppressions)
|
||||
errCount, _ := countSeverities(findings)
|
||||
|
||||
|
||||
@@ -823,6 +823,71 @@ build-job:
|
||||
}
|
||||
}
|
||||
|
||||
// ── context-scoped linting ───────────────────────────────────────────────────
|
||||
|
||||
func TestCmdCheck_ContextScopedLinting_SuppressesSkippedJob(t *testing.T) {
|
||||
// deploy-job has needs: [nonexistent] but is gated to tag pipelines only.
|
||||
// With --branch main the job is skipped → GL027 should be suppressed.
|
||||
content := `
|
||||
stages: [build, deploy]
|
||||
build-job:
|
||||
stage: build
|
||||
script: make
|
||||
deploy-job:
|
||||
stage: deploy
|
||||
script: make deploy
|
||||
needs: [nonexistent-job]
|
||||
rules:
|
||||
- if: '$CI_COMMIT_TAG != ""'
|
||||
when: on_success
|
||||
- when: never
|
||||
`
|
||||
path := writePipeline(t, content)
|
||||
|
||||
// Without context (permissive default branch=main): deploy-job IS skipped
|
||||
// by rules evaluation → needs cross-check suppressed → exit 0.
|
||||
code := captureExit(t)
|
||||
cmdCheck([]string{"--branch", "main", path})
|
||||
if *code == 1 {
|
||||
t.Error("skipped job's needs: error should be suppressed in context-scoped lint")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdCheck_ContextScopedLinting_ActiveJobStillErrors(t *testing.T) {
|
||||
// Same pipeline but with --tag set: deploy-job is active → GL027 fires.
|
||||
content := `
|
||||
stages: [build, deploy]
|
||||
build-job:
|
||||
stage: build
|
||||
script: make
|
||||
deploy-job:
|
||||
stage: deploy
|
||||
script: make deploy
|
||||
needs: [nonexistent-job]
|
||||
rules:
|
||||
- if: '$CI_COMMIT_TAG != ""'
|
||||
when: on_success
|
||||
- when: never
|
||||
`
|
||||
path := writePipeline(t, content)
|
||||
|
||||
code := captureExit(t)
|
||||
cmdCheck([]string{"--tag", "v1.0.0", path})
|
||||
if *code != 1 {
|
||||
t.Error("active job's bad needs: should still produce GL027 error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdCheck_ContextScopedLinting_SkippedSet_IsNilWhenAllActive(t *testing.T) {
|
||||
// All jobs active → skipped set is nil → no regression in normal behaviour.
|
||||
code := captureExit(t)
|
||||
path := writePipeline(t, minimalPipeline)
|
||||
cmdCheck([]string{"--branch", "main", path})
|
||||
if *code == 1 {
|
||||
t.Error("valid pipeline with all-active jobs should not produce errors")
|
||||
}
|
||||
}
|
||||
|
||||
// ── parseContextSpec ─────────────────────────────────────────────────────────
|
||||
|
||||
func TestParseContextSpec(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user