feat(linter): propagate workflow:rules:variables: into job rule evaluation
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>
This commit is contained in:
@@ -6,7 +6,8 @@ import "strings"
|
||||
// pipeline evaluation (rules:if:, only:, except:, workflow:rules:).
|
||||
// Variables are keyed by their name without the leading $.
|
||||
type Context struct {
|
||||
Vars map[string]string
|
||||
Vars map[string]string
|
||||
pinned map[string]bool // vars set via --var or shortcuts; never overwritten by Inject
|
||||
}
|
||||
|
||||
// New builds a Context from high-level shortcut values and optional KEY=VALUE
|
||||
@@ -17,46 +18,55 @@ type Context struct {
|
||||
// preserving the existing linting behaviour when no context flags are given.
|
||||
//
|
||||
// Override priority (highest wins): extraVars > branch/tag/source shortcuts.
|
||||
// Both shortcut-derived and extraVar variables are pinned — they will not be
|
||||
// overwritten by Inject (used for pipeline-level and workflow-rule variables).
|
||||
func New(branch, tag, source string, extraVars []string) *Context {
|
||||
if branch == "" && tag == "" && source == "" && len(extraVars) == 0 {
|
||||
return &Context{}
|
||||
}
|
||||
|
||||
vars := make(map[string]string)
|
||||
pinned := make(map[string]bool)
|
||||
|
||||
pin := func(k, v string) {
|
||||
vars[k] = v
|
||||
pinned[k] = true
|
||||
}
|
||||
|
||||
if branch != "" {
|
||||
vars["CI_COMMIT_BRANCH"] = branch
|
||||
vars["CI_COMMIT_REF_NAME"] = branch
|
||||
vars["CI_COMMIT_REF_SLUG"] = slugify(branch)
|
||||
pin("CI_COMMIT_BRANCH", branch)
|
||||
pin("CI_COMMIT_REF_NAME", branch)
|
||||
pin("CI_COMMIT_REF_SLUG", slugify(branch))
|
||||
if source == "" {
|
||||
source = "push"
|
||||
}
|
||||
}
|
||||
if tag != "" {
|
||||
vars["CI_COMMIT_TAG"] = tag
|
||||
vars["CI_COMMIT_REF_NAME"] = tag
|
||||
vars["CI_COMMIT_REF_SLUG"] = slugify(tag)
|
||||
delete(vars, "CI_COMMIT_BRANCH") // tag pushes have no branch variable
|
||||
pin("CI_COMMIT_TAG", tag)
|
||||
pin("CI_COMMIT_REF_NAME", tag)
|
||||
pin("CI_COMMIT_REF_SLUG", slugify(tag))
|
||||
delete(vars, "CI_COMMIT_BRANCH")
|
||||
delete(pinned, "CI_COMMIT_BRANCH")
|
||||
if source == "" {
|
||||
source = "push"
|
||||
}
|
||||
}
|
||||
if source != "" {
|
||||
vars["CI_PIPELINE_SOURCE"] = source
|
||||
pin("CI_PIPELINE_SOURCE", source)
|
||||
}
|
||||
if _, ok := vars["CI_DEFAULT_BRANCH"]; !ok {
|
||||
vars["CI_DEFAULT_BRANCH"] = "main"
|
||||
}
|
||||
|
||||
// KEY=VALUE overrides win over shortcuts.
|
||||
// KEY=VALUE overrides win over shortcuts and everything else.
|
||||
for _, kv := range extraVars {
|
||||
k, v, ok := strings.Cut(kv, "=")
|
||||
if ok {
|
||||
vars[k] = v
|
||||
pin(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return &Context{Vars: vars}
|
||||
return &Context{Vars: vars, pinned: pinned}
|
||||
}
|
||||
|
||||
// IsEmpty reports whether no variables have been set (no context flags given).
|
||||
@@ -73,6 +83,21 @@ func (c *Context) Get(key string) string {
|
||||
return c.Vars[key]
|
||||
}
|
||||
|
||||
// Inject sets key=value only if key is not already pinned (i.e. not set via
|
||||
// --branch / --tag / --source / --var). Used to inject pipeline-level variable
|
||||
// defaults and workflow-rule variables without overriding explicit user input.
|
||||
// Calling Inject in order from lowest-priority to highest-priority source
|
||||
// ensures later calls win over earlier ones.
|
||||
func (c *Context) Inject(key, value string) {
|
||||
if c.pinned[key] {
|
||||
return
|
||||
}
|
||||
if c.Vars == nil {
|
||||
c.Vars = make(map[string]string)
|
||||
}
|
||||
c.Vars[key] = value
|
||||
}
|
||||
|
||||
// Summary returns a short human-readable description of the context for CLI output.
|
||||
func (c *Context) Summary() string {
|
||||
if c.IsEmpty() {
|
||||
@@ -90,6 +115,28 @@ func (c *Context) Summary() string {
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// ExtractStringVars converts a map[string]any variable block (as used by
|
||||
// Pipeline.Variables and Rule.Variables) to a flat map[string]string.
|
||||
// Plain string values are used directly. Extended {value: "..."} map form
|
||||
// uses the "value" key. Other forms are skipped.
|
||||
func ExtractStringVars(m map[string]any) map[string]string {
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(m))
|
||||
for k, v := range m {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
out[k] = val
|
||||
case map[string]any:
|
||||
if s, ok := val["value"].(string); ok {
|
||||
out[k] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// slugify converts a ref name to its GitLab slug form:
|
||||
// lowercased, non-alphanumeric characters replaced with '-', leading/trailing '-' removed.
|
||||
func slugify(s string) string {
|
||||
|
||||
Reference in New Issue
Block a user