From 7faa0329ff824570b349775e03c5d2741f3f1d55 Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Sat, 25 Apr 2026 09:35:09 -0400 Subject: [PATCH] fix: replace inline python3 -c blocks with heredoc in three workflows Fixes #5, #6, #7. The unquoted multiline `python3 -c "..."` syntax in validate.yml, update-natives.yml, and update-docs-index.yml was being rejected by the YAML scanner due to colons inside the implicit scalar. Replaces each with `run: |` block scalars containing python3 heredocs (`python3 << 'EOF' ... EOF`). The new form is parseable, preserves the inline-python pattern for readability, and now actually runs the intended validation logic that has been silently failing since 2026-04-08. Also fixes two pre-existing latent bugs in validate.yml's validate-counts and validate-templates jobs, where `run: python3 << 'EOF'` was used without the `run: |` block-scalar marker. Those blocks parsed as plain scalars and would have folded their python bodies into a single line at runtime; they were never reached because the earlier YAML scanner errors halted parsing first. Now structured as `run: |` followed by the heredoc, matching the rest of the file. Surfaced during Phase 2 Session D D-1 canary on 2026-04-25 when the first drift-check job revealed validate.yml had been failing for weeks. Audit (TMHSDigital/Developer-Tools-Directory#1 close-out) surfaced the same pattern in update-natives.yml and update-docs-index.yml. Verified locally: - All 3 workflow files parse cleanly under PyYAML safe_load with newlines preserved in every python script body. - The python content runs cleanly against the repository's natives_gta5.json (6338 entries), natives_rdr3.json (5875 entries), events.json (101 entries), and docs_index.json (82 pages with snippets). - validate-counts confirmed: 9 skills, 6 rules, 24 snippets, 11 templates match plugin.json and README.md. Closes #5, #6, #7. Signed-off-by: 154358121+TMHSDigital@users.noreply.github.com Made-with: Cursor --- .github/workflows/update-docs-index.yml | 5 +- .github/workflows/update-natives.yml | 15 +++--- .github/workflows/validate.yml | 71 ++++++++++++++----------- 3 files changed, 51 insertions(+), 40 deletions(-) diff --git a/.github/workflows/update-docs-index.yml b/.github/workflows/update-docs-index.yml index be2d99d..cc8a4a3 100644 --- a/.github/workflows/update-docs-index.yml +++ b/.github/workflows/update-docs-index.yml @@ -23,7 +23,8 @@ jobs: run: python3 .github/scripts/build_docs_index.py - name: Validate index - run: python3 -c " + run: | + python3 << 'EOF' import json data = json.load(open('mcp-server/data/docs_index.json')) assert isinstance(data, list), 'Expected array' @@ -34,7 +35,7 @@ jobs: assert 'section' in page, f'Missing section' assert 'snippet' in page, f'Missing snippet' print(f'Docs index: {len(data)} pages validated') - " + EOF - name: Check for changes id: diff diff --git a/.github/workflows/update-natives.yml b/.github/workflows/update-natives.yml index 0adf55d..68c8c7a 100644 --- a/.github/workflows/update-natives.yml +++ b/.github/workflows/update-natives.yml @@ -38,7 +38,8 @@ jobs: run: python3 .github/scripts/transform_natives.py - name: Validate transformed data - run: python3 -c " + run: | + python3 << 'EOF' import json for game in ['gta5', 'rdr3']: @@ -48,13 +49,13 @@ jobs: assert len(data) > 0, f'{path}: empty array' for n in data: assert 'name' in n, f'{path}: missing name' - assert 'hash' in n, f'{path}: missing hash in {n[\"name\"]}' - assert 'side' in n, f'{path}: missing side in {n[\"name\"]}' - assert 'category' in n, f'{path}: missing category in {n[\"name\"]}' - assert 'params' in n, f'{path}: missing params in {n[\"name\"]}' - assert isinstance(n.get('deprecated'), bool), f'{path}: deprecated must be bool in {n[\"name\"]}' + assert 'hash' in n, f'{path}: missing hash in {n["name"]}' + assert 'side' in n, f'{path}: missing side in {n["name"]}' + assert 'category' in n, f'{path}: missing category in {n["name"]}' + assert 'params' in n, f'{path}: missing params in {n["name"]}' + assert isinstance(n.get('deprecated'), bool), f'{path}: deprecated must be bool in {n["name"]}' print(f'{game}: {len(data)} natives validated') - " + EOF - name: Check for changes id: diff diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index f1a3d83..98b1f14 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -23,50 +23,54 @@ jobs: run: python3 -c "import json; json.load(open('.cursor/mcp.json'))" - name: Validate natives_gta5.json - run: python3 -c " + run: | + python3 << 'EOF' import json data = json.load(open('mcp-server/data/natives_gta5.json')) assert isinstance(data, list), 'Expected array' for n in data: assert 'name' in n, f'Missing name in {n}' - assert 'hash' in n, f'Missing hash in {n.get(\"name\", \"unknown\")}' - assert 'side' in n, f'Missing side in {n.get(\"name\", \"unknown\")}' - assert 'category' in n, f'Missing category in {n.get(\"name\", \"unknown\")}' - assert isinstance(n.get('deprecated'), bool), f'deprecated must be bool in {n.get(\"name\", \"unknown\")}' - assert n.get('examples') is None or isinstance(n.get('examples'), str), f'examples must be string or null in {n.get(\"name\", \"unknown\")}' + assert 'hash' in n, f'Missing hash in {n.get("name", "unknown")}' + assert 'side' in n, f'Missing side in {n.get("name", "unknown")}' + assert 'category' in n, f'Missing category in {n.get("name", "unknown")}' + assert isinstance(n.get('deprecated'), bool), f'deprecated must be bool in {n.get("name", "unknown")}' + assert n.get('examples') is None or isinstance(n.get('examples'), str), f'examples must be string or null in {n.get("name", "unknown")}' print(f'GTA5 natives: {len(data)} entries validated') - " + EOF - name: Validate natives_rdr3.json - run: python3 -c " + run: | + python3 << 'EOF' import json data = json.load(open('mcp-server/data/natives_rdr3.json')) assert isinstance(data, list), 'Expected array' for n in data: assert 'name' in n, f'Missing name in {n}' - assert 'hash' in n, f'Missing hash in {n.get(\"name\", \"unknown\")}' - assert 'side' in n, f'Missing side in {n.get(\"name\", \"unknown\")}' - assert 'category' in n, f'Missing category in {n.get(\"name\", \"unknown\")}' - assert isinstance(n.get('deprecated'), bool), f'deprecated must be bool in {n.get(\"name\", \"unknown\")}' - assert n.get('examples') is None or isinstance(n.get('examples'), str), f'examples must be string or null in {n.get(\"name\", \"unknown\")}' + assert 'hash' in n, f'Missing hash in {n.get("name", "unknown")}' + assert 'side' in n, f'Missing side in {n.get("name", "unknown")}' + assert 'category' in n, f'Missing category in {n.get("name", "unknown")}' + assert isinstance(n.get('deprecated'), bool), f'deprecated must be bool in {n.get("name", "unknown")}' + assert n.get('examples') is None or isinstance(n.get('examples'), str), f'examples must be string or null in {n.get("name", "unknown")}' print(f'RDR3 natives: {len(data)} entries validated') - " + EOF - name: Validate events.json - run: python3 -c " + run: | + python3 << 'EOF' import json data = json.load(open('mcp-server/data/events.json')) assert isinstance(data, list), 'Expected array' for e in data: assert 'name' in e, f'Missing name in {e}' - assert 'side' in e, f'Missing side in {e.get(\"name\", \"unknown\")}' - assert 'framework' in e, f'Missing framework in {e.get(\"name\", \"unknown\")}' - assert 'game' in e, f'Missing game in {e.get(\"name\", \"unknown\")}' + assert 'side' in e, f'Missing side in {e.get("name", "unknown")}' + assert 'framework' in e, f'Missing framework in {e.get("name", "unknown")}' + assert 'game' in e, f'Missing game in {e.get("name", "unknown")}' print(f'Events: {len(data)} entries validated') - " + EOF - name: Validate docs_index.json - run: python3 -c " + run: | + python3 << 'EOF' import json data = json.load(open('mcp-server/data/docs_index.json')) assert isinstance(data, list), 'Expected array' @@ -75,7 +79,7 @@ jobs: assert 'url' in page, f'Missing url' assert 'section' in page, f'Missing section' print(f'Docs index: {len(data)} pages validated') - " + EOF validate-plugin-manifest: name: Validate plugin manifest @@ -84,7 +88,8 @@ jobs: - uses: actions/checkout@v4 - name: Check required manifest fields - run: python3 -c " + run: | + python3 << 'EOF' import json m = json.load(open('.cursor-plugin/plugin.json')) required = ['name', 'displayName', 'description', 'version', 'author', 'license', 'skills', 'rules'] @@ -93,25 +98,27 @@ jobs: import re assert re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', m['name']), 'name must be lowercase kebab-case' print('Plugin manifest valid') - " + EOF - name: Check skill files exist - run: python3 -c " + run: | + python3 << 'EOF' import json, os m = json.load(open('.cursor-plugin/plugin.json')) for skill in m.get('skills', []): assert os.path.exists(skill), f'Skill not found: {skill}' - print(f'All {len(m[\"skills\"])} skill files exist') - " + print(f'All {len(m["skills"])} skill files exist') + EOF - name: Check rule files exist - run: python3 -c " + run: | + python3 << 'EOF' import json, os m = json.load(open('.cursor-plugin/plugin.json')) for rule in m.get('rules', []): assert os.path.exists(rule), f'Rule not found: {rule}' - print(f'All {len(m[\"rules\"])} rule files exist') - " + print(f'All {len(m["rules"])} rule files exist') + EOF validate-content: name: Validate content quality @@ -151,7 +158,8 @@ jobs: - uses: actions/checkout@v4 - name: Check skill and snippet counts match plugin.json - run: python3 << 'EOF' + run: | + python3 << 'EOF' import json, os, sys m = json.load(open('.cursor-plugin/plugin.json')) @@ -197,7 +205,8 @@ jobs: - uses: actions/checkout@v4 - name: Check fxmanifest.lua files have required fields - run: python3 << 'EOF' + run: | + python3 << 'EOF' import os, re, sys errors = []