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 variablesandglci run --show-variables.writeRuleTracemasks the condition annotation but appendsRuleOutcome.Reasonverbatim, 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
--jsonform of that trace.maskTraceRefsmasksRefsbut copiesReasonthrough, 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: alwaysglci 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.logThe value appears on none of the four surfaces: the stderr warning, the daemon log, the rendered trace, or --json.
Relevant issues and other links
Closes #172.
Same class of leak as #38 (closed), on a different sink.
- GitLab's masking contract:
mask_variables_from regexp/syntax.Errorcarries the offending expression in itsExprfield, which is why the wrapped error is not propagated: https://pkg.go.dev/regexp/syntax#Error