# claude-release-watch — the real bugs found via actionlint

Five PR cycles of guessing didn't fix this. Running `actionlint` locally surfaced both bugs in 30 seconds.

## Bug 1 — heredoc body breaks YAML block scalar

actionlint output:

    .github/workflows/claude-release-watch.yml:114:0:
      could not parse as YAML: could not find expected ':' [syntax-check]
      | 114 | **Auto-filed by cc-release-watch** — see ...

The `BODY=$(cat <<BODY ... BODY)` heredoc body started at column 1. YAML's `run: |` block scalar requires every line to be indented at least as much as the block's base indentation (column 11 in this file). YAML's parser sees the unindented heredoc line, decides the block ENDED, and tries to parse `**Auto-filed by ...` as a top-level YAML construct — fails. GHA accepts the file but silently fails to register `name:` and `workflow_dispatch:`.

Fix: replace the heredoc with `printf '%s\n'` with each line as a separate quoted argument. Every line stays indented inside the YAML block, bash sees a multi-line string with embedded newlines.

    BODY=$(printf '%s\n' \
      "**Auto-filed by cc-release-watch** — see ..." \
      "" \
      "**Key:** \`${KEY}\`" \
      ...)

## Bug 2 — secrets context not allowed in step-level if:

actionlint output:

    .github/workflows/claude-release-watch.yml:55:17:
      context "secrets" is not allowed here.
      available contexts are "env", "github", "inputs", "job", "matrix", "needs",
      "runner", "steps", "strategy", "vars". [expression]
      | 55 | if: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN != '' }}

This was a bug I introduced in the M130 hotfix PR #1591. I "fixed" the original `env.CLAUDE_CODE_OAUTH_TOKEN` by switching to `secrets.X`, on the wrong belief that step-level `env:` can't be read in step-level `if:`. Step-level env CAN'T, but the `secrets` context isn't available in `if:` at all.

Fix: hoist `CLAUDE_CODE_OAUTH_TOKEN` to job-level `env:`. Job-level env IS available in step-level `if:`, so the original `if: ${{ env.CLAUDE_CODE_OAUTH_TOKEN != '' }}` works correctly. Bonus: the redundant step-level `env:` blocks for the same secrets are now removed (DRY).

    jobs:
      watch:
        env:
          CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        steps:
          ...
          - name: LLM-assisted feature extraction
            if: ${{ env.CLAUDE_CODE_OAUTH_TOKEN != '' }}
            run: node scripts/cc-triage.mjs

## What I should have done from the start

1. Run `actionlint .github/workflows/*.yml` on every workflow change. It's installed (`/usr/local/bin/actionlint`) and caught both bugs immediately.
2. Add an actionlint pre-commit hook to make this automatic. Already partially done via `.github/workflows/...` CI but apparently not enforcing on PRs to non-workflow files? — verify and harden.
3. Stop guessing. The five PRs (#1592 explicit form, #1593 rename, #1594 cleanup) were all built on hypotheses without running the validator. That cost ~45 minutes of round-trip CI time and four unnecessary main-branch commits.

## Verification

1. This PR merges to main.
2. `gh workflow list` shows `Claude Release Watch` as a friendly name (not file path).
3. `gh workflow run "Claude Release Watch"` returns a run URL.
4. The dispatched run actually executes the LLM triage step end-to-end.