diff --git a/CHANGELOG.md b/CHANGELOG.md index 112388a..771ef33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project uses [Semantic Versioning](https://semver.org). +## [0.5.1] - 2026-07-30 + +### Fixed + +- **AND-group variable cascading** — variables set by an earlier AND-group member's `variables:` block are now visible when evaluating later members' `if:` conditions. Previously all members were evaluated with the same base context, so a group like `[{if: "$CI_COMMIT_TAG", variables: {DEPLOY: "true"}}, {if: "$DEPLOY"}]` would never fire because `$DEPLOY` was not yet injected when the second element was tested. This matches GitLab CI's actual behaviour. + ## [0.5.0] - 2026-07-30 ### Added diff --git a/README.md b/README.md index 7281a5b..50e99b8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

License - Release + Release

> **Disclaimer:** This tool was built through iterative AI-assisted development with [Claude](https://claude.ai). It is experimental, incomplete, and not intended for production use. Coverage of GitLab CI keywords is best-effort and may lag behind GitLab's evolving spec. Use it at your own discretion — no correctness guarantees are made. Contributions and bug reports are welcome. diff --git a/internal/cicontext/reachability.go b/internal/cicontext/reachability.go index 644963a..8f48d1b 100644 --- a/internal/cicontext/reachability.go +++ b/internal/cicontext/reachability.go @@ -96,21 +96,31 @@ func EvalJob(job model.Job, ctx *Context) JobState { // All conditions in the group must match for the group to fire. // Returns (matched, effectiveWhen, mergedVars). When strict=true, unparseable // if: expressions count as no-match; when false, they count as match (permissive). +// +// Variables defined on an earlier member of the AND-group are cascaded to later +// members when evaluating their if: conditions. This matches GitLab CI behaviour +// where e.g. element 0 sets DEPLOY=true and element 1 can then test $DEPLOY. func evalRuleGroup(group []model.Rule, vars func(string) string, ctx *Context, strict bool) (bool, string, map[string]string) { + merged := make(map[string]string) + effectiveWhen := "" for _, rule := range group { + // Cascade: variables accumulated from previous group members are visible + // to this member's if: condition, shadowing the base context. + cascadeVars := func(key string) string { + if v, ok := merged[key]; ok { + return v + } + return vars(key) + } var ifOk bool if strict { - ifOk = ruleIfMatchesStrict(rule.If, vars) + ifOk = ruleIfMatchesStrict(rule.If, cascadeVars) } else { - ifOk = ruleIfMatches(rule.If, vars) + ifOk = ruleIfMatches(rule.If, cascadeVars) } if !ifOk || !changesMatch(rule.Changes, ctx) { return false, "", nil } - } - merged := make(map[string]string) - effectiveWhen := "" - for _, rule := range group { for k, v := range ExtractStringVars(rule.Variables) { merged[k] = v } diff --git a/testdata/nested_rules.yml b/testdata/nested_rules.yml index e091784..38ac2ad 100644 --- a/testdata/nested_rules.yml +++ b/testdata/nested_rules.yml @@ -17,6 +17,16 @@ workflow: variables: PIPELINE_NAME: "deploy" + # AND-group where element 0 sets PHASE=build; element 1 checks $PHASE (cascade). + # Without cascading, element 1 would never match because PHASE is not in + # pipeline variables. With cascading, element 1 sees PHASE from element 0. + - - if: "$CI_COMMIT_BRANCH" + variables: + PHASE: build + - if: "$PHASE" + variables: + PIPELINE_NAME: "branch-build" + variables: DEPLOY: "false"