de6a526560
workflow:rules: can define variables: on matching rules (GitLab CI 15.0+).
These variables are now injected into the evaluation context before job
rules:if: expressions are evaluated, making patterns like:
workflow:
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
DEPLOY_TARGET: production
deploy:
rules:
- if: '$DEPLOY_TARGET == "production"'
work correctly with glint check --branch main.
Changes:
- model.Rule: add Variables map[string]any field (yaml:"variables")
- cicontext.Context: add pinned map tracking which vars must not be
overwritten; New() pins all shortcut and --var variables; add
Inject(key, value) which writes only when key is not pinned
- cicontext.ExtractStringVars: shared helper that converts map[string]any
variable blocks (plain string or {value:...} form) to map[string]string
- cicontext.EvalWorkflow: returns (bool, map[string]string) — the vars of
the matching workflow rule alongside the runs/no-runs result
- cmd/glint/main.go: enrichContext() injects pipeline-level variable
defaults then workflow-rule variables before printContext; applied in
both cmdCheck and cmdGraph
Injection priority (highest wins):
--var CLI overrides > --branch/--tag/--source shortcuts
> workflow-rule variables > pipeline variables: defaults
Adds 15 unit tests (TestEvalWorkflow, TestContextInject,
TestExtractStringVars, TestWorkflowVarsJobEval) and a testdata fixture
(workflow_vars.yml) validated across four branch contexts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
244 lines
6.3 KiB
Go
244 lines
6.3 KiB
Go
package cicontext
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.k3nny.fr/glint/internal/model"
|
|
)
|
|
|
|
func TestEvalWorkflow(t *testing.T) {
|
|
makePipeline := func(rules []model.Rule) *model.Pipeline {
|
|
return &model.Pipeline{Workflow: &model.Workflow{Rules: rules}}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
rules []model.Rule
|
|
branch string
|
|
wantRuns bool
|
|
wantVars map[string]string
|
|
}{
|
|
{
|
|
name: "no workflow block",
|
|
rules: nil,
|
|
branch: "main",
|
|
wantRuns: true,
|
|
wantVars: nil,
|
|
},
|
|
{
|
|
name: "matching rule runs always",
|
|
rules: []model.Rule{
|
|
{If: `$CI_COMMIT_BRANCH == "main"`, When: "always"},
|
|
},
|
|
branch: "main",
|
|
wantRuns: true,
|
|
},
|
|
{
|
|
name: "matching rule when never",
|
|
rules: []model.Rule{
|
|
{If: `$CI_COMMIT_BRANCH == "main"`, When: "never"},
|
|
},
|
|
branch: "main",
|
|
wantRuns: false,
|
|
},
|
|
{
|
|
name: "no rule matched",
|
|
rules: []model.Rule{
|
|
{If: `$CI_COMMIT_BRANCH == "main"`},
|
|
},
|
|
branch: "develop",
|
|
wantRuns: false,
|
|
},
|
|
{
|
|
name: "matching rule with variables",
|
|
rules: []model.Rule{
|
|
{
|
|
If: `$CI_COMMIT_BRANCH == "main"`,
|
|
When: "always",
|
|
Variables: map[string]any{
|
|
"DEPLOY_TARGET": "production",
|
|
"ENVIRONMENT": "prod",
|
|
},
|
|
},
|
|
{When: "always"},
|
|
},
|
|
branch: "main",
|
|
wantRuns: true,
|
|
wantVars: map[string]string{
|
|
"DEPLOY_TARGET": "production",
|
|
"ENVIRONMENT": "prod",
|
|
},
|
|
},
|
|
{
|
|
name: "fallback rule with different variables",
|
|
rules: []model.Rule{
|
|
{If: `$CI_COMMIT_BRANCH == "main"`, Variables: map[string]any{"DEPLOY_TARGET": "production"}},
|
|
{When: "always", Variables: map[string]any{"DEPLOY_TARGET": "staging"}},
|
|
},
|
|
branch: "develop",
|
|
wantRuns: true,
|
|
wantVars: map[string]string{"DEPLOY_TARGET": "staging"},
|
|
},
|
|
{
|
|
name: "empty context always runs",
|
|
rules: []model.Rule{
|
|
{If: `$CI_COMMIT_BRANCH == "main"`, When: "never"},
|
|
},
|
|
branch: "", // no context
|
|
wantRuns: true,
|
|
wantVars: nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var p *model.Pipeline
|
|
if tc.rules == nil {
|
|
p = &model.Pipeline{}
|
|
} else {
|
|
p = makePipeline(tc.rules)
|
|
}
|
|
ctx := New(tc.branch, "", "", nil)
|
|
runs, vars := EvalWorkflow(p, ctx)
|
|
if runs != tc.wantRuns {
|
|
t.Errorf("EvalWorkflow runs = %v, want %v", runs, tc.wantRuns)
|
|
}
|
|
for k, want := range tc.wantVars {
|
|
if got := vars[k]; got != want {
|
|
t.Errorf("EvalWorkflow vars[%q] = %q, want %q", k, got, want)
|
|
}
|
|
}
|
|
if len(vars) != len(tc.wantVars) {
|
|
t.Errorf("EvalWorkflow returned %d vars, want %d; got %v", len(vars), len(tc.wantVars), vars)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestContextInject(t *testing.T) {
|
|
t.Run("inject does not overwrite pinned var", func(t *testing.T) {
|
|
ctx := New("main", "", "", []string{"DEPLOY_TARGET=override"})
|
|
ctx.Inject("DEPLOY_TARGET", "workflow-value")
|
|
if got := ctx.Get("DEPLOY_TARGET"); got != "override" {
|
|
t.Errorf("Inject overwrote pinned var: got %q, want %q", got, "override")
|
|
}
|
|
})
|
|
t.Run("inject does not overwrite shortcut var", func(t *testing.T) {
|
|
ctx := New("main", "", "", nil)
|
|
ctx.Inject("CI_COMMIT_BRANCH", "other")
|
|
if got := ctx.Get("CI_COMMIT_BRANCH"); got != "main" {
|
|
t.Errorf("Inject overwrote shortcut var: got %q, want %q", got, "main")
|
|
}
|
|
})
|
|
t.Run("inject sets new variable", func(t *testing.T) {
|
|
ctx := New("main", "", "", nil)
|
|
ctx.Inject("DEPLOY_TARGET", "production")
|
|
if got := ctx.Get("DEPLOY_TARGET"); got != "production" {
|
|
t.Errorf("Inject did not set variable: got %q", got)
|
|
}
|
|
})
|
|
t.Run("inject later call overrides earlier call", func(t *testing.T) {
|
|
ctx := New("main", "", "", nil)
|
|
ctx.Inject("DEPLOY_TARGET", "pipeline-default")
|
|
ctx.Inject("DEPLOY_TARGET", "workflow-override")
|
|
if got := ctx.Get("DEPLOY_TARGET"); got != "workflow-override" {
|
|
t.Errorf("second Inject did not win: got %q, want %q", got, "workflow-override")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestExtractStringVars(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in map[string]any
|
|
want map[string]string
|
|
}{
|
|
{
|
|
name: "plain strings",
|
|
in: map[string]any{"A": "hello", "B": "world"},
|
|
want: map[string]string{"A": "hello", "B": "world"},
|
|
},
|
|
{
|
|
name: "extended value form",
|
|
in: map[string]any{
|
|
"KEY": map[string]any{"value": "extended", "description": "some desc"},
|
|
},
|
|
want: map[string]string{"KEY": "extended"},
|
|
},
|
|
{
|
|
name: "mixed forms",
|
|
in: map[string]any{
|
|
"PLAIN": "str",
|
|
"COMPLEX": map[string]any{"value": "val"},
|
|
},
|
|
want: map[string]string{"PLAIN": "str", "COMPLEX": "val"},
|
|
},
|
|
{
|
|
name: "nil map",
|
|
in: nil,
|
|
want: nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := ExtractStringVars(tc.in)
|
|
for k, want := range tc.want {
|
|
if got[k] != want {
|
|
t.Errorf("ExtractStringVars[%q] = %q, want %q", k, got[k], want)
|
|
}
|
|
}
|
|
if len(got) != len(tc.want) {
|
|
t.Errorf("ExtractStringVars returned %d entries, want %d", len(got), len(tc.want))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWorkflowVarsJobEval verifies the end-to-end flow: workflow rule injects
|
|
// DEPLOY_TARGET, which is then used in a job's rules:if: expression.
|
|
func TestWorkflowVarsJobEval(t *testing.T) {
|
|
rules := []model.Rule{
|
|
{If: `$CI_COMMIT_BRANCH == "main"`, Variables: map[string]any{"DEPLOY_TARGET": "production"}},
|
|
{When: "always", Variables: map[string]any{"DEPLOY_TARGET": "staging"}},
|
|
}
|
|
p := &model.Pipeline{
|
|
Workflow: &model.Workflow{Rules: rules},
|
|
Jobs: map[string]model.Job{
|
|
"deploy-prod": {
|
|
Rules: []model.Rule{
|
|
{If: `$DEPLOY_TARGET == "production"`, When: "on_success"},
|
|
{When: "never"},
|
|
},
|
|
},
|
|
"deploy-staging": {
|
|
Rules: []model.Rule{
|
|
{If: `$DEPLOY_TARGET == "staging"`, When: "on_success"},
|
|
{When: "never"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
branch string
|
|
wantProd JobState
|
|
wantStaging JobState
|
|
}{
|
|
{"main", JobActive, JobSkipped},
|
|
{"develop", JobSkipped, JobActive},
|
|
} {
|
|
ctx := New(tc.branch, "", "", nil)
|
|
_, ruleVars := EvalWorkflow(p, ctx)
|
|
for k, v := range ruleVars {
|
|
ctx.Inject(k, v)
|
|
}
|
|
if got := EvalJob(p.Jobs["deploy-prod"], ctx); got != tc.wantProd {
|
|
t.Errorf("branch=%q deploy-prod = %v, want %v", tc.branch, got, tc.wantProd)
|
|
}
|
|
if got := EvalJob(p.Jobs["deploy-staging"], ctx); got != tc.wantStaging {
|
|
t.Errorf("branch=%q deploy-staging = %v, want %v", tc.branch, got, tc.wantStaging)
|
|
}
|
|
}
|
|
}
|