name: Skills Registry CI on: push: branches: ["main"] pull_request: branches: ["main"] workflow_dispatch: permissions: contents: read jobs: pr-policy: if: github.event_name == 'pull_request' runs-on: ubuntu-latest outputs: primary_category: ${{ steps.intake.outputs.primary_category }} categories: ${{ steps.intake.outputs.categories }} requires_references: ${{ steps.intake.outputs.requires_references }} direct_derived_changes_count: ${{ steps.intake.outputs.direct_derived_changes_count }} has_quality_checklist: ${{ steps.intake.outputs.has_quality_checklist }} has_issue_link: ${{ steps.intake.outputs.has_issue_link }} steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up Node uses: actions/setup-node@v5 with: node-version: "lts/*" - name: Fetch base branch run: git fetch origin "${{ github.base_ref }}" - name: Intake PR change id: intake run: | node tools/scripts/pr_preflight.cjs \ --base "origin/${{ github.base_ref }}" \ --head "HEAD" \ --event-path "$GITHUB_EVENT_PATH" \ --no-run \ --write-github-output \ --write-step-summary - name: Enforce PR source-only contract run: | if [ "${{ steps.intake.outputs.direct_derived_changes_count }}" != "0" ]; then echo "Pull requests must stay source-only." echo "Remove derived files and let main regenerate them after merge." exit 1 fi if [ "${{ steps.intake.outputs.has_quality_checklist }}" != "true" ]; then echo "PR body must include the Quality Bar Checklist from the template." exit 1 fi if [ "${{ steps.intake.outputs.has_issue_link }}" != "true" ]; then echo "::notice::No Closes/Fixes issue link detected in the PR body." fi source-validation: if: github.event_name == 'pull_request' runs-on: ubuntu-latest needs: pr-policy steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install Python dependencies run: pip install -r tools/requirements.txt - name: Set up Node uses: actions/setup-node@v5 with: node-version: "lts/*" - name: Fetch base branch run: git fetch origin "${{ github.base_ref }}" - name: Install npm dependencies run: npm ci - name: Verify directory structure run: | test -d skills/ test -d apps/web-app/ test -d tools/scripts/ test -d tools/lib/ test -f README.md test -f CONTRIBUTING.md - name: Validate source changes run: npm run validate - name: Enforce validation warning budget run: npm run check:warning-budget - name: Verify README source credits for changed skills run: npm run check:readme-credits -- --base "origin/${{ github.base_ref }}" --head HEAD - name: Validate references if: needs.pr-policy.outputs.requires_references == 'true' run: npm run validate:references - name: Audit npm dependencies run: npm audit --audit-level=high - name: Run tests env: ENABLE_NETWORK_TESTS: "1" run: npm run test - name: Install web-app dependencies run: npm run app:install - name: Run web app coverage run: npm run app:test:coverage - name: Run docs security checks run: npm run security:docs artifact-preview: if: github.event_name == 'pull_request' runs-on: ubuntu-latest needs: [pr-policy, source-validation] steps: - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install Python dependencies run: pip install -r tools/requirements.txt - name: Set up Node uses: actions/setup-node@v5 with: node-version: "lts/*" - name: Install npm dependencies run: npm ci - name: Generate canonical artifacts preview run: | npm run chain npm run catalog - name: Report generated drift run: | mapfile -t managed_files < <(node tools/scripts/generated_files.js --include-mixed) if [ "${#managed_files[@]}" -eq 0 ]; then echo "::error::No managed files resolved from generated_files contract." exit 1 fi drift_files=$(git diff --name-only -- "${managed_files[@]}") { echo "## Artifact Preview" echo echo "- Primary change: \`${{ needs.pr-policy.outputs.primary_category }}\`" echo "- Categories: \`${{ needs.pr-policy.outputs.categories }}\`" echo "- Derived-file policy: PRs remain source-only; main will canonicalize final generated outputs." echo } >> "$GITHUB_STEP_SUMMARY" if [ -z "$drift_files" ]; then echo "No generated drift detected after preview." echo "- Generated drift: none" >> "$GITHUB_STEP_SUMMARY" exit 0 fi echo "::notice::Generated drift detected in artifact preview." { echo "- Generated drift: detected" echo echo "Predicted file updates:" printf '%s\n' "$drift_files" | sed "s/^/- \`/; s/\$/\`/" } >> "$GITHUB_STEP_SUMMARY" main-validation-and-sync: if: github.event_name != 'pull_request' runs-on: ubuntu-latest concurrency: group: canonical-main-sync cancel-in-progress: false permissions: contents: write env: GH_TOKEN: ${{ github.token }} steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install Python dependencies run: pip install -r tools/requirements.txt - name: Set up Node uses: actions/setup-node@v5 with: node-version: "lts/*" - name: Install npm dependencies run: npm ci - name: Verify directory structure run: | test -d skills/ test -d apps/web-app/ test -d tools/scripts/ test -d tools/lib/ test -f README.md test -f CONTRIBUTING.md - name: Validate references run: npm run validate:references - name: Run repo-state sync run: npm run sync:repo-state - name: Audit npm dependencies run: npm audit --audit-level=high - name: Run tests env: ENABLE_NETWORK_TESTS: "1" run: npm run test - name: Install web-app dependencies run: npm run app:install - name: Run web app coverage run: npm run app:test:coverage - name: Run docs security checks run: npm run security:docs - name: Set up GitHub credentials if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | set -euo pipefail git config user.name 'github-actions[bot]' git config user.email 'github-actions[bot]@users.noreply.github.com' git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git git fetch origin main - name: Auto-commit canonical artifacts if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | set -euo pipefail mapfile -t managed_files < <(node tools/scripts/generated_files.js --include-mixed) if [ "${#managed_files[@]}" -eq 0 ]; then echo "No managed files resolved from generated_files contract." exit 1 fi if git diff --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then echo "No canonical repo-state drift detected." exit 0 fi git add -- "${managed_files[@]}" || true if git diff --cached --quiet; then echo "Repo-state sync produced unmanaged drift only." git status --short exit 1 fi if [ -n "$(git diff --name-only)" ] || [ -n "$(git ls-files --others --exclude-standard)" ]; then echo "Repo-state sync produced unmanaged drift alongside canonical changes." git status --short exit 1 fi git commit -m "chore: sync repo state [ci skip]" git pull origin main --rebase git push origin HEAD - name: Check for uncommitted drift if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | if ! git diff --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]; then echo "❌ Detected leftover drift after the canonical bot sync." echo echo "The bot may only commit managed canonical files and must leave a clean tree." echo "To fix locally, run the canonical maintainer flow:" echo " npm run release:preflight" echo " npm run sync:repo-state" echo " git status" git status --short exit 1 fi