feat(cli): inject GitLab predefined variables into the simulation context
ci / vet, staticcheck, test, build (push) Successful in 2m5s
ci / vet, staticcheck, test, build (push) Successful in 2m5s
Two categories of predefined variables are now injected automatically: 1. Always-available (CI=true, GITLAB_CI=true): set at lowest priority for every non-empty context so that rules:if: expressions like '$CI == "true"' evaluate correctly without requiring --var. 2. MR-specific (CI_MERGE_REQUEST_IID, CI_MERGE_REQUEST_SOURCE_BRANCH_NAME, CI_MERGE_REQUEST_TARGET_BRANCH_NAME, …): injected as placeholder values when --source merge_request_event is given, so MR-gated jobs evaluate as active rather than silently skipped. CI_MERGE_REQUEST_SOURCE_BRANCH_NAME is derived from --branch when provided. All injected defaults are non-pinned: --var overrides them, and pipeline variables: blocks can also override via Inject(). The empty context (no flags at all) is unchanged — no predefined vars are injected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+10
-2
@@ -258,7 +258,10 @@ Note: when none of --branch, --tag, --source, or --var are given, glint
|
||||
detects the current git branch automatically and uses it as the default
|
||||
(falling back to 'main' when not inside a git repository or in detached-HEAD
|
||||
state). --source defaults to 'push' so that rules:if: expressions are always
|
||||
evaluated.
|
||||
evaluated. GitLab's always-available predefined variables (CI=true,
|
||||
GITLAB_CI=true) are injected automatically; MR-specific variables
|
||||
(CI_MERGE_REQUEST_IID, CI_MERGE_REQUEST_SOURCE_BRANCH_NAME, …) are injected
|
||||
when --source merge_request_event is given. Override any with --var.
|
||||
|
||||
Examples:
|
||||
glint check .gitlab-ci.yml
|
||||
@@ -269,6 +272,8 @@ Examples:
|
||||
glint check --branch develop .gitlab-ci.yml
|
||||
glint check --tag v1.0.0 .gitlab-ci.yml
|
||||
glint check --source merge_request_event .gitlab-ci.yml
|
||||
glint check --source merge_request_event --branch feature/my-branch .gitlab-ci.yml
|
||||
glint check --source merge_request_event --var CI_MERGE_REQUEST_IID=42 .gitlab-ci.yml
|
||||
glint check --list-vars .gitlab-ci.yml
|
||||
GITLAB_TOKEN=glpat-xxxx glint check .gitlab-ci.yml
|
||||
glint check --token glpat-xxxx --gitlab-url https://gitlab.example.com .gitlab-ci.yml
|
||||
@@ -619,7 +624,10 @@ Note: when none of --branch, --tag, --source, or --var are given, glint
|
||||
detects the current git branch automatically and uses it as the default
|
||||
(falling back to 'main' when not inside a git repository or in detached-HEAD
|
||||
state). --source defaults to 'push' so that rules:if: expressions are always
|
||||
evaluated.
|
||||
evaluated. GitLab's always-available predefined variables (CI=true,
|
||||
GITLAB_CI=true) are injected automatically; MR-specific variables
|
||||
(CI_MERGE_REQUEST_IID, CI_MERGE_REQUEST_SOURCE_BRANCH_NAME, …) are injected
|
||||
when --source merge_request_event is given. Override any with --var.
|
||||
|
||||
Examples:
|
||||
glint graph .gitlab-ci.yml
|
||||
|
||||
@@ -14,6 +14,32 @@ type Context struct {
|
||||
changedFiles []string // nil = not provided (permissive); non-nil = known set of changed files
|
||||
}
|
||||
|
||||
// gitlabAlwaysVars are GitLab predefined variables that are constant across
|
||||
// every pipeline, instance, and project. Injected at the lowest priority so
|
||||
// that pipeline variables: blocks and --var overrides can still win.
|
||||
var gitlabAlwaysVars = map[string]string{
|
||||
"CI": "true",
|
||||
"GITLAB_CI": "true",
|
||||
}
|
||||
|
||||
// gitlabMRVars are predefined variables GitLab injects only for
|
||||
// CI_PIPELINE_SOURCE=merge_request_event pipelines. Placeholder values are
|
||||
// used so that rules:if: expressions that gate on MR context evaluate
|
||||
// correctly. All can be overridden with --var.
|
||||
//
|
||||
// CI_MERGE_REQUEST_SOURCE_BRANCH_NAME is left empty here and filled from
|
||||
// CI_COMMIT_BRANCH (the --branch flag) when available.
|
||||
var gitlabMRVars = map[string]string{
|
||||
"CI_MERGE_REQUEST_ID": "1",
|
||||
"CI_MERGE_REQUEST_IID": "1",
|
||||
"CI_MERGE_REQUEST_SOURCE_BRANCH_NAME": "",
|
||||
"CI_MERGE_REQUEST_TARGET_BRANCH_NAME": "main",
|
||||
"CI_MERGE_REQUEST_PROJECT_PATH": "namespace/project",
|
||||
"CI_MERGE_REQUEST_TITLE": "Draft: placeholder",
|
||||
"CI_MERGE_REQUEST_LABELS": "",
|
||||
"CI_OPEN_MERGE_REQUESTS": "namespace/project!1",
|
||||
}
|
||||
|
||||
// New builds a Context from high-level shortcut values and optional KEY=VALUE
|
||||
// overrides. Predefined CI variables are derived from the shortcuts so callers
|
||||
// do not need to know their exact names.
|
||||
@@ -21,15 +47,18 @@ type Context struct {
|
||||
// Returns an empty Context (IsEmpty() == true) when all inputs are zero values,
|
||||
// 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).
|
||||
// Override priority (highest wins): extraVars > branch/tag/source shortcuts >
|
||||
// pipeline variables (via Inject) > GitLab predefined defaults.
|
||||
func New(branch, tag, source string, extraVars []string) *Context {
|
||||
if branch == "" && tag == "" && source == "" && len(extraVars) == 0 {
|
||||
return &Context{}
|
||||
}
|
||||
|
||||
vars := make(map[string]string)
|
||||
// Seed with always-available GitLab predefined variables at lowest priority.
|
||||
vars := make(map[string]string, len(gitlabAlwaysVars)+8)
|
||||
for k, v := range gitlabAlwaysVars {
|
||||
vars[k] = v
|
||||
}
|
||||
pinned := make(map[string]bool)
|
||||
|
||||
pin := func(k, v string) {
|
||||
@@ -62,6 +91,22 @@ func New(branch, tag, source string, extraVars []string) *Context {
|
||||
vars["CI_DEFAULT_BRANCH"] = "main"
|
||||
}
|
||||
|
||||
// For MR pipelines, inject placeholder values for the predefined MR
|
||||
// variables so that rules:if: expressions like '$CI_MERGE_REQUEST_IID'
|
||||
// evaluate as non-empty (truthy). These are non-pinned so --var overrides
|
||||
// them and pipeline variables: can also override via Inject.
|
||||
if source == "merge_request_event" {
|
||||
for k, v := range gitlabMRVars {
|
||||
if !pinned[k] {
|
||||
vars[k] = v
|
||||
}
|
||||
}
|
||||
// Derive source branch from --branch when available.
|
||||
if branch != "" && vars["CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"] == "" {
|
||||
vars["CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"] = branch
|
||||
}
|
||||
}
|
||||
|
||||
// KEY=VALUE overrides win over shortcuts and everything else.
|
||||
for _, kv := range extraVars {
|
||||
k, v, ok := strings.Cut(kv, "=")
|
||||
|
||||
@@ -78,6 +78,66 @@ func TestNew_DefaultBranch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_PredefinedAlwaysVars(t *testing.T) {
|
||||
ctx := New("main", "", "", nil)
|
||||
if ctx.Get("CI") != "true" {
|
||||
t.Errorf("CI should be 'true', got %q", ctx.Get("CI"))
|
||||
}
|
||||
if ctx.Get("GITLAB_CI") != "true" {
|
||||
t.Errorf("GITLAB_CI should be 'true', got %q", ctx.Get("GITLAB_CI"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_PredefinedAlwaysVars_EmptyContext(t *testing.T) {
|
||||
// Always-vars must NOT be injected when the context is empty (no flags).
|
||||
ctx := New("", "", "", nil)
|
||||
if ctx.Get("CI") != "" {
|
||||
t.Errorf("CI should be empty in empty context, got %q", ctx.Get("CI"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_PredefinedAlwaysVars_OverridableByVar(t *testing.T) {
|
||||
ctx := New("main", "", "", []string{"CI=false"})
|
||||
if ctx.Get("CI") != "false" {
|
||||
t.Errorf("--var should override CI, got %q", ctx.Get("CI"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_PredefinedAlwaysVars_OverridableByInject(t *testing.T) {
|
||||
ctx := New("main", "", "", nil)
|
||||
ctx.Inject("CI", "custom")
|
||||
if ctx.Get("CI") != "custom" {
|
||||
t.Errorf("Inject should override non-pinned CI, got %q", ctx.Get("CI"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_MRVars(t *testing.T) {
|
||||
ctx := New("feature/my-branch", "", "merge_request_event", nil)
|
||||
if ctx.Get("CI_MERGE_REQUEST_IID") != "1" {
|
||||
t.Errorf("CI_MERGE_REQUEST_IID should be '1', got %q", ctx.Get("CI_MERGE_REQUEST_IID"))
|
||||
}
|
||||
if ctx.Get("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME") != "feature/my-branch" {
|
||||
t.Errorf("source branch should match --branch, got %q", ctx.Get("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"))
|
||||
}
|
||||
if ctx.Get("CI_MERGE_REQUEST_TARGET_BRANCH_NAME") != "main" {
|
||||
t.Errorf("target branch should be 'main', got %q", ctx.Get("CI_MERGE_REQUEST_TARGET_BRANCH_NAME"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_MRVars_NotInjectedForNonMR(t *testing.T) {
|
||||
ctx := New("main", "", "push", nil)
|
||||
if ctx.Get("CI_MERGE_REQUEST_IID") != "" {
|
||||
t.Errorf("CI_MERGE_REQUEST_IID should be empty for push pipeline, got %q", ctx.Get("CI_MERGE_REQUEST_IID"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_MRVars_OverridableByVar(t *testing.T) {
|
||||
ctx := New("", "", "merge_request_event", []string{"CI_MERGE_REQUEST_IID=42"})
|
||||
if ctx.Get("CI_MERGE_REQUEST_IID") != "42" {
|
||||
t.Errorf("--var should override CI_MERGE_REQUEST_IID, got %q", ctx.Get("CI_MERGE_REQUEST_IID"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInject(t *testing.T) {
|
||||
ctx := New("main", "", "", nil)
|
||||
// pinned var should not be overwritten
|
||||
|
||||
Reference in New Issue
Block a user