Files
playbook/ui-ux-pro-max/.github/workflows/check-asset-sync.yml
T
2026-09-03 08:57:39 +00:00

96 lines
5.9 KiB
YAML

name: Check asset sync
on:
pull_request:
paths:
- "src/ui-ux-pro-max/**"
- "cli/assets/**"
- ".claude/skills/**"
- "cli/scripts/sync-assets.mjs"
- "cli/package.json"
- "scripts/evaluate-relevance.py"
- "scripts/relevance_metrics.py"
- ".github/workflows/check-asset-sync.yml"
push:
branches: [main]
paths:
- "src/ui-ux-pro-max/**"
- "cli/assets/**"
- "cli/package.json"
- ".claude/skills/**"
- "cli/scripts/sync-assets.mjs"
- ".github/workflows/check-asset-sync.yml"
jobs:
check-assets:
name: cli/assets and .claude/skills/ui-ux-pro-max must match src/ui-ux-pro-max
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
# check:assets runs `node scripts/sync-assets.mjs --check`, which uses only
# node builtins (no npm install needed) and normalizes CRLF/LF before
# hashing, so it compares content rather than line endings. It checks
# both cli/assets/ AND .claude/skills/ui-ux-pro-max/{data,scripts} --
# the latter is what Claude Code actually loads when this repo is
# installed as a plugin, and previously had no sync check at all.
- name: Check assets are in sync with source of truth
run: npm --prefix cli run check:assets
# Path contract (#474): skill instructions and scripts reach their scripts
# via skill-relative paths ("scripts/<file>" for the skill's own, "../<skill>/scripts/<file>"
# for a sibling sub-skill) so they resolve in every install context: marketplace/
# plugin cache, project-level CLI install, CLI --global install, manual copy.
# Home-rooted ("~/.claude/skills/<skill>"), project-rooted (".claude/skills/<skill>")
# and variable-rooted ("$HOME/...", "${PWD}/...") forms each work in only one of them.
# Every file under both skill trees is checked, not just SKILL.md - the first
# version of this step looked only at SKILL.md and missed 27 home-rooted paths
# one directory down in references/. The one allowed absolute form is
# "${CLAUDE_PLUGIN_ROOT}/.claude/skills/ui-ux-pro-max/..." (braced or bare variable,
# directly followed by "/"): the core skill's SKILL.md is hand-authored for the
# plugin install only and that variable anchors it there. The same variable into a
# sub-skill is flagged, because sub-skills are also installed by the CLI where it is
# unset - and a third check pins the token itself to that one file (plus the checker
# that names it), so a sub-skill cannot borrow the core form either: sub-skills ship
# through the CLI too, where the variable does not exist. Both patterns require a path INTO a named skill ("skills/<name>"), so a bare
# mention of the directory in prose or a code comment ("~/.claude/skills/, or ...")
# is not a hit - naming a skill after "skills/" in prose is.
# LC_ALL=C so that only NUL-containing files count as binary and an offending line
# with a stray non-UTF-8 byte is printed instead of suppressed as improperly encoded
# (the verdict is the same in both locales; the diagnostic is not); -I then skips
# .claude/skills/ui-styling/scripts/.coverage, a tracked SQLite database whose
# recorded absolute paths contain "/.claude/skills/ui-styling/".
# Not covered: backslash-separated Windows spellings and the platform-root-relative
# "skills/<skill>/..." form - the docs are bash-fenced and skill-relative, so neither
# appears; the positive side (every documented invocation names a file that ships)
# is src/ui-ux-pro-max/scripts/tests/test_skill_script_paths.py.
# grep exit codes: 0 = hits (violation), 1 = clean, 2 = error - only 1 passes, so an
# unreadable file can never turn into a green run (a missing tree is caught above).
- name: Path contract - no install-specific skill paths
run: |
for d in .claude/skills cli/assets/skills; do
[ -d "$d" ] || { echo "::error::$d is missing - the path contract has nothing to scan"; exit 1; }
done
status=0
rc=0; LC_ALL=C grep -rnIP '~/\.claude/skills/[A-Za-z0-9_-]+\b' .claude/skills cli/assets/skills || rc=$?
if [ "$rc" -ne 1 ]; then
echo "::error::home-rooted skill paths (~/.claude/skills/<skill>) only resolve for one install layout - use skill-relative paths (see #474); grep rc=$rc"
status=1
fi
rc=0; LC_ALL=C grep -rnIP '(?<![/\w])\.claude/skills/[A-Za-z0-9_-]+\b|(?<!~)(?<!\$CLAUDE_PLUGIN_ROOT)(?<!\$\{CLAUDE_PLUGIN_ROOT\})/\.claude/skills/[A-Za-z0-9_-]+\b|\$\{?CLAUDE_PLUGIN_ROOT\}?/\.claude/skills/(?!ui-ux-pro-max\b)[A-Za-z0-9_-]+\b' .claude/skills cli/assets/skills || rc=$?
if [ "$rc" -ne 1 ]; then
echo "::error::project- or variable-rooted skill paths (.claude/skills/<skill>, \$HOME/..., \${CLAUDE_PLUGIN_ROOT}/... outside the core skill) only resolve for one install layout - use skill-relative paths (see #474); grep rc=$rc"
status=1
fi
rc=0; found=$(LC_ALL=C grep -rlIF 'CLAUDE_PLUGIN_ROOT' .claude/skills cli/assets/skills) || rc=$?
if [ "$rc" -eq 2 ]; then echo "::error::grep failed while scanning for CLAUDE_PLUGIN_ROOT (rc=2)"; status=1; fi
offenders=$(printf '%s\n' "$found" | grep -vxF -e '.claude/skills/ui-ux-pro-max/SKILL.md' -e '.claude/skills/ui-ux-pro-max/scripts/tests/test_skill_script_paths.py' | grep -v '^$' || true)
if [ -n "$offenders" ]; then
printf '%s\n' "$offenders"
echo "::error::CLAUDE_PLUGIN_ROOT is only defined under a plugin install; only the plugin-only core SKILL.md may use it - sub-skills ship through the CLI too (see #474)"
status=1
fi
if [ "$status" -eq 0 ]; then echo "OK: all skill paths are skill-relative"; fi
exit "$status"