From f22359bfa0a1551048bdf4db9c11f56be41ad260 Mon Sep 17 00:00:00 2001 From: Core447 <100139110+Core447@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:20:36 +0200 Subject: [PATCH 01/13] ci: auto-repair invalid JSON in PRs and on direct pushes to main The JSON check previously only reported syntax errors; it didn't block merges and couldn't fix anything. This adds: - scripts/validate_json.py: validates the top-level *.json files, with a --fix mode that repairs minor syntax issues (trailing commas, etc.) via json_repair, and a --restore-fallback mode that reverts a file to its last known-good version from git history if a repair isn't safe (e.g. it would drop most of the entries). - On pull_request (same-repo branches): auto-repairs and pushes the fix back to the PR branch; still fails the check if something can't be fixed, so it can be made a required status check to block merging. - On push to main (direct commits): auto-repairs, falling back to restoring the last known-good version, so main is never left pointing at broken JSON that the app would choke on. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/jsoncheck.yml | 70 ++++++++++++++++++-- scripts/validate_json.py | 113 ++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 scripts/validate_json.py diff --git a/.github/workflows/jsoncheck.yml b/.github/workflows/jsoncheck.yml index 2dd7cf1..2dad783 100644 --- a/.github/workflows/jsoncheck.yml +++ b/.github/workflows/jsoncheck.yml @@ -2,16 +2,76 @@ name: JSON check on: push: + branches: [main] paths: - '**.json' pull_request: + paths: + - '**.json' + +permissions: + contents: write jobs: - test: + check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: json-syntax-check - uses: limitusus/json-syntax-check@v2 + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} + fetch-depth: 0 + token: ${{ github.event_name == 'push' && secrets.STORE_AUTOMATION_TOKEN || github.token }} + + - name: Set up Python + uses: actions/setup-python@v5 with: - pattern: "\\.json$" \ No newline at end of file + python-version: '3.x' + + - name: Install dependencies + run: pip install json_repair + + - name: Pick validation mode + id: mode + run: | + if [ "${{ github.event_name }}" = "push" ]; then + # Direct commit landed on main: try to repair it, and if that's not + # possible/safe, fall back to the last known-good version so the app + # is never left pointed at broken JSON. + echo "args=--fix --restore-fallback" >> "$GITHUB_OUTPUT" + elif [ "${{ github.event.pull_request.head.repo.full_name }}" = "${{ github.repository }}" ]; then + # PR from a branch in this repo: safe to push an auto-fix commit back to it. + echo "args=--fix" >> "$GITHUB_OUTPUT" + else + # PR from a fork: we can't push to it, so just check and report. + echo "args=" >> "$GITHUB_OUTPUT" + fi + + - name: Validate JSON + id: validate + continue-on-error: true + run: python scripts/validate_json.py ${{ steps.mode.outputs.args }} + + - name: Commit auto-fix + if: always() + env: + GH_TOKEN: ${{ github.event_name == 'push' && secrets.STORE_AUTOMATION_TOKEN || github.token }} + run: | + if ! git diff --quiet -- '*.json'; then + git config user.name "streamcontroller-bot" + git config user.email "actions@github.com" + git add -- '*.json' + if [ "${{ github.event_name }}" = "push" ]; then + git commit -m "fix(json): auto-repair invalid JSON pushed directly to main" + git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" HEAD:${{ github.ref_name }} + else + git commit -m "fix(json): auto-repair invalid JSON" + git push origin HEAD:${{ github.head_ref }} + fi + else + echo "No changes to commit" + fi + + - name: Fail if invalid JSON remains + if: steps.validate.outcome == 'failure' + run: exit 1 diff --git a/scripts/validate_json.py b/scripts/validate_json.py new file mode 100644 index 0000000..cc38139 --- /dev/null +++ b/scripts/validate_json.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Validate the top-level *.json files in this repo, optionally repairing them in place. + +Modes: + (no flags) check only, exit 1 if any file is invalid JSON + --fix also try to repair invalid files with json_repair; if the + repaired result looks sane, write it back + --restore-fallback when a file can't be repaired (or the repair looks unsafe, + e.g. it dropped most of the entries), fall back to the last + version of that file in git history that was valid JSON +""" +import argparse +import json +import subprocess +import sys +from pathlib import Path + +from json_repair import repair_json + +ROOT = Path(__file__).resolve().parent.parent +JSON_FILES = sorted(ROOT.glob("*.json")) + + +def git_show(ref: str, rel_path: str) -> str | None: + result = subprocess.run( + ["git", "show", f"{ref}:{rel_path}"], + cwd=ROOT, capture_output=True, text=True, + ) + return result.stdout if result.returncode == 0 else None + + +def last_good_version(rel_path: str): + """Walk commit history for this file and return (text, obj) of the newest valid revision.""" + log = subprocess.run( + ["git", "log", "--format=%H", "--", rel_path], + cwd=ROOT, capture_output=True, text=True, + ).stdout.split() + for commit in log: + content = git_show(commit, rel_path) + if content is None: + continue + try: + return content, json.loads(content) + except json.JSONDecodeError: + continue + return None + + +def looks_sane(repaired: object, reference: object) -> bool: + """Guard against a 'repair' that silently throws away most of the data.""" + if type(repaired) is not type(reference): + return False + if isinstance(reference, (list, dict)): + return len(repaired) >= max(1, int(len(reference) * 0.8)) + return True + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--fix", action="store_true") + parser.add_argument("--restore-fallback", action="store_true") + args = parser.parse_args() + + changed = [] + failed = [] + + for path in JSON_FILES: + rel_path = str(path.relative_to(ROOT)) + original = path.read_text(encoding="utf-8") + try: + json.loads(original) + continue # already valid, nothing to do + except json.JSONDecodeError as exc: + print(f"::error file={rel_path}::invalid JSON: {exc}") + + if not args.fix: + failed.append(rel_path) + continue + + reference = last_good_version(rel_path) + fixed = False + try: + repaired_text = repair_json(original, ensure_ascii=False) + repaired_obj = json.loads(repaired_text) + if reference is None or looks_sane(repaired_obj, reference[1]): + path.write_text( + json.dumps(repaired_obj, indent=4, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + print(f"Repaired {rel_path}") + changed.append(rel_path) + fixed = True + except Exception as exc: + print(f"repair attempt for {rel_path} failed: {exc}") + + if not fixed: + if args.restore_fallback and reference is not None: + path.write_text(reference[0], encoding="utf-8") + print(f"Restored {rel_path} to its last known-good version from git history") + changed.append(rel_path) + else: + failed.append(rel_path) + + if changed: + print("CHANGED:" + ",".join(changed)) + if failed: + print("FAILED:" + ",".join(failed)) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From ce6c4f85acc3cf22d578074a4b988f20293f8155 Mon Sep 17 00:00:00 2001 From: Core447 <100139110+Core447@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:21:49 +0200 Subject: [PATCH 02/13] test: intentionally break Icons.json to verify the auto-fix workflow --- Icons.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Icons.json b/Icons.json index 50fed55..a7a95ea 100644 --- a/Icons.json +++ b/Icons.json @@ -31,5 +31,5 @@ "commits": { "1.0.0": "e5a8cade8fc348e48ba97c47207c096d7d39c61e" } - } + }, ] From a6100764c3ec15a936c7ed397d3d2d55b86b0716 Mon Sep 17 00:00:00 2001 From: Core447 <100139110+Core447@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:23:49 +0200 Subject: [PATCH 03/13] test: trigger check From d32b94ca6b667fd20a0a7305a9b33e8688695d5e Mon Sep 17 00:00:00 2001 From: Core447 <100139110+Core447@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:26:50 +0200 Subject: [PATCH 04/13] test: revert intentional Icons.json breakage (Actions isn't running currently, see PR) --- Icons.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Icons.json b/Icons.json index a7a95ea..50fed55 100644 --- a/Icons.json +++ b/Icons.json @@ -31,5 +31,5 @@ "commits": { "1.0.0": "e5a8cade8fc348e48ba97c47207c096d7d39c61e" } - }, + } ] From 06e9b58c0dce2ace7d376676ce74727628770670 Mon Sep 17 00:00:00 2001 From: Core447 <100139110+Core447@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:51:50 +0200 Subject: [PATCH 05/13] test: re-trigger check now that GitHub Actions is back --- Icons.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Icons.json b/Icons.json index 50fed55..a7a95ea 100644 --- a/Icons.json +++ b/Icons.json @@ -31,5 +31,5 @@ "commits": { "1.0.0": "e5a8cade8fc348e48ba97c47207c096d7d39c61e" } - } + }, ] From bc5e528e98534e64e66de07a247b4226c28de73f Mon Sep 17 00:00:00 2001 From: streamcontroller-bot Date: Fri, 7 Aug 2026 08:52:06 +0000 Subject: [PATCH 06/13] fix(json): auto-repair invalid JSON --- Icons.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Icons.json b/Icons.json index a7a95ea..50fed55 100644 --- a/Icons.json +++ b/Icons.json @@ -31,5 +31,5 @@ "commits": { "1.0.0": "e5a8cade8fc348e48ba97c47207c096d7d39c61e" } - }, + } ] From b853f12cf44ea1e4cce11db0cc1eb6b2f4f67bb9 Mon Sep 17 00:00:00 2001 From: Core447 <100139110+Core447@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:53:54 +0200 Subject: [PATCH 07/13] ci: use the automation PAT to push auto-fix commits, not the default token The default GITHUB_TOKEN deliberately doesn't trigger new workflow runs when it pushes (anti-recursion protection). Verified this live on PR #247: the fix commit landed but never got its own check run, which would permanently block merging once this becomes a required status check. Push with STORE_AUTOMATION_TOKEN instead so the fix commit re-triggers CI and reports its own green check. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/jsoncheck.yml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/jsoncheck.yml b/.github/workflows/jsoncheck.yml index 2dad783..d8ea745 100644 --- a/.github/workflows/jsoncheck.yml +++ b/.github/workflows/jsoncheck.yml @@ -16,12 +16,24 @@ jobs: check: runs-on: ubuntu-latest steps: + - name: Decide whether we may push a fix commit + id: pushable + run: | + if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event.pull_request.head.repo.full_name }}" = "${{ github.repository }}" ]; then + echo "value=true" >> "$GITHUB_OUTPUT" + else + echo "value=false" >> "$GITHUB_OUTPUT" + fi + - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} fetch-depth: 0 - token: ${{ github.event_name == 'push' && secrets.STORE_AUTOMATION_TOKEN || github.token }} + # The default GITHUB_TOKEN doesn't re-trigger workflow runs when it pushes, + # which would leave a required status check permanently unreported on the + # fix commit. Use the automation PAT whenever we might push one. + token: ${{ steps.pushable.outputs.value == 'true' && secrets.STORE_AUTOMATION_TOKEN || github.token }} - name: Set up Python uses: actions/setup-python@v5 @@ -39,7 +51,7 @@ jobs: # possible/safe, fall back to the last known-good version so the app # is never left pointed at broken JSON. echo "args=--fix --restore-fallback" >> "$GITHUB_OUTPUT" - elif [ "${{ github.event.pull_request.head.repo.full_name }}" = "${{ github.repository }}" ]; then + elif [ "${{ steps.pushable.outputs.value }}" = "true" ]; then # PR from a branch in this repo: safe to push an auto-fix commit back to it. echo "args=--fix" >> "$GITHUB_OUTPUT" else @@ -53,9 +65,7 @@ jobs: run: python scripts/validate_json.py ${{ steps.mode.outputs.args }} - name: Commit auto-fix - if: always() - env: - GH_TOKEN: ${{ github.event_name == 'push' && secrets.STORE_AUTOMATION_TOKEN || github.token }} + if: always() && steps.pushable.outputs.value == 'true' run: | if ! git diff --quiet -- '*.json'; then git config user.name "streamcontroller-bot" @@ -63,7 +73,7 @@ jobs: git add -- '*.json' if [ "${{ github.event_name }}" = "push" ]; then git commit -m "fix(json): auto-repair invalid JSON pushed directly to main" - git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" HEAD:${{ github.ref_name }} + git push origin HEAD:${{ github.ref_name }} else git commit -m "fix(json): auto-repair invalid JSON" git push origin HEAD:${{ github.head_ref }} From 131c2e73b0af3dc801eb68d113e8a847677803a1 Mon Sep 17 00:00:00 2001 From: Core447 <100139110+Core447@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:54:15 +0200 Subject: [PATCH 08/13] test: re-verify auto-fix now pushes with a token that re-triggers CI --- Icons.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Icons.json b/Icons.json index 50fed55..a7a95ea 100644 --- a/Icons.json +++ b/Icons.json @@ -31,5 +31,5 @@ "commits": { "1.0.0": "e5a8cade8fc348e48ba97c47207c096d7d39c61e" } - } + }, ] From d57faa764339daa9a903e308dd874f917c5e04e7 Mon Sep 17 00:00:00 2001 From: streamcontroller-bot Date: Fri, 7 Aug 2026 08:54:35 +0000 Subject: [PATCH 09/13] fix(json): auto-repair invalid JSON --- Icons.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Icons.json b/Icons.json index a7a95ea..50fed55 100644 --- a/Icons.json +++ b/Icons.json @@ -31,5 +31,5 @@ "commits": { "1.0.0": "e5a8cade8fc348e48ba97c47207c096d7d39c61e" } - }, + } ] From 4819499b00555df7fb41251d6f0c10e5f82bf23d Mon Sep 17 00:00:00 2001 From: Core447 <100139110+Core447@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:59:30 +0200 Subject: [PATCH 10/13] debug: temporarily print STORE_AUTOMATION_TOKEN length to diagnose retrigger issue --- .github/workflows/jsoncheck.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/jsoncheck.yml b/.github/workflows/jsoncheck.yml index d8ea745..eb1566e 100644 --- a/.github/workflows/jsoncheck.yml +++ b/.github/workflows/jsoncheck.yml @@ -25,6 +25,11 @@ jobs: echo "value=false" >> "$GITHUB_OUTPUT" fi + - name: Debug token length (temporary) + env: + TOKEN: ${{ secrets.STORE_AUTOMATION_TOKEN }} + run: echo "STORE_AUTOMATION_TOKEN length = ${#TOKEN}" + - name: Checkout uses: actions/checkout@v4 with: From efa7bf1c8087b326396cc80711030e40077b1c11 Mon Sep 17 00:00:00 2001 From: Core447 <100139110+Core447@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:00:03 +0200 Subject: [PATCH 11/13] test: trigger check with token-length debug step active --- Icons.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Icons.json b/Icons.json index 50fed55..a7a95ea 100644 --- a/Icons.json +++ b/Icons.json @@ -31,5 +31,5 @@ "commits": { "1.0.0": "e5a8cade8fc348e48ba97c47207c096d7d39c61e" } - } + }, ] From 4daa56d2312455c6e81b09e0df01e9950e574ae1 Mon Sep 17 00:00:00 2001 From: streamcontroller-bot Date: Fri, 7 Aug 2026 09:00:30 +0000 Subject: [PATCH 12/13] fix(json): auto-repair invalid JSON --- Icons.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Icons.json b/Icons.json index a7a95ea..50fed55 100644 --- a/Icons.json +++ b/Icons.json @@ -31,5 +31,5 @@ "commits": { "1.0.0": "e5a8cade8fc348e48ba97c47207c096d7d39c61e" } - }, + } ] From ef1c2d1ec213824b527b1009eee9fdd90f796274 Mon Sep 17 00:00:00 2001 From: Core447 <100139110+Core447@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:02:06 +0200 Subject: [PATCH 13/13] chore: remove temporary token-length debug step --- .github/workflows/jsoncheck.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/jsoncheck.yml b/.github/workflows/jsoncheck.yml index eb1566e..d8ea745 100644 --- a/.github/workflows/jsoncheck.yml +++ b/.github/workflows/jsoncheck.yml @@ -25,11 +25,6 @@ jobs: echo "value=false" >> "$GITHUB_OUTPUT" fi - - name: Debug token length (temporary) - env: - TOKEN: ${{ secrets.STORE_AUTOMATION_TOKEN }} - run: echo "STORE_AUTOMATION_TOKEN length = ${#TOKEN}" - - name: Checkout uses: actions/checkout@v4 with: