fix: make test suite pass on Windows (path separators + URL.pathname) #8474
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright (c) Meta Platforms, Inc. and affiliates. | |
| # PR CI: test, build, analyze, a11y audit, and comment | |
| # Everything runs in parallel where possible | |
| # | |
| # Merge queue: test + build also run on merge_group events so the | |
| # merge queue can gate on them before landing a PR. | |
| name: CI | |
| on: | |
| pull_request: | |
| branches: ["main"] | |
| merge_group: | |
| permissions: {} | |
| concurrency: | |
| group: "ci-${{ github.event.merge_group.head_sha || github.head_ref }}" | |
| cancel-in-progress: true | |
| jobs: | |
| # Detect docsite-only changes — lets docsite PRs skip heavy jobs | |
| check-scope: | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| docsite_only: ${{ steps.scope.outputs.docsite_only }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 50 | |
| - name: Fetch base branch | |
| run: git fetch origin ${{ github.base_ref }} --depth=50 | |
| - name: Determine change scope | |
| id: scope | |
| run: | | |
| MERGE_BASE=$(git merge-base HEAD origin/${{ github.base_ref }} 2>/dev/null || echo "") | |
| if [ -z "$MERGE_BASE" ]; then | |
| echo "docsite_only=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| CHANGED=$(git diff --name-only "$MERGE_BASE"...HEAD) | |
| NON_DOCSITE=$(echo "$CHANGED" | grep -v '^apps/docsite/' | head -1) | |
| if [ -z "$NON_DOCSITE" ]; then | |
| echo "docsite_only=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "docsite_only=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Docsite data extraction tests — always runs, fast (~2s) | |
| docsite-test: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| # docsite `generate` runs `astryx theme build`, which imports the compiled | |
| # @astryxdesign/core/theme entry (dist/theme/index.js). Build core first so that | |
| # entry exists — otherwise generate fails with "Cannot find module". | |
| - name: Build core package | |
| run: pnpm -F @astryxdesign/core build | |
| - name: Generate and test docsite data | |
| run: pnpm -F @astryxdesign/docsite generate && pnpm -F @astryxdesign/docsite test | |
| # Lightweight check — does the PR touch any core components? | |
| # Gates pr-a11y to avoid unnecessary work | |
| check-components: | |
| needs: [check-scope] | |
| if: github.event_name == 'pull_request' && needs.check-scope.outputs.docsite_only != 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| has_components: ${{ steps.check.outputs.has_components }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Fetch base branch | |
| run: git fetch origin ${{ github.base_ref }} --depth=1 | |
| - name: Check for component changes | |
| id: check | |
| run: | | |
| CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- packages/core/src/ | grep -v -E '(hooks|theme|utils)/' | head -1) | |
| echo "has_components=$( [ -n "$CHANGED" ] && echo true || echo false )" >> $GITHUB_OUTPUT | |
| # Run tests (parallel with build) | |
| test: | |
| needs: [check-scope] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Skip for docsite-only changes | |
| if: needs.check-scope.outputs.docsite_only == 'true' | |
| run: echo "Docsite-only PR — skipping full test suite" | |
| - uses: actions/checkout@v7 | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| - uses: pnpm/action-setup@v6 | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| - uses: actions/setup-node@v6 | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| with: | |
| node-version: 24 | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| - name: Check copyright headers | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| run: ./scripts/add-copyright.sh --check | |
| - name: Check package.json exports are in sync | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| run: node scripts/sync-exports.js --check | |
| - name: Check token docs are in sync | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| run: node scripts/generate-token-docs.mjs --check | |
| - run: pnpm test | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| # Build storybook + analyze components (parallel with test) | |
| # Uploads storybook preview and analysis artifacts for downstream jobs | |
| build: | |
| needs: [check-scope] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| storybook_url: ${{ steps.urls.outputs.storybook_url }} | |
| short_hash: ${{ steps.urls.outputs.short_hash }} | |
| sandbox_url: ${{ steps.urls.outputs.sandbox_url }} | |
| pr_number: ${{ steps.urls.outputs.pr_number }} | |
| steps: | |
| - name: Skip for docsite-only changes | |
| if: needs.check-scope.outputs.docsite_only == 'true' | |
| run: echo "Docsite-only PR — skipping full build" | |
| - name: Checkout | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| uses: actions/checkout@v7 | |
| - name: Fetch base branch for PR analysis | |
| if: needs.check-scope.outputs.docsite_only != 'true' && github.event_name == 'pull_request' | |
| run: git fetch origin ${{ github.base_ref }} --depth=1 | |
| - name: Setup pnpm | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| uses: pnpm/action-setup@v6 | |
| - name: Setup Node.js | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: 'pnpm' | |
| - name: Install dependencies | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| run: pnpm install --frozen-lockfile | |
| - name: Build core package | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| run: pnpm build | |
| - name: Verify package exports | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| run: node scripts/verify-exports.mjs | |
| - name: Typecheck component docs | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| run: pnpm -F @astryxdesign/core typecheck:docs | |
| - name: Typecheck template docs | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| run: pnpm -F @astryxdesign/cli typecheck:template-docs | |
| - name: Typecheck storybook | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| run: pnpm -F @astryxdesign/storybook typecheck | |
| - name: Typecheck core (including tests) | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| run: pnpm -F @astryxdesign/core typecheck | |
| - name: Cache Next.js build | |
| if: needs.check-scope.outputs.docsite_only != 'true' && github.event_name == 'pull_request' | |
| uses: actions/cache@v6 | |
| with: | |
| path: apps/sandbox/.next/cache | |
| # No restore-keys fallback — see deploy.yml for the full rationale. | |
| # The Next.js module cache bakes in @astryxdesign/core's resolved export graph, | |
| # so a loose fallback can restore a cache built against a different | |
| # export shape and silently produce wrong builds (broke #2941's | |
| # post-merge deploy). A changed key yields a cold (safe) rebuild. | |
| key: nextjs-sandbox-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('packages/core/dist/**') }} | |
| - name: Build Storybook | |
| if: needs.check-scope.outputs.docsite_only != 'true' | |
| run: pnpm -F @astryxdesign/storybook build | |
| - name: Compute PR preview path | |
| if: needs.check-scope.outputs.docsite_only != 'true' && github.event_name == 'pull_request' | |
| id: urls | |
| run: | | |
| PR_NUMBER="${{ github.event.pull_request.number }}" | |
| COMMIT_HASH="${{ github.event.pull_request.head.sha }}" | |
| SHORT_HASH="${COMMIT_HASH:0:7}" | |
| REPO_NAME="${{ github.event.repository.name }}" | |
| REPO_OWNER="${{ github.repository_owner }}" | |
| echo "short_hash=${SHORT_HASH}" >> $GITHUB_OUTPUT | |
| echo "pr_number=${PR_NUMBER}" >> $GITHUB_OUTPUT | |
| echo "sandbox_base_path=/${REPO_NAME}/pr/${PR_NUMBER}/sandbox" >> $GITHUB_OUTPUT | |
| echo "storybook_url=https://${REPO_OWNER}.github.io/${REPO_NAME}/pr/${PR_NUMBER}/" >> $GITHUB_OUTPUT | |
| echo "sandbox_url=https://${REPO_OWNER}.github.io/${REPO_NAME}/pr/${PR_NUMBER}/sandbox/" >> $GITHUB_OUTPUT | |
| - name: Build Sandbox | |
| if: needs.check-scope.outputs.docsite_only != 'true' && github.event_name == 'pull_request' | |
| run: pnpm -F @astryxdesign/sandbox build | |
| env: | |
| # Must match the deployed Pages path (<owner>.github.io/<repo>/pr/<n>/sandbox). | |
| # Built without the /<repo> prefix, the sandbox's root-absolute asset URLs | |
| # drop that segment and 404 — breaking all styles/JS on the preview. | |
| SANDBOX_BASE_PATH: ${{ steps.urls.outputs.sandbox_base_path }} | |
| NODE_OPTIONS: '--max-old-space-size=8192' | |
| - name: Upload Storybook artifact | |
| if: needs.check-scope.outputs.docsite_only != 'true' && github.event_name == 'pull_request' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: storybook-${{ steps.urls.outputs.short_hash }} | |
| path: apps/storybook/dist/ | |
| retention-days: 30 | |
| - name: Upload Sandbox artifact | |
| if: needs.check-scope.outputs.docsite_only != 'true' && github.event_name == 'pull_request' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: sandbox-${{ steps.urls.outputs.short_hash }} | |
| path: apps/sandbox/out/ | |
| retention-days: 30 | |
| - name: Analyze components | |
| if: needs.check-scope.outputs.docsite_only != 'true' && github.event_name == 'pull_request' | |
| run: | | |
| node .github/scripts/analyze-pr.js \ | |
| --base origin/${{ github.base_ref }} \ | |
| --head HEAD \ | |
| --output analysis.json | |
| - name: Upload analysis artifact | |
| if: needs.check-scope.outputs.docsite_only != 'true' && github.event_name == 'pull_request' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: pr-analysis | |
| path: analysis.json | |
| retention-days: 1 | |
| # Deploy PR preview to GitHub Pages. | |
| # Uses a per-PR concurrency group so multiple PRs deploy in parallel. | |
| # Instead of peaceiris/actions-gh-pages (which can conflict with main's | |
| # force-push), we do a manual git push with retry-on-conflict. | |
| # Fork PRs run with a read-only GITHUB_TOKEN, so the `git push origin gh-pages` | |
| # below would 403. Skip on forks (neutral, not failing). A maintainer can deploy | |
| # a trusted fork PR's preview manually via the Re-deploy Preview workflow. | |
| deploy-preview: | |
| if: >- | |
| github.event_name == 'pull_request' && | |
| needs.check-scope.outputs.docsite_only != 'true' && | |
| github.event.pull_request.head.repo.full_name == github.repository | |
| needs: [build, check-scope] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: "pr-preview-${{ needs.build.outputs.pr_number }}" | |
| cancel-in-progress: true | |
| steps: | |
| - name: Download Storybook artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: storybook-${{ needs.build.outputs.short_hash }} | |
| path: storybook-dist | |
| - name: Download Sandbox artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: sandbox-${{ needs.build.outputs.short_hash }} | |
| path: sandbox-dist | |
| - name: Deploy PR preview to GitHub Pages | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR_NUMBER: ${{ needs.build.outputs.pr_number }} | |
| run: | | |
| REPO_URL="https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" | |
| MAX_ATTEMPTS=5 | |
| WORK_DIR="$PWD" | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
| for attempt in $(seq 1 $MAX_ATTEMPTS); do | |
| echo "::group::Attempt $attempt of $MAX_ATTEMPTS" | |
| cd "$WORK_DIR" | |
| rm -rf /tmp/gh-pages | |
| git clone --depth=1 --single-branch --branch gh-pages "$REPO_URL" /tmp/gh-pages | |
| rm -rf /tmp/gh-pages/pr/${PR_NUMBER} | |
| mkdir -p /tmp/gh-pages/pr/${PR_NUMBER} | |
| cp -r storybook-dist/* /tmp/gh-pages/pr/${PR_NUMBER}/ | |
| mkdir -p /tmp/gh-pages/pr/${PR_NUMBER}/sandbox | |
| cp -r sandbox-dist/* /tmp/gh-pages/pr/${PR_NUMBER}/sandbox/ | |
| cd /tmp/gh-pages | |
| git add -A | |
| git commit -m "Deploy PR #${PR_NUMBER} preview" || { echo "Nothing to commit"; echo "::endgroup::"; exit 0; } | |
| if git push origin gh-pages; then | |
| echo "Deployed successfully on attempt $attempt" | |
| echo "::endgroup::" | |
| exit 0 | |
| fi | |
| echo "Push failed (likely concurrent update), retrying in $((attempt * 2))s..." | |
| echo "::endgroup::" | |
| sleep $((attempt * 2)) | |
| done | |
| echo "::error::Failed to deploy PR preview after $MAX_ATTEMPTS attempts" | |
| exit 1 | |
| # Post PR comment with preview links + analysis as soon as build finishes | |
| # Does NOT wait for test or a11y — those update the comment later | |
| # Posting a comment needs `pull-requests: write`, which fork PRs don't get | |
| # (their GITHUB_TOKEN is read-only). Skip on forks so the check stays neutral | |
| # instead of failing. The analysis artifact is still uploaded by `build`. | |
| pr-comment: | |
| if: >- | |
| github.event_name == 'pull_request' && | |
| needs.check-scope.outputs.docsite_only != 'true' && | |
| github.event.pull_request.head.repo.full_name == github.repository | |
| needs: [build, check-scope] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| - name: Download analysis artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: pr-analysis | |
| - name: Generate PR comment | |
| run: | | |
| RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| echo '{"components":{},"summary":{"componentsAudited":0,"totalViolations":0}}' > a11y-report.json | |
| COMMENT_BODY=$(node .github/scripts/generate-pr-comment.js \ | |
| --analysis analysis.json \ | |
| --a11y a11y-report.json \ | |
| --storybook-url "${{ needs.build.outputs.storybook_url }}" \ | |
| --sandbox-url "${{ needs.build.outputs.sandbox_url }}" \ | |
| --run-url "$RUN_URL" \ | |
| --pr-number "${{ github.event.pull_request.number }}" \ | |
| --pending) | |
| echo "$COMMENT_BODY" > pr-comment.md | |
| - name: Comment on PR | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const body = fs.readFileSync('pr-comment.md', 'utf8'); | |
| const prNumber = ${{ github.event.pull_request.number }}; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| }); | |
| const botComment = comments.find(comment => | |
| comment.user.type === 'Bot' && | |
| comment.body.includes('PR Analysis Report') | |
| ); | |
| if (botComment) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body: body | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: body | |
| }); | |
| } | |
| # Accessibility audit | |
| # Skipped when no components changed. | |
| pr-a11y: | |
| needs: [build, check-components] | |
| if: github.event_name == 'pull_request' && needs.check-components.outputs.has_components == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: 'pnpm' | |
| - name: Download analysis artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: pr-analysis | |
| - name: Download Storybook artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: storybook-${{ needs.build.outputs.short_hash }} | |
| path: apps/storybook/dist | |
| - name: Install Playwright | |
| run: pnpm install --frozen-lockfile && npx playwright install chromium | |
| - name: Run accessibility audit | |
| run: | | |
| COMPONENTS=$(cat analysis.json | jq -r '(.newComponents + .modifiedComponents) | join(",")') | |
| node .github/scripts/accessibility-audit.js \ | |
| --storybook-dir apps/storybook/dist \ | |
| --output a11y-report.json \ | |
| --components "$COMPONENTS" | |
| - name: Upload a11y artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: a11y-report | |
| path: a11y-report.json | |
| retention-days: 1 | |
| # Update PR comment with a11y results once they're ready. | |
| # Skipped on fork PRs for the same read-only-token reason as pr-comment. | |
| pr-comment-update: | |
| needs: [build, check-components, pr-a11y] | |
| if: >- | |
| always() && github.event_name == 'pull_request' && | |
| needs.build.result == 'success' && | |
| needs.check-components.outputs.has_components == 'true' && | |
| github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| - name: Download analysis artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: pr-analysis | |
| - name: Download a11y artifact | |
| if: needs.pr-a11y.result == 'success' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: a11y-report | |
| - name: Generate updated PR comment | |
| run: | | |
| RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| if [ ! -f a11y-report.json ]; then | |
| echo '{"components":{},"summary":{"componentsAudited":0,"totalViolations":0}}' > a11y-report.json | |
| fi | |
| COMMENT_BODY=$(node .github/scripts/generate-pr-comment.js \ | |
| --analysis analysis.json \ | |
| --a11y a11y-report.json \ | |
| --storybook-url "${{ needs.build.outputs.storybook_url }}" \ | |
| --sandbox-url "${{ needs.build.outputs.sandbox_url }}" \ | |
| --run-url "$RUN_URL" \ | |
| --pr-number "${{ github.event.pull_request.number }}") | |
| echo "$COMMENT_BODY" > pr-comment.md | |
| - name: Update PR comment | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const body = fs.readFileSync('pr-comment.md', 'utf8'); | |
| const prNumber = ${{ github.event.pull_request.number }}; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| }); | |
| const botComment = comments.find(comment => | |
| comment.user.type === 'Bot' && | |
| comment.body.includes('PR Analysis Report') | |
| ); | |
| if (botComment) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body: body | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: body | |
| }); | |
| } |