fix: resolve develop CI failures (cli # escape + skip-check + allowed_bots)#2520
fix: resolve develop CI failures (cli # escape + skip-check + allowed_bots)#2520
Conversation
Module.cfc:3723 prints a help URL containing the unescaped fragment "testing#testing-against-different-engines". Lucee's parser interprets unescaped # in CFScript string literals as expression delimiters and crashes the file's compilation with "Invalid Syntax Closing [#] not found" when no closing # is found. This crashed Module.cfc compilation, which broke `wheels new` and the Wheels Snapshots smoke test pipeline (build / Smoke Test Installed Distribution). Every push to develop has been failing this gate since PR #2517 introduced the line. CFML rule: ## inside a string literal outputs a literal #. The fix is a one-character change. See CLAUDE.md "# escape gotcha" for the same bug class historical context.
There was a problem hiding this comment.
LGTM — one-character escape of # to ## in a CFML string literal, exactly the gotcha documented in CLAUDE.md.
Extended reasoning...
Overview
Single-line change in cli/lucli/Module.cfc escaping # to ## inside a string passed to out(). The visible URL fragment for end users remains testing#testing-against-different-engines (CFML doubles ## to render a single #).
Security risks
None. The change is in a help banner string and does not affect parsing, auth, I/O, or any user-controlled data path.
Level of scrutiny
Minimal. This is a mechanical, one-character correctness fix for a CFML/Lucee parser quirk that was crashing Module.cfc compilation in the smoke test pipeline. The fix exactly matches the pattern documented in the project's own CLAUDE.md ("# escape gotcha").
Other factors
No bugs were flagged by the bug hunting system. The PR description clearly explains the root cause, why other tests missed it, and what to verify post-merge. The change is self-contained with no risk to other code paths.
…action Composite actions cannot read the `vars` context — that lookup is only available in workflow files. The skip-check action's internal kill-switch check failed every time the action was invoked, with: Unrecognized named-value: 'vars'. Located at position 1 within expression: vars.WHEELS_BOT_ENABLED This silently broke from PR #2518 onwards but only surfaced after the kill switch was activated and Reviewer A actually fired on a real PR. The kill switch is already enforced at the job level via `if: vars.WHEELS_BOT_ENABLED == 'true'` in every wheels-bot workflow, so the action's internal check was redundant as well as broken. Removed: - `WHEELS_BOT_ENABLED` env binding (couldn't resolve) - The `if [[ ... != "true" ]]; then skip=true ...` block (redundant) Description updated to note that the kill switch lives at the job level, not in this action. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: Two focused bug fixes bundled together because the smoke test that surfaces fix 1 was also triggering fix 2's failure mode. Both fixes are correct, minimal, and well-explained. Approving.
Correctness
Both fixes address the stated root causes accurately.
Fix 1 — cli/lucli/Module.cfc:3723
- out("See: command-line-tools/wheels-commands/testing#testing-against-different-engines", "yellow");
+ out("See: command-line-tools/wheels-commands/testing##testing-against-different-engines", "yellow");Correct. Lucee's CFScript parser treats unescaped # in string literals as expression delimiters and crashes the file's compilation. ## is the canonical escape that emits a literal # at runtime. This is the exact pattern documented in CLAUDE.md under Testing Quick Reference:
Unescaped
#in strings causes "Invalid Syntax Closing [#] not found" compilation errors that crash the entire test suite (not just that file).
Fix 2 — .github/actions/wheels-bot-skip-check/action.yml
The removed block:
env:
WHEELS_BOT_ENABLED: ${{ vars.WHEELS_BOT_ENABLED }}if [[ "${WHEELS_BOT_ENABLED:-}" != "true" ]]; then
skip=true
reason="vars.WHEELS_BOT_ENABLED is not 'true'"
fiCorrect diagnosis. GitHub Actions composite actions cannot access the vars context — only caller workflow files can. The check would always evaluate WHEELS_BOT_ENABLED as an empty string, causing every invocation to set skip=true regardless of the repository variable's actual value.
The kill-switch coverage claim is verified by reading the workflow files: all six bot workflows (bot-triage.yml, bot-research.yml, bot-propose-fix.yml, bot-review-a.yml, bot-review-b.yml, bot-auto-close.yml) gate on if: vars.WHEELS_BOT_ENABLED == 'true' at the job level. The action-level check was both broken and redundant.
The refactored script correctly flattens the previously nested conditionals (which were gated on skip == "false" from the now-removed block). Logic is unchanged for the remaining checks: label scan, title substring match, idempotency marker grep.
Commits
Both commits conform to commitlint.config.js:
fix(cli): escape # in test help URL fragment— typefix, scopecli(valid), subject within 100 chars.ci: remove unreachable vars.WHEELS_BOT_ENABLED check from skip-check action— typeci, no scope (correct — CI infrastructure does not map cleanly to a layer scope), subject within 100 chars.
Tests
No automated test changes — appropriate for both fixes:
- Fix 1 is a one-character string literal correction; the smoke test pipeline that was crashing serves as the integration validator.
- Fix 2 is CI infrastructure; the action's behavior is validated by the bot workflows running successfully post-merge.
Security
No concerns. Neither change touches authentication, user-controlled data paths, or secrets handling.
The anthropics/claude-code-action defaults to blocking workflow runs initiated by non-human actors (any GitHub Bot identity). Without an explicit allowlist, every bot-triggered workflow fails fast with: Action failed with error: Workflow initiated by non-human actor: wheels-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots. This surfaced first on bot-review-b.yml — Reviewer B is triggered by wheels-bot[bot] submitting a Reviewer A review, and the action blocked on the bot initiator. Same shape would hit bot-research, bot-propose-fix, and bot-auto-close (cron actor is github-actions[bot]) as the rollout progresses through Phases 2-5. Adding `allowed_bots: 'wheels-bot[bot],github-actions[bot]'` to all six wheels-bot workflows. Specific allowlist (not '*') because the repo is public — '*' would let any external GitHub App invoke the action with prompts they could influence (per the action's docs/security.md). The two identities allowed: - wheels-bot[bot]: our App's bot identity - github-actions[bot]: GitHub's own actor for scheduled and workflow-internal triggers Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Three sequentially-discovered fixes that together restore develop CI to green. Each new fix was uncovered by the previous one passing.
1.
cli/lucli/Module.cfc:3723— unescaped#in CFML string literalPR #2517 added a help banner whose URL fragment contains
testing#testing-against-different-engines. Lucee's parser interprets unescaped#in CFScript string literals as expression delimiters and crashes the file's compilation withInvalid Syntax Closing [#] not found.Crashed
Module.cfc, which brokewheels newand the Wheels Snapshots → Smoke Test Installed Distribution pipeline on every push since #2517.Fix: one character —
testing#→testing##. CFML rule:##in a string literal outputs a literal#.2.
.github/actions/wheels-bot-skip-check/action.yml—varscontext not accessible in composite actionsThe skip-check composite action's internal
env:block declaredWHEELS_BOT_ENABLED: ${{ vars.WHEELS_BOT_ENABLED }}. Composite actions don't have access to thevarscontext — only workflow files do. Every invocation failed withUnrecognized named-value: 'vars', causing Reviewer A to fail-fast in 13 seconds on every PR after the kill switch was activated.The kill switch is already enforced at the job level via
if: vars.WHEELS_BOT_ENABLED == 'true'in every wheels-bot workflow, so the action's internal check was redundant as well as broken.Fix: remove the unreachable check. The action now focuses on label/title/marker checks; the kill switch lives at the job level.
3. All wheels-bot workflows —
claude-code-actionblocks bot-initiated runs by defaultAfter fix #2, Reviewer A passed and triggered Reviewer B. Reviewer B failed in 17s with:
The
anthropics/claude-code-actiondefaults to blocking workflow runs initiated by any GitHub Bot identity. Same shape would hitbot-research,bot-propose-fix, andbot-auto-close(cron actor isgithub-actions[bot]) as the rollout progresses through Phases 2-5.Fix: add
allowed_bots: 'wheels-bot[bot],github-actions[bot]'to all 6 wheels-bot workflows (review-a, review-b, research, propose-fix, triage, auto-close). Specific allowlist preferred over'*'because the repo is public —'*'would let any external GitHub App invoke the action with prompts they could influence.Why one PR for three fixes
Each new fix was a consequence of the prior one passing. Without #1, the smoke test failure masked everything else. Without #2, Reviewer A never ran successfully so the bot-initiator block in #3 was never surfaced. Splitting into three PRs would mean each ran into the unfixed downstream issues in CI; bundling resolves them as a unit.
Test plan
wheels test --db=mysql(without--core) prints the help banner with a literal#in the URL🤖 Generated with Claude Code