Playbook: GitHub Actions YAML parse error — multiline —body strings

Trigger

A GitHub Actions workflow stops firing scheduled runs, gh workflow run fails with HTTP 422 “Workflow does not have ‘workflow_dispatch’ trigger”, and gh api .../actions/workflows/<id> shows name as the file path (e.g. .github/workflows/health-check.yml) instead of the human-readable name: field.

How to confirm

# 1. Check if GitHub can parse the workflow
gh api repos/radieu/p24-infra/actions/workflows/272318005 --jq .name
# BROKEN: returns ".github/workflows/health-check.yml"
# OK: returns "Infrastructure Health Check"
 
# 2. Try to trigger manually (fails if YAML unparseable)
gh workflow run health-check.yml --repo radieu/p24-infra
# BROKEN: HTTP 422 "Workflow does not have 'workflow_dispatch' trigger"
 
# 3. Validate YAML locally
git show origin/main:.github/workflows/health-check.yml | \
  python -c "import sys, yaml; yaml.safe_load(sys.stdin.read()); print('OK')"
# BROKEN: ScannerError: while scanning an alias ... expected alphabetic or numeric character

Root cause

YAML literal block scalars (run: |) use indentation to determine block boundaries. The block ends when a line has indentation LESS THAN the block’s minimum indent.

If a run: | block starts with content at 10 spaces (minimum), any line with fewer spaces — including completely unindented lines from multiline shell strings — terminates the block. GitHub’s parser then tries to parse those lines as top-level YAML.

Common culprit: gh issue create --body "..." where the body spans multiple lines and some lines (e.g. markdown bold **Workflow:**) appear at column 0 in the file.

# BROKEN — **Workflow:** at col 0 terminates the run: | block
        run: |
          gh issue create \
            --body "## Title
 
**Workflow:** $NAME"   ← column 0 lines end the YAML block scalar here!

GitHub’s YAML parser hits ** and tries to scan a YAML alias → ScannerError.

Fix

Replace unindented --body "..." strings with the temp-file heredoc pattern. Keep all heredoc content at the block’s minimum indent (10 spaces here) so YAML includes it in the block. After YAML stripping, the content is at 0 spaces in bash (correct for markdown and for heredoc delimiter recognition).

# FIXED — all content at 10-space indent in the YAML file
        run: |
          BODY_FILE=$(mktemp /tmp/body-XXXXXX.md)
          cat > "$BODY_FILE" << BODYEOF
          ## Title
 
          **Workflow:** $NAME
          **Run:** $RUN_URL
          BODYEOF
          gh issue create --body-file "$BODY_FILE"
          rm -f "$BODY_FILE"

After YAML stripping 10 spaces:

  • ## Title → at col 0 (correct markdown, correct heredoc content)
  • BODYEOF → at col 0 (bash recognizes this as heredoc closing delimiter)

Validation

# Before committing, always validate:
python -c "import yaml; yaml.safe_load(open('.github/workflows/health-check.yml', encoding='utf-8').read()); print('YAML OK')"

After merge to main

The schedule resumes automatically. If GitHub doesn’t pick it up within the next 2h window:

gh workflow enable health-check.yml --repo radieu/p24-infra

Escalation

If YAML validates OK but GitHub still shows name: <filepath>, check:

  1. Does the file have a BOM? python -c "print(open('file.yml','rb').read()[:3])"
  2. Are there tab characters? grep -Pn '\t' .github/workflows/health-check.yml
  3. Are there surrogate code points (U+D800–U+DFFF) in the actual file bytes?

Prevention

  • Never embed multiline markdown in a --body "..." shell argument inside run: | blocks
  • Always use --body-file with a heredoc temp file for multi-line issue bodies
  • Run python -c "import yaml; yaml.safe_load(open(...))" on any edited workflow before push