Skip to content

Commit dfd11cf

Browse files
pfeerickclaude
andauthored
feat: sync issue template tags/categories from scripts.json (#65)
* feat(tools): sync issue template tags/categories from scripts.json Category and tag lists were hardcoded in four places (validate_scripts.py, issue_to_scripts.py, and both issue form templates) and had already drifted once. scripts.schema.json is now the single canonical source: tags and categories are auto-promoted from actual usage in scripts.json (tags are a pure usage snapshot; categories are monotonic add-only since they drive the site's nav tabs and shouldn't vanish just because they're briefly empty). tools/sync_issue_template_options.py regenerates the schema's examples and both templates' marker-bounded option blocks, with --check/--write modes and tests. A new "New Category" free-text field mirrors the existing "Additional Tags" field, since the dropdown previously had no way to introduce a new value. CI (validate-issue-templates.yml) self-heals same-repo branches on push, gates PRs with --check, and — since GITHUB_TOKEN can never push to a fork — posts a sticky comment with the diff and self-resolve instructions on fork PRs via a carefully scoped pull_request_target job. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): disable uv dependency caching in issue-template workflow No lock/requirements file exists in this repo for setup-uv to key a cache on, so it was warning that the cache could never invalidate. There's nothing to actually cache (the tools/*.py scripts are stdlib-only), so disable it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): disable uv dependency caching in validate-scripts-json workflow Same fix as the issue-template workflow — no lock/requirements file exists for setup-uv to key a cache on, so it was warning the cache could never invalidate. Nothing to cache here either. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): disable uv dependency caching in remaining workflows Completes the same fix for gh-pages.yml and script-submission.yml — no lock/requirements file exists for setup-uv to key a cache on in this repo, so it was warning the cache could never invalidate. Nothing to cache here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(tools): sort tags/categories case-insensitively Plain sorted() put 'GPS & Mapping' before 'Games & Fun' (uppercase 'P' < lowercase 'a' in ASCII) — not the order a human reading the dropdown would expect. Also fixes a related bug the case-insensitive change would otherwise introduce: the schema-vs-current comparisons re-sorted both sides before comparing, which hid ordering drift in scripts.schema.json's own examples arrays (they're supposed to already hold the canonical sorted list, so a stale on-disk order needs to be caught, not normalized away). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(tools): add --prune-categories to sync script Removing a stale, currently-unused category previously meant hand-editing scripts.schema.json's examples array directly. --prune-categories overrides the normal monotonic-add-only behavior for a single invocation, removing categories with zero current scripts.json entries from the schema and both templates in the same pass as a normal --write. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 0a81528 commit dfd11cf

12 files changed

Lines changed: 917 additions & 44 deletions

‎.github/ISSUE_TEMPLATE/add-script.yml‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,33 @@ body:
2525
id: category
2626
attributes:
2727
label: Category
28-
description: Choose the most appropriate category.
28+
description: |
29+
Choose the most appropriate category. If none of these fit, leave this blank and
30+
use the New Category field below instead.
2931
options:
32+
# --- BEGIN AUTO-GENERATED CATEGORIES (see scripts.schema.json: items.properties.category.examples) ---
3033
- Audio & Media
3134
- Flight Controller Config
3235
- Games & Fun
3336
- GPS & Mapping
3437
- Logging & Analysis
3538
- Radio Tools
3639
- Telemetry & Widgets
40+
# --- END AUTO-GENERATED CATEGORIES ---
3741
validations:
38-
required: true
42+
required: false
43+
44+
- type: input
45+
id: category_extra
46+
attributes:
47+
label: New Category
48+
description: |
49+
Only fill this in if none of the categories above fit — this creates a brand-new
50+
gallery category (a new top-level tab on the site), so please only use it if you're
51+
confident an existing one genuinely doesn't apply.
52+
placeholder: e.g. Simulators
53+
validations:
54+
required: false
3955

4056
- type: textarea
4157
id: description
@@ -72,6 +88,7 @@ body:
7288
label: Tags
7389
description: Check all that apply.
7490
options:
91+
# --- BEGIN AUTO-GENERATED TAGS (see scripts.schema.json: items.properties.tags.items.examples) ---
7592
- label: ardupilot
7693
- label: battery
7794
- label: betaflight
@@ -85,12 +102,14 @@ body:
85102
- label: gps
86103
- label: heli
87104
- label: inav
88-
- label: logging
105+
- label: multi-protocol
89106
- label: quad
90107
- label: rotorflight
91108
- label: script
109+
- label: spektrum
92110
- label: tool
93111
- label: widget
112+
# --- END AUTO-GENERATED TAGS ---
94113

95114
- type: input
96115
id: tags_extra

‎.github/ISSUE_TEMPLATE/update-script.yml‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,31 @@ body:
2323
id: category
2424
attributes:
2525
label: Category
26-
description: Leave unselected to keep the existing category.
26+
description: |
27+
Leave unselected to keep the existing category. If none of these fit, use the
28+
New Category field below instead.
2729
options:
30+
# --- BEGIN AUTO-GENERATED CATEGORIES (see scripts.schema.json: items.properties.category.examples) ---
2831
- Audio & Media
2932
- Flight Controller Config
3033
- Games & Fun
3134
- GPS & Mapping
3235
- Logging & Analysis
3336
- Radio Tools
3437
- Telemetry & Widgets
38+
# --- END AUTO-GENERATED CATEGORIES ---
39+
validations:
40+
required: false
41+
42+
- type: input
43+
id: category_extra
44+
attributes:
45+
label: New Category
46+
description: |
47+
Only fill this in if you want to change to a category not in the dropdown above —
48+
this creates a brand-new gallery category (a new top-level tab on the site), so
49+
please only use it if you're confident an existing one genuinely doesn't apply.
50+
placeholder: e.g. Simulators
3551
validations:
3652
required: false
3753

@@ -72,6 +88,7 @@ body:
7288
Leave **all boxes unchecked** to keep existing tags unchanged.
7389
Checking any box (or filling in Additional Tags) will **replace the full tag list**.
7490
options:
91+
# --- BEGIN AUTO-GENERATED TAGS (see scripts.schema.json: items.properties.tags.items.examples) ---
7592
- label: ardupilot
7693
- label: battery
7794
- label: betaflight
@@ -85,12 +102,14 @@ body:
85102
- label: gps
86103
- label: heli
87104
- label: inav
88-
- label: logging
105+
- label: multi-protocol
89106
- label: quad
90107
- label: rotorflight
91108
- label: script
109+
- label: spektrum
92110
- label: tool
93111
- label: widget
112+
# --- END AUTO-GENERATED TAGS ---
94113

95114
- type: input
96115
id: tags_extra

‎.github/workflows/gh-pages.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ jobs:
3131
steps:
3232
- uses: actions/checkout@v6
3333
- uses: astral-sh/setup-uv@v8.1.0
34+
with:
35+
enable-cache: false
3436
- name: Generate site
3537
run: uv run tools/generate_site.py --scripts-json scripts.json --assets-dir ASSETS --output-dir site
3638
- name: Upload Pages artifact

‎.github/workflows/script-submission.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ jobs:
2121
- uses: actions/checkout@v6
2222

2323
- uses: astral-sh/setup-uv@v8.1.0
24+
with:
25+
enable-cache: false
2426

2527
- name: Write issue body to file
2628
env:
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
name: Validate Issue Template Tags & Categories
2+
3+
on:
4+
push:
5+
paths:
6+
- 'scripts.json'
7+
- 'scripts.schema.json'
8+
- '.github/ISSUE_TEMPLATE/add-script.yml'
9+
- '.github/ISSUE_TEMPLATE/update-script.yml'
10+
- 'tools/sync_issue_template_options.py'
11+
- 'tools/test_sync_issue_template_options.py'
12+
- '.github/workflows/validate-issue-templates.yml'
13+
pull_request:
14+
paths:
15+
- 'scripts.json'
16+
- 'scripts.schema.json'
17+
- '.github/ISSUE_TEMPLATE/add-script.yml'
18+
- '.github/ISSUE_TEMPLATE/update-script.yml'
19+
- 'tools/sync_issue_template_options.py'
20+
- 'tools/test_sync_issue_template_options.py'
21+
- '.github/workflows/validate-issue-templates.yml'
22+
pull_request_target:
23+
paths:
24+
- 'scripts.json'
25+
- 'scripts.schema.json'
26+
- '.github/ISSUE_TEMPLATE/add-script.yml'
27+
- '.github/ISSUE_TEMPLATE/update-script.yml'
28+
- 'tools/sync_issue_template_options.py'
29+
- 'tools/test_sync_issue_template_options.py'
30+
- '.github/workflows/validate-issue-templates.yml'
31+
32+
permissions:
33+
contents: read
34+
35+
jobs:
36+
# Read-only check. Runs for every PR, including forks (which get a
37+
# restricted, non-writable GITHUB_TOKEN here — that's fine, this job never
38+
# writes anything).
39+
check:
40+
if: github.event_name == 'pull_request'
41+
runs-on: ubuntu-latest
42+
steps:
43+
- uses: actions/checkout@v6
44+
- uses: astral-sh/setup-uv@v8.1.0
45+
with:
46+
enable-cache: false
47+
- name: Run sync script tests
48+
run: uv run tools/test_sync_issue_template_options.py
49+
- name: Check tags/category sync
50+
run: uv run tools/sync_issue_template_options.py --check
51+
52+
# Self-heals same-repo branches (this workflow's own commit is made with
53+
# GITHUB_TOKEN, so it never re-triggers itself or any other workflow —
54+
# GitHub Actions doesn't fire new workflow-triggering events for
55+
# GITHUB_TOKEN-authored pushes).
56+
sync:
57+
if: github.event_name == 'push'
58+
runs-on: ubuntu-latest
59+
permissions:
60+
contents: write
61+
steps:
62+
- uses: actions/checkout@v6
63+
- uses: astral-sh/setup-uv@v8.1.0
64+
with:
65+
enable-cache: false
66+
- name: Run sync script tests
67+
run: uv run tools/test_sync_issue_template_options.py
68+
- name: Sync tags/category
69+
run: uv run tools/sync_issue_template_options.py --write | tee /tmp/sync-output.txt
70+
- name: Annotate new categories
71+
run: |
72+
line=$(grep '^new_categories=' /tmp/sync-output.txt || true)
73+
if [ -n "$line" ]; then
74+
echo "::warning::New categories introduced: ${line#new_categories=} — please confirm these aren't duplicates/typos before merging."
75+
fi
76+
- name: Annotate unused categories
77+
run: |
78+
line=$(grep '^unused_categories=' /tmp/sync-output.txt || true)
79+
if [ -n "$line" ]; then
80+
echo "::notice::Categories with no current entries: ${line#unused_categories=} — consider pruning scripts.schema.json if any of these are stale."
81+
fi
82+
- name: Commit corrections if needed
83+
run: |
84+
if ! git diff --quiet -- scripts.schema.json .github/ISSUE_TEMPLATE/add-script.yml .github/ISSUE_TEMPLATE/update-script.yml; then
85+
git config user.name "github-actions[bot]"
86+
git config user.email "github-actions[bot]@users.noreply.github.com"
87+
git add scripts.schema.json .github/ISSUE_TEMPLATE/add-script.yml .github/ISSUE_TEMPLATE/update-script.yml
88+
git commit -m "chore: sync issue template tags/categories"
89+
git push origin "HEAD:${{ github.ref_name }}"
90+
fi
91+
92+
# Fork PRs can never be auto-committed to (GITHUB_TOKEN cannot push to a
93+
# different repository under any trigger, pull_request_target included —
94+
# that event only grants a writable token for *this* repo). Instead this
95+
# job safely computes the same fix and posts it as a sticky PR comment.
96+
suggest-fix-for-fork-prs:
97+
if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository
98+
runs-on: ubuntu-latest
99+
permissions:
100+
pull-requests: write
101+
contents: read
102+
steps:
103+
# Deliberately no `ref:` override: this checks out the *base* branch's
104+
# trusted copy of the sync script, never the PR's own version — a
105+
# malicious PR can't smuggle in a modified script for this elevated
106+
# job to execute.
107+
- uses: actions/checkout@v6
108+
- uses: astral-sh/setup-uv@v8.1.0
109+
with:
110+
enable-cache: false
111+
- name: Fetch PR's scripts.json as data only (never executed)
112+
run: |
113+
git fetch origin "pull/${{ github.event.pull_request.number }}/head"
114+
git show FETCH_HEAD:scripts.json > /tmp/pr-scripts.json
115+
- name: Compute what --write would change, without committing
116+
id: diff
117+
run: |
118+
uv run tools/sync_issue_template_options.py --write --scripts-json /tmp/pr-scripts.json || true
119+
if git diff --quiet -- scripts.schema.json .github/ISSUE_TEMPLATE/add-script.yml .github/ISSUE_TEMPLATE/update-script.yml; then
120+
echo "has_diff=false" >> "$GITHUB_OUTPUT"
121+
else
122+
git diff -- scripts.schema.json .github/ISSUE_TEMPLATE/add-script.yml .github/ISSUE_TEMPLATE/update-script.yml | head -n 200 > /tmp/sync.diff
123+
echo "has_diff=true" >> "$GITHUB_OUTPUT"
124+
fi
125+
# Discard the local write — nothing from this job is ever committed.
126+
git checkout -- scripts.schema.json .github/ISSUE_TEMPLATE/add-script.yml .github/ISSUE_TEMPLATE/update-script.yml
127+
- name: Post or update sticky PR comment
128+
uses: actions/github-script@v9
129+
with:
130+
script: |
131+
const fs = require('fs');
132+
const marker = '<!-- sync-issue-template-options-bot -->';
133+
const hasDiff = '${{ steps.diff.outputs.has_diff }}' === 'true';
134+
let body;
135+
if (hasDiff) {
136+
const diff = fs.existsSync('/tmp/sync.diff') ? fs.readFileSync('/tmp/sync.diff', 'utf8') : '';
137+
body = marker + '\n\n⚠️ This PR introduces a new tag or category not yet reflected in `scripts.schema.json` / the issue form templates.\n\n' +
138+
'Since this PR comes from a fork, this can\'t be fixed automatically here. To resolve it:\n' +
139+
'- a maintainer can pull this branch into the repo (the automatic fix runs on push), or\n' +
140+
'- you (or a maintainer) can run `uv run tools/sync_issue_template_options.py --write` locally and commit the result to this branch.\n\n' +
141+
'<details><summary>Diff that would be applied (truncated to 200 lines)</summary>\n\n```diff\n' + diff + '\n```\n</details>';
142+
} else {
143+
body = marker + '\n\n✅ Tags/categories are in sync.';
144+
}
145+
const { data: comments } = await github.rest.issues.listComments({
146+
owner: context.repo.owner,
147+
repo: context.repo.repo,
148+
issue_number: context.issue.number,
149+
});
150+
const existing = comments.find(c => c.body.includes(marker));
151+
if (existing) {
152+
await github.rest.issues.updateComment({
153+
owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body,
154+
});
155+
} else if (hasDiff) {
156+
await github.rest.issues.createComment({
157+
owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body,
158+
});
159+
}

‎.github/workflows/validate-scripts-json.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@ jobs:
1919
steps:
2020
- uses: actions/checkout@v6
2121
- uses: astral-sh/setup-uv@v8.1.0
22+
with:
23+
enable-cache: false
2224
- name: Validate scripts.json
2325
run: uv run tools/validate_scripts.py --scripts-json scripts.json

‎.vscode/settings.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"json.schemas": [
3+
{
4+
"fileMatch": ["/scripts.json"],
5+
"url": "./scripts.schema.json"
6+
}
7+
]
8+
}

‎scripts.schema.json‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "EdgeTX Lua Scripts Gallery - scripts.json",
4+
"description": "Schema for scripts.json. Also the canonical source for the known 'category' and 'tags' vocabularies, read and written at runtime by tools/sync_issue_template_options.py to keep the Issue Form templates in sync with actual usage in scripts.json. 'category.examples' and 'tags.items.examples' are machine-generated - do not hand-edit them, they will be overwritten on the next sync. category.examples grows monotonically (never auto-pruned); tags.items.examples is a pure snapshot of current usage (shrinks when a tag falls out of use).",
5+
"type": "array",
6+
"items": {
7+
"type": "object",
8+
"title": "ScriptEntry",
9+
"required": [
10+
"name",
11+
"category",
12+
"description",
13+
"infourl",
14+
"tags"
15+
],
16+
"properties": {
17+
"name": {
18+
"type": "string",
19+
"minLength": 1,
20+
"description": "Display name as shown in the gallery."
21+
},
22+
"category": {
23+
"type": "string",
24+
"minLength": 1,
25+
"description": "Gallery category - drives the site's top-level navigation tabs. Machine-generated examples list below (monotonic add-only, see tools/sync_issue_template_options.py).",
26+
"examples": [
27+
"Audio & Media",
28+
"Flight Controller Config",
29+
"Games & Fun",
30+
"GPS & Mapping",
31+
"Logging & Analysis",
32+
"Radio Tools",
33+
"Telemetry & Widgets"
34+
]
35+
},
36+
"description": {
37+
"type": "string",
38+
"minLength": 1,
39+
"description": "Short 1-3 sentence description of the script."
40+
},
41+
"infourl": {
42+
"type": "string",
43+
"format": "uri",
44+
"description": "Link to the project page, GitHub repo, or documentation. Must start with http:// or https:// (enforced by tools/validate_scripts.py and tools/issue_to_scripts.py, not by this schema)."
45+
},
46+
"images": {
47+
"type": "array",
48+
"items": {
49+
"type": "string"
50+
},
51+
"description": "Screenshot references: local ASSETS/ paths or external image URLs."
52+
},
53+
"tags": {
54+
"type": "array",
55+
"minItems": 1,
56+
"items": {
57+
"type": "string",
58+
"minLength": 1,
59+
"description": "Machine-generated examples list below (current usage snapshot, see tools/sync_issue_template_options.py).",
60+
"examples": [
61+
"ardupilot",
62+
"battery",
63+
"betaflight",
64+
"black-and-white",
65+
"color",
66+
"crawler",
67+
"crossfire",
68+
"expresslrs",
69+
"fixed-wing",
70+
"game",
71+
"gps",
72+
"heli",
73+
"inav",
74+
"multi-protocol",
75+
"quad",
76+
"rotorflight",
77+
"script",
78+
"spektrum",
79+
"tool",
80+
"widget"
81+
]
82+
}
83+
}
84+
},
85+
"additionalProperties": true
86+
}
87+
}

0 commit comments

Comments
 (0)