From 8ee3fbf52664e20fdfd8bd1f622cca364bdb6b99 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 18:35:09 +0000 Subject: [PATCH 1/4] chore(hooks): auto-rebuild editor-app assets on pre-commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When any file under editor-app/src/ is staged, the pre-commit hook now runs npm ci + npm run build and stages the updated app.js/app.css automatically — same pattern as the existing dist-bundle rebuild. https://claude.ai/code/session_01MR5eh7i7GRVpvudPrCjm2w --- .githooks/pre-commit | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 25f96390..845df42d 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -23,3 +23,13 @@ echo "$outputs" | while IFS= read -r f; do git add "$f" fi done + +# Rebuild the Bricks editor app when any of its sources are staged. +EDITOR_SRC="plugins/SLASHED-for-WP/integrations/bricks/editor-app/src" +STAGED_EDITOR=$(git diff --cached --name-only --diff-filter=ACM | grep "^${EDITOR_SRC}" || true) +if [ -n "$STAGED_EDITOR" ]; then + echo "reBEMer editor-app sources changed — rebuilding assets..." + (cd plugins/SLASHED-for-WP/integrations/bricks/editor-app && npm ci --silent && npm run build --silent) + git add plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.js + git add plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.css +fi From c03b88d4af110f3ba1aa758f581343c21e041436 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 18:38:08 +0000 Subject: [PATCH 2/4] chore: auto-rebuild all generated artifacts on commit + CI Pre-commit hook now covers admin-app (same pattern as editor-app) and regenerates docs/inventory when CSS sources change. CI gets a matching admin-app-freshness job. Every committed build artifact now has both a local auto-rebuild guard and a CI staleness check. https://claude.ai/code/session_01MR5eh7i7GRVpvudPrCjm2w --- .githooks/pre-commit | 20 ++++++++++++++++++++ .github/workflows/ci.yml | 19 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 845df42d..bbb8853c 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -33,3 +33,23 @@ if [ -n "$STAGED_EDITOR" ]; then git add plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.js git add plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.css fi + +# Rebuild the Bricks admin app when any of its sources are staged. +ADMIN_SRC="plugins/SLASHED-for-WP/integrations/bricks/admin-app/src" +STAGED_ADMIN=$(git diff --cached --name-only --diff-filter=ACM | grep "^${ADMIN_SRC}" || true) +if [ -n "$STAGED_ADMIN" ]; then + echo "Admin-app sources changed — rebuilding assets..." + (cd plugins/SLASHED-for-WP/integrations/bricks/admin-app && npm ci --silent && npm run build --silent) + git add plugins/SLASHED-for-WP/integrations/bricks/assets/admin-app/app.js + git add plugins/SLASHED-for-WP/integrations/bricks/assets/admin-app/app.css +fi + +# Regenerate docs/inventory when CSS source files are staged. +STAGED_CSS_SRC=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^(core|optional)/' || true) +if [ -n "$STAGED_CSS_SRC" ]; then + echo "CSS sources changed — regenerating docs and inventory..." + npm run docs --silent + git add docs/tokens.md docs/classes.md + git add plugins/SLASHED-for-WP/integrations/bricks/data/inventory.json + git add plugins/SLASHED-for-WP/data/classes-hints.json +fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f4be180..338c25b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,25 @@ jobs: - run: git diff --exit-code plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.js || (echo "::error::assets/editor-app/app.js is stale — run 'npm run build' in editor-app/ and commit" && exit 1) - run: git diff --exit-code plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.css || (echo "::error::assets/editor-app/app.css is stale — run 'npm run build' in editor-app/ and commit" && exit 1) + admin-app-freshness: + name: Verify admin app build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22 + cache: npm + cache-dependency-path: plugins/SLASHED-for-WP/integrations/bricks/admin-app/package-lock.json + - run: npm ci + working-directory: plugins/SLASHED-for-WP/integrations/bricks/admin-app + - run: npm run build + working-directory: plugins/SLASHED-for-WP/integrations/bricks/admin-app + - run: git diff --exit-code plugins/SLASHED-for-WP/integrations/bricks/assets/admin-app/app.js || (echo "::error::assets/admin-app/app.js is stale — run 'npm run build' in admin-app/ and commit" && exit 1) + - run: git diff --exit-code plugins/SLASHED-for-WP/integrations/bricks/assets/admin-app/app.css || (echo "::error::assets/admin-app/app.css is stale — run 'npm run build' in admin-app/ and commit" && exit 1) + test: name: Regression tests runs-on: ubuntu-latest From 0924a119d01fab9da3c828ce138599f51b91e86a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 18:40:05 +0000 Subject: [PATCH 3/4] chore: single manifest drives all artifact freshness checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/artifacts.json is now the one place to register a build artifact (source glob → build command → outputs). Both the pre-commit hook and CI read from it via scripts/check-artifacts.js: --fix (hook) rebuilds only affected artifacts and re-stages outputs --check (CI) rebuilds everything and fails on any stale output Replaces the three separate *-freshness CI jobs and the inline per-artifact logic in the pre-commit hook. https://claude.ai/code/session_01MR5eh7i7GRVpvudPrCjm2w --- .githooks/pre-commit | 49 ++-------------------------- .github/workflows/ci.yml | 48 ++------------------------- scripts/artifacts.json | 33 +++++++++++++++++++ scripts/check-artifacts.js | 67 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 91 deletions(-) create mode 100644 scripts/artifacts.json create mode 100644 scripts/check-artifacts.js diff --git a/.githooks/pre-commit b/.githooks/pre-commit index bbb8853c..5e700c75 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -7,49 +7,6 @@ if [ -n "$STAGED_CSS" ]; then echo "$STAGED_CSS" | tr '\n' '\0' | xargs -0 npx stylelint fi -# Rebuild dist bundles and stage them. -# -# dist/*.css is gitignored on main (built by `npm run build`, served via -# the dist branch + Releases — see .gitignore), so `git add` on those -# paths errors out and, under `set -e`, aborts every commit. Only stage -# outputs that are actually tracked; skip ignored ones. This keeps the -# rebuild-and-stage behaviour for any context where dist IS tracked -# without breaking commits where it isn't. -node scripts/bundle.js -outputs=$(node -e "const c=require('./bundle.config.json'); const b=Array.isArray(c.bundles)?c.bundles:[c]; process.stdout.write(b.map(x=>x.output).join('\n'))") -echo "$outputs" | while IFS= read -r f; do - [ -n "$f" ] || continue - if git ls-files --error-unmatch "$f" >/dev/null 2>&1; then - git add "$f" - fi -done - -# Rebuild the Bricks editor app when any of its sources are staged. -EDITOR_SRC="plugins/SLASHED-for-WP/integrations/bricks/editor-app/src" -STAGED_EDITOR=$(git diff --cached --name-only --diff-filter=ACM | grep "^${EDITOR_SRC}" || true) -if [ -n "$STAGED_EDITOR" ]; then - echo "reBEMer editor-app sources changed — rebuilding assets..." - (cd plugins/SLASHED-for-WP/integrations/bricks/editor-app && npm ci --silent && npm run build --silent) - git add plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.js - git add plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.css -fi - -# Rebuild the Bricks admin app when any of its sources are staged. -ADMIN_SRC="plugins/SLASHED-for-WP/integrations/bricks/admin-app/src" -STAGED_ADMIN=$(git diff --cached --name-only --diff-filter=ACM | grep "^${ADMIN_SRC}" || true) -if [ -n "$STAGED_ADMIN" ]; then - echo "Admin-app sources changed — rebuilding assets..." - (cd plugins/SLASHED-for-WP/integrations/bricks/admin-app && npm ci --silent && npm run build --silent) - git add plugins/SLASHED-for-WP/integrations/bricks/assets/admin-app/app.js - git add plugins/SLASHED-for-WP/integrations/bricks/assets/admin-app/app.css -fi - -# Regenerate docs/inventory when CSS source files are staged. -STAGED_CSS_SRC=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^(core|optional)/' || true) -if [ -n "$STAGED_CSS_SRC" ]; then - echo "CSS sources changed — regenerating docs and inventory..." - npm run docs --silent - git add docs/tokens.md docs/classes.md - git add plugins/SLASHED-for-WP/integrations/bricks/data/inventory.json - git add plugins/SLASHED-for-WP/data/classes-hints.json -fi +# Rebuild any generated artifacts whose sources changed and re-stage the outputs. +# Registration lives in scripts/artifacts.json — add entries there, not here. +node scripts/check-artifacts.js --fix diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 338c25b0..09c7f4be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,8 +38,8 @@ jobs: name: dist path: dist/ - docs-freshness: - name: Verify generated docs + artifacts-freshness: + name: Verify all generated artifacts runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -50,50 +50,8 @@ jobs: node-version: 22 cache: npm - run: npm ci - - run: npm run docs - run: npm run audit:check - - run: git diff --exit-code docs/tokens.md || (echo "::error::docs/tokens.md is stale — run 'npm run docs' and commit" && exit 1) - - run: git diff --exit-code docs/classes.md || (echo "::error::docs/classes.md is stale — run 'npm run docs' and commit" && exit 1) - - run: git diff --exit-code plugins/SLASHED-for-WP/integrations/bricks/data/inventory.json || (echo "::error::inventory.json is stale — run 'npm run docs' and commit" && exit 1) - - run: git diff --exit-code plugins/SLASHED-for-WP/data/classes-hints.json || (echo "::error::classes-hints.json is stale — run 'npm run docs' and commit" && exit 1) - - editor-app-freshness: - name: Verify editor app build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 22 - cache: npm - cache-dependency-path: plugins/SLASHED-for-WP/integrations/bricks/editor-app/package-lock.json - - run: npm ci - working-directory: plugins/SLASHED-for-WP/integrations/bricks/editor-app - - run: npm run build - working-directory: plugins/SLASHED-for-WP/integrations/bricks/editor-app - - run: git diff --exit-code plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.js || (echo "::error::assets/editor-app/app.js is stale — run 'npm run build' in editor-app/ and commit" && exit 1) - - run: git diff --exit-code plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.css || (echo "::error::assets/editor-app/app.css is stale — run 'npm run build' in editor-app/ and commit" && exit 1) - - admin-app-freshness: - name: Verify admin app build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 22 - cache: npm - cache-dependency-path: plugins/SLASHED-for-WP/integrations/bricks/admin-app/package-lock.json - - run: npm ci - working-directory: plugins/SLASHED-for-WP/integrations/bricks/admin-app - - run: npm run build - working-directory: plugins/SLASHED-for-WP/integrations/bricks/admin-app - - run: git diff --exit-code plugins/SLASHED-for-WP/integrations/bricks/assets/admin-app/app.js || (echo "::error::assets/admin-app/app.js is stale — run 'npm run build' in admin-app/ and commit" && exit 1) - - run: git diff --exit-code plugins/SLASHED-for-WP/integrations/bricks/assets/admin-app/app.css || (echo "::error::assets/admin-app/app.css is stale — run 'npm run build' in admin-app/ and commit" && exit 1) + - run: node scripts/check-artifacts.js --check test: name: Regression tests diff --git a/scripts/artifacts.json b/scripts/artifacts.json new file mode 100644 index 00000000..031a99c0 --- /dev/null +++ b/scripts/artifacts.json @@ -0,0 +1,33 @@ +[ + { + "name": "CSS bundles + docs", + "srcGlobs": ["core/", "optional/"], + "cmd": "node scripts/bundle.js && npm run docs", + "outputs": [ + "docs/tokens.md", + "docs/classes.md", + "plugins/SLASHED-for-WP/integrations/bricks/data/inventory.json", + "plugins/SLASHED-for-WP/data/classes-hints.json" + ] + }, + { + "name": "editor-app", + "srcGlobs": ["plugins/SLASHED-for-WP/integrations/bricks/editor-app/src/"], + "cwd": "plugins/SLASHED-for-WP/integrations/bricks/editor-app", + "cmd": "npm ci --silent && npm run build --silent", + "outputs": [ + "plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.js", + "plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.css" + ] + }, + { + "name": "admin-app", + "srcGlobs": ["plugins/SLASHED-for-WP/integrations/bricks/admin-app/src/"], + "cwd": "plugins/SLASHED-for-WP/integrations/bricks/admin-app", + "cmd": "npm ci --silent && npm run build --silent", + "outputs": [ + "plugins/SLASHED-for-WP/integrations/bricks/assets/admin-app/app.js", + "plugins/SLASHED-for-WP/integrations/bricks/assets/admin-app/app.css" + ] + } +] diff --git a/scripts/check-artifacts.js b/scripts/check-artifacts.js new file mode 100644 index 00000000..bc3a23ed --- /dev/null +++ b/scripts/check-artifacts.js @@ -0,0 +1,67 @@ +#!/usr/bin/env node +/** + * Ensures all generated/compiled artifacts are in sync with their sources. + * + * Modes: + * --fix (pre-commit) Run only the builds whose sources are staged, then + * stage the outputs. Skips gitignored outputs silently. + * --check (CI) Run every build unconditionally, then assert git diff is + * clean for each output. Exits non-zero on any staleness. + * + * To register a new build artifact, add an entry to scripts/artifacts.json. + */ + +import { execSync } from 'child_process'; +import { readFileSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const artifacts = JSON.parse(readFileSync(resolve(root, 'scripts/artifacts.json'), 'utf8')); +const mode = process.argv.includes('--check') ? 'check' : 'fix'; + +function run(cmd, cwd = root) { + execSync(cmd, { cwd, stdio: 'inherit', shell: true }); +} + +function git(args) { + return execSync(`git ${args}`, { cwd: root, encoding: 'utf8' }); +} + +function isTracked(path) { + try { git(`ls-files --error-unmatch ${path}`); return true; } catch { return false; } +} + +if (mode === 'fix') { + const staged = new Set(git('diff --cached --name-only --diff-filter=ACM').trim().split('\n').filter(Boolean)); + + for (const artifact of artifacts) { + const triggered = artifact.srcGlobs.some(glob => [...staged].some(f => f.startsWith(glob))); + if (!triggered) continue; + + console.log(`[artifacts] ${artifact.name} sources changed — rebuilding…`); + run(artifact.cmd, artifact.cwd ? resolve(root, artifact.cwd) : root); + + for (const out of artifact.outputs) { + if (isTracked(out)) run(`git add ${out}`); + } + } +} else { + let failed = false; + + for (const artifact of artifacts) { + console.log(`[artifacts] Checking ${artifact.name}…`); + run(artifact.cmd, artifact.cwd ? resolve(root, artifact.cwd) : root); + + for (const out of artifact.outputs) { + try { + git(`diff --exit-code ${out}`); + } catch { + console.error(`::error::${out} is stale — rebuild ${artifact.name} and commit`); + failed = true; + } + } + } + + if (failed) process.exit(1); +} From 7019136cb2a3cc3f74ad01ac191c80b1409f6f59 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 18:50:04 +0000 Subject: [PATCH 4/4] fix(check-artifacts): address CodeRabbit review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert to CommonJS (was ESM, root package.json has no type:module) - Rename srcGlobs → srcPrefixes to match actual prefix-matching behaviour - Split cmd → installCmd + buildCmd; pre-commit --fix skips installCmd so npm ci doesn't run on every local commit - Use execFileSync with argument arrays for all git calls (space-safe, no shell-injection surface) - Add permissions: contents: read to artifacts-freshness CI job https://claude.ai/code/session_01MR5eh7i7GRVpvudPrCjm2w --- .github/workflows/ci.yml | 2 ++ scripts/artifacts.json | 14 +++++++------ scripts/check-artifacts.js | 42 +++++++++++++++++++++++--------------- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09c7f4be..11c1180d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,8 @@ jobs: artifacts-freshness: name: Verify all generated artifacts runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: diff --git a/scripts/artifacts.json b/scripts/artifacts.json index 031a99c0..84418049 100644 --- a/scripts/artifacts.json +++ b/scripts/artifacts.json @@ -1,8 +1,8 @@ [ { "name": "CSS bundles + docs", - "srcGlobs": ["core/", "optional/"], - "cmd": "node scripts/bundle.js && npm run docs", + "srcPrefixes": ["core/", "optional/"], + "buildCmd": "node scripts/bundle.js && npm run docs", "outputs": [ "docs/tokens.md", "docs/classes.md", @@ -12,9 +12,10 @@ }, { "name": "editor-app", - "srcGlobs": ["plugins/SLASHED-for-WP/integrations/bricks/editor-app/src/"], + "srcPrefixes": ["plugins/SLASHED-for-WP/integrations/bricks/editor-app/src/"], "cwd": "plugins/SLASHED-for-WP/integrations/bricks/editor-app", - "cmd": "npm ci --silent && npm run build --silent", + "installCmd": "npm ci --silent", + "buildCmd": "npm run build --silent", "outputs": [ "plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.js", "plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.css" @@ -22,9 +23,10 @@ }, { "name": "admin-app", - "srcGlobs": ["plugins/SLASHED-for-WP/integrations/bricks/admin-app/src/"], + "srcPrefixes": ["plugins/SLASHED-for-WP/integrations/bricks/admin-app/src/"], "cwd": "plugins/SLASHED-for-WP/integrations/bricks/admin-app", - "cmd": "npm ci --silent && npm run build --silent", + "installCmd": "npm ci --silent", + "buildCmd": "npm run build --silent", "outputs": [ "plugins/SLASHED-for-WP/integrations/bricks/assets/admin-app/app.js", "plugins/SLASHED-for-WP/integrations/bricks/assets/admin-app/app.css" diff --git a/scripts/check-artifacts.js b/scripts/check-artifacts.js index bc3a23ed..c531dda2 100644 --- a/scripts/check-artifacts.js +++ b/scripts/check-artifacts.js @@ -4,46 +4,50 @@ * * Modes: * --fix (pre-commit) Run only the builds whose sources are staged, then - * stage the outputs. Skips gitignored outputs silently. + * stage the outputs. Skips untracked outputs silently. * --check (CI) Run every build unconditionally, then assert git diff is * clean for each output. Exits non-zero on any staleness. * * To register a new build artifact, add an entry to scripts/artifacts.json. */ -import { execSync } from 'child_process'; -import { readFileSync } from 'fs'; -import { resolve, dirname } from 'path'; -import { fileURLToPath } from 'url'; +'use strict'; -const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const { execSync, execFileSync } = require('child_process'); +const { readFileSync } = require('fs'); +const { resolve } = require('path'); + +const root = resolve(__dirname, '..'); const artifacts = JSON.parse(readFileSync(resolve(root, 'scripts/artifacts.json'), 'utf8')); const mode = process.argv.includes('--check') ? 'check' : 'fix'; -function run(cmd, cwd = root) { - execSync(cmd, { cwd, stdio: 'inherit', shell: true }); +function run(cmd, cwd) { + execSync(cmd, { cwd: cwd || root, stdio: 'inherit', shell: true }); } -function git(args) { - return execSync(`git ${args}`, { cwd: root, encoding: 'utf8' }); +function gitFile(...args) { + return execFileSync('git', args, { cwd: root, encoding: 'utf8' }); } function isTracked(path) { - try { git(`ls-files --error-unmatch ${path}`); return true; } catch { return false; } + try { gitFile('ls-files', '--error-unmatch', path); return true; } catch { return false; } } if (mode === 'fix') { - const staged = new Set(git('diff --cached --name-only --diff-filter=ACM').trim().split('\n').filter(Boolean)); + const staged = new Set( + gitFile('diff', '--cached', '--name-only', '--diff-filter=ACM') + .trim().split('\n').filter(Boolean) + ); for (const artifact of artifacts) { - const triggered = artifact.srcGlobs.some(glob => [...staged].some(f => f.startsWith(glob))); + const triggered = artifact.srcPrefixes.some(prefix => [...staged].some(f => f.startsWith(prefix))); if (!triggered) continue; console.log(`[artifacts] ${artifact.name} sources changed — rebuilding…`); - run(artifact.cmd, artifact.cwd ? resolve(root, artifact.cwd) : root); + run(artifact.buildCmd, artifact.cwd ? resolve(root, artifact.cwd) : root); for (const out of artifact.outputs) { - if (isTracked(out)) run(`git add ${out}`); + if (isTracked(out)) gitFile('add', out); } } } else { @@ -51,11 +55,15 @@ if (mode === 'fix') { for (const artifact of artifacts) { console.log(`[artifacts] Checking ${artifact.name}…`); - run(artifact.cmd, artifact.cwd ? resolve(root, artifact.cwd) : root); + // In CI, run the full install+build; locally (--fix) only buildCmd is used. + const cmd = artifact.installCmd + ? `${artifact.installCmd} && ${artifact.buildCmd}` + : artifact.buildCmd; + run(cmd, artifact.cwd ? resolve(root, artifact.cwd) : root); for (const out of artifact.outputs) { try { - git(`diff --exit-code ${out}`); + gitFile('diff', '--exit-code', '--', out); } catch { console.error(`::error::${out} is stale — rebuild ${artifact.name} and commit`); failed = true;