Skip to content

fix: expand variables in the rules:exists:paths form - #1923

Open
luantaraschi wants to merge 1 commit into
firecow:masterfrom
luantaraschi:fix/rules-exists-paths-variable-expansion
Open

fix: expand variables in the rules:exists:paths form#1923
luantaraschi wants to merge 1 commit into
firecow:masterfrom
luantaraschi:fix/rules-exists-paths-variable-expansion

Conversation

@luantaraschi

@luantaraschi luantaraschi commented Aug 18, 2026

Copy link
Copy Markdown

rules:exists accepts two shapes, a list of globs and an object with a paths key. Utils.evaluateRuleExist knows both and normalises them:

// src/utils.ts
static evaluateRuleExist (cwd: string, ruleExists: string[] | {paths: string[]} | undefined): boolean {
    if (ruleExists === undefined) return true;

    // Normalize rules:exists:paths to rules:exists
    if (!Array.isArray(ruleExists)) ruleExists = ruleExists.paths;

The variable expansion in the Job constructor only knows one:

// src/job.ts, two blocks apart
const changes = Array.isArray(rule.changes) ? rule.changes : rule.changes?.paths;
const exists = Array.isArray(rule.exists) ? rule.exists : null;

So a job written with the paths form keeps its ${VAR} literals, globbySync gets a pattern with a dollar sign in it, nothing matches, and the rule quietly evaluates to false. The job is skipped and nothing is printed to explain why, which is the part that makes it hard to spot from the outside.

The declared type shows the same asymmetry, changes?: string[] | {paths: string[]} next to exists?: string[], which is what let the null branch look complete.

The fix is the exists line rewritten to match the changes line above it, plus the type. The paths form arrived in #1530 for exists and the expansion was not extended along with it.

Testing

tests/test-cases/rules-exists/.gitlab-ci.yml already has var-expand-test, which uses ${DIR} in the list form, and executed-job-paths, which uses the paths form without a variable. The gap was exactly the combination of the two, so I added var-expand-paths-test for it and one assertion in integration.test.ts.

On master the new job never starts, so the assertion fails on missing output while var-expand-test right beside it runs and expands correctly. With the change both run.

bunx vitest run tests/test-cases/rules-exists/
  Tests  1 passed (1)

I also ran the neighbouring rule suites, rules-blank, rules-curly-bracket-if, rules-needs, include-rules and workflow-rules-variables, all passing. rules-changes fails for me with spawn docker ENOENT, and it fails the same way on a clean master checkout in the same environment, so that one is my sandbox and not this change.

tsc --noEmit and eslint are clean.

A note on the environment, since it affects what I can claim: my machine is Windows and the suite needs rsync, which Utils.rsyncTrackedFiles calls through Utils.bash, so everything above was run inside a Debian container on Bun 1.3.14 with --pool=forks. The default threads pool crashes there on this._thread.stdout.pipe, which looks like a Bun and vitest interaction rather than anything about this repository, so I did not chase it. I did not run the full suite or the dind-* tests.

Disclosure: I used an AI coding assistant while working on this. Every command and result reported above I ran and checked myself.


Summary by cubic

Expands variable placeholders in rules:exists when using the paths form so jobs no longer skip silently. Previously, only the list form expanded ${VAR}; now both the list and {paths: []} forms expand, matching rules:changes.

  • Update Job constructor to expand rule.exists?.paths; widen the type to exists?: string[] | {paths: string[]}.
  • Add var-expand-paths-test and an integration assertion to cover ${VAR} expansion in the paths form.
  • No migration required. Pipelines using rules:exists:paths with variables may now trigger where they previously did not.

Written for commit 820e7dc. Summary will update on new commits.

Review in cubic

rules:exists accepts both a list of globs and an object with a paths key,
and Utils.evaluateRuleExist normalises the two. The variable expansion in
the Job constructor only recognised the list, so a job written with the
paths form kept its ${VAR} literals and the pattern handed to globbySync
matched nothing, silently turning the rule false.

The line now mirrors the rules:changes expansion right above it, which has
handled both forms since the paths form was added.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/job.ts">

<violation number="1" location="src/job.ts:200">
P1: When a job uses `parallel:matrix` with a variable in `rules:exists:paths`, later permutations reuse the first permutation's expanded path because this expansion mutates shared `jobData.rules`. Clone the rules and nested path arrays per job, or build an expanded rule copy without mutating the parsed job data.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/job.ts
// Expand variables in rules:exists
this.rules.forEach((rule, ruleIdx, rules) => {
const exists = Array.isArray(rule.exists) ? rule.exists : null;
const exists = Array.isArray(rule.exists) ? rule.exists : rule.exists?.paths;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a job uses parallel:matrix with a variable in rules:exists:paths, later permutations reuse the first permutation's expanded path because this expansion mutates shared jobData.rules. Clone the rules and nested path arrays per job, or build an expanded rule copy without mutating the parsed job data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/job.ts, line 200:

<comment>When a job uses `parallel:matrix` with a variable in `rules:exists:paths`, later permutations reuse the first permutation's expanded path because this expansion mutates shared `jobData.rules`. Clone the rules and nested path arrays per job, or build an expanded rule copy without mutating the parsed job data.</comment>

<file context>
@@ -197,7 +197,7 @@ export class Job {
             // Expand variables in rules:exists
             this.rules.forEach((rule, ruleIdx, rules) => {
-                const exists = Array.isArray(rule.exists) ? rule.exists : null;
+                const exists = Array.isArray(rule.exists) ? rule.exists : rule.exists?.paths;
                 if (!exists) {
                     return;
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant