fix(rules): Stop a rules: if: regex error quoting the pattern

What this MR does and why?

A masked CI/CD variable's value was printed in clear text when the variable was used as the right-hand side of =~ or !~ in a rules: if: expression and its value did not compile as a regular expression.

matchRegex formatted the pattern into its error twice over: once itself, as invalid regex %q, and again through the wrapped regexp.Compile failure, whose *regexp/syntax.Error embeds the offending expression. Dropping only the first would not have closed it.

That error reached four output surfaces:

  • the glci: warning: line on stderr, which the config-resolving commands write straight to your terminal;
  • the same warning on glci run, where it is the daemon's stderr and so is written to ~/.glci/daemon.log — persisting the value to disk;
  • the rendered rule trace, the default output of glci variables and glci run --show-variables. writeRuleTrace masks the condition annotation but appends RuleOutcome.Reason verbatim, so it carries no redaction of its own and put the value next to the [masked] form of the same variable, twelve characters away;
  • the --json form of that trace. maskTraceRefs masks Refs but copies Reason through, so this leaked even with masking on.

The fix stops the value being formatted into the message at all, rather than redacting it afterwards. parsePrimary captures the right operand's source text as written — $TOKEN, /re/ — and matchRegex reports only regexp/syntax.Error.Code, a fixed enum string, qualified by that source text. The error becomes invalid regex in $TOKEN: missing closing ].

Source-fixing rather than redaction is deliberate. It needs no secret-value set threaded into pkg/rules, which has no notion of masking today; it survives --unmask; it covers variables that were never marked masked; and it is immune to the "(?i)" + pattern rewrite that a substring redactor would have missed. #38 closed the same class of leak on the include-resolution path, where the symbolic form is not available and redaction is the only option. Here it is available, so not printing the value is strictly stronger.

Evaluation behaviour is unchanged — this is a message-only change. An invalid regex is still an evaluation error and the rule still does not match. A 16-case probe across =~/!~, variable and literal operands, bad patterns, unset variables, flags and nested || produces identical booleans before and after.

The unsupported-flag error moved off the same path: /re/x is a valid regex that glci merely does not support the flag for, so reporting it as a bad pattern misdiagnosed it.

Steps to reproduce

regex_rhs:
  script: echo r
  rules:
    - if: $CI_COMMIT_BRANCH =~ $DEPLOY_PATTERN
    - when: always
glci variables regex_rhs --env 'DEPLOY_PATTERN=[glpatEXAMPLE'

Before, the value appears twice in one line — once from the %q, once from the wrapped syntax error:

    #0  $CI_COMMIT_BRANCH () =~ $DEPLOY_PATTERN ([glpatEXAMPLE)  → false  (if: could not be evaluated: invalid regex "[glpatEXAMPLE": error parsing regexp: missing closing ]: `[glpatEXAMPLE`)

After:

    #0  $CI_COMMIT_BRANCH () =~ $DEPLOY_PATTERN ([glpatEXAMPLE)  → false  (if: could not be evaluated: invalid regex in $DEPLOY_PATTERN: missing closing ])

The ($DEPLOY_PATTERN) annotation still shows the value here because --env is not a secret source; it is masked on its own terms when the variable is. The reason string no longer carries it either way.

For the masked case and the on-disk sink, with a masked project variable holding an invalid pattern:

glci run --simulate --show-variables --secrets project
grep 'could not evaluate rule' ~/.glci/daemon.log

The value appears on none of the four surfaces: the stderr warning, the daemon log, the rendered trace, or --json.

Closes #172.

Same class of leak as #38 (closed), on a different sink.

Merge request reports

Loading
Loading