diff --git a/.github/workflows/component-pr-welcome.yml b/.github/workflows/component-pr-welcome.yml index 54e70b8c..9050b95c 100644 --- a/.github/workflows/component-pr-welcome.yml +++ b/.github/workflows/component-pr-welcome.yml @@ -73,6 +73,12 @@ jobs: `4. šŸ“¦ **Catalog regeneration** — the component catalog is rebuilt automatically.`, `5. šŸš€ **Live on [aitmpl.com](https://www.aitmpl.com)** — your component appears on the website after deploy.`, ``, + `### āš ļø Do not commit generated catalog files`, + `Please make sure your PR does **not** include \`docs/components.json\` or anything under \`dashboard/public/\` `, + `(\`components.json\`, \`counts.json\`, \`search-index.json\`, \`components/*.json\`, \`component-content/**\`). `, + `Those files are regenerated automatically by \`scripts/generate_components_json.py\` after merge, and committing them causes merge conflicts. `, + `If you already ran the script, revert them with: \`git checkout origin/main -- docs/components.json dashboard/public/\``, + ``, `### While you wait`, `- Check the **Security Audit** comment below for any issues to fix.`, `- Make sure your component follows the [contribution guide](https://github.com/${owner}/${repo}/blob/main/CLAUDE.md#component-system).`, diff --git a/.github/workflows/generated-files-guard.yml b/.github/workflows/generated-files-guard.yml new file mode 100644 index 00000000..b6714ba2 --- /dev/null +++ b/.github/workflows/generated-files-guard.yml @@ -0,0 +1,111 @@ +name: Generated Files Guard + +# Blocks external PRs that commit generated catalog artifacts. Those files are +# produced by scripts/generate_components_json.py (run by maintainers and the +# daily update-json-data workflow) and committing them from a fork causes merge +# conflicts with every other catalog change. +# +# Uses pull_request_target so it can comment on fork PRs. It never checks out +# or executes PR code — it only reads the changed-file list via the API. + +on: + pull_request_target: + types: [opened, reopened, synchronize] + paths: + - 'docs/components.json' + - 'dashboard/public/components.json' + - 'dashboard/public/counts.json' + - 'dashboard/public/search-index.json' + - 'dashboard/public/components/**' + - 'dashboard/public/component-content/**' + +permissions: + pull-requests: write + +jobs: + guard: + name: No generated catalog files + runs-on: ubuntu-latest + steps: + - name: Check for generated catalog files in the PR + uses: actions/github-script@v8 + with: + script: | + const { owner, repo } = context.repo; + const pr = context.payload.pull_request; + const prNumber = pr.number; + const marker = ''; + + // Maintainers regenerate the catalog on purpose (e.g. sync PRs). + const trusted = ['OWNER', 'MEMBER', 'COLLABORATOR']; + if (trusted.includes(pr.author_association)) { + core.info(`Author is ${pr.author_association} — skipping guard.`); + return; + } + + const generatedPatterns = [ + /^docs\/components\.json$/, + /^dashboard\/public\/components\.json$/, + /^dashboard\/public\/counts\.json$/, + /^dashboard\/public\/search-index\.json$/, + /^dashboard\/public\/components\//, + /^dashboard\/public\/component-content\//, + ]; + + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, repo, pull_number: prNumber, per_page: 100, + }); + const offending = files + .map(f => f.filename) + .filter(name => generatedPatterns.some(re => re.test(name))); + + const existing = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number: prNumber, per_page: 100, + }); + const previous = existing.find(c => c.body && c.body.includes(marker)); + + if (offending.length === 0) { + if (previous) { + await github.rest.issues.updateComment({ + owner, repo, comment_id: previous.id, + body: `${marker}\nāœ… Generated catalog files have been removed from this PR. Thanks!`, + }); + } + core.info('No generated catalog files in this PR.'); + return; + } + + const list = offending.map(f => `- \`${f}\``).join('\n'); + const body = [ + marker, + `## āš ļø Please remove generated catalog files from this PR`, + ``, + `This PR commits files that are **generated automatically** by \`scripts/generate_components_json.py\`. `, + `Committing them causes merge conflicts with every other PR that touches the catalog, so contributor PRs must not include them:`, + ``, + list, + ``, + `### How to fix`, + ``, + `From your branch, restore those files to their \`main\` version and push:`, + ``, + '```bash', + `git fetch origin main`, + `git checkout origin/main -- docs/components.json dashboard/public/`, + `git commit -m "chore: drop generated catalog files"`, + `git push`, + '```', + ``, + `Your PR should only contain changes under \`cli-tool/components/\` (plus any supporting files). `, + `The catalog and dashboard counts are regenerated automatically after merge — you don't need to run the script.`, + ``, + `_This check will pass automatically once the files above are no longer part of the PR._`, + ].join('\n'); + + if (previous) { + await github.rest.issues.updateComment({ owner, repo, comment_id: previous.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body }); + } + + core.setFailed(`PR includes ${offending.length} generated catalog file(s). See the PR comment for instructions.`); diff --git a/CLAUDE.md b/CLAUDE.md index 23de2c11..3a9f7980 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,6 +105,30 @@ Use the component-reviewer agent to review [component-path] `generate_components_json.py`, commit, or publish until testing is confirmed or explicitly skipped by the user. 7. Run `python scripts/generate_components_json.py` to update catalog + (**maintainers only** — see below) + +#### Generated catalog files: who regenerates them + +`scripts/generate_components_json.py` writes `docs/components.json` and the +split `dashboard/public/` artifacts (`components.json`, `counts.json`, +`search-index.json`, `components/*.json`, `component-content/**`). These files +are **generated output, never hand-edited**, and who commits them depends on +the workflow: + +| Workflow | Regenerate + commit the catalog? | +|----------|----------------------------------| +| Maintainer working directly on this repo (local branch, sync PRs, agent-driven migrations) | āœ… Yes — run the script and commit the output with the component | +| **External contributor PR (fork)** | āŒ **No** — the PR must only contain files under `cli-tool/components/` (plus supporting files). The catalog is regenerated automatically after merge (`update-json-data.yml` daily cron, or a maintainer). | + +Why: the generated JSON files are single-line blobs that change on every +component/download-count update, so two PRs that both commit them always +conflict. `.github/workflows/generated-files-guard.yml` fails any +non-maintainer PR that touches them and posts revert instructions; the +`component-pr-welcome.yml` bot also warns about it up front. + +When reviewing a contributor PR that includes these files, ask them to revert +with `git checkout origin/main -- docs/components.json dashboard/public/` rather +than resolving the conflict by hand. **The component-reviewer agent checks:** - āœ… Valid YAML frontmatter and required fields