diff --git a/.github/actions/setup-pnpm-node/action.yml b/.github/actions/setup-pnpm-node/action.yml new file mode 100644 index 00000000000..42988cb1fd6 --- /dev/null +++ b/.github/actions/setup-pnpm-node/action.yml @@ -0,0 +1,18 @@ +name: Setup pnpm + Node +description: Install pnpm, set up Node.js (pinned via .nvmrc) with pnpm cache, and install dependencies (frozen lockfile) + +runs: + using: composite + steps: + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: pnpm + + - name: Install dependencies + shell: bash + run: pnpm install --frozen-lockfile diff --git a/.github/actions/wait-for-server/action.yml b/.github/actions/wait-for-server/action.yml new file mode 100644 index 00000000000..febff39d741 --- /dev/null +++ b/.github/actions/wait-for-server/action.yml @@ -0,0 +1,29 @@ +name: Wait for HTTP server +description: Poll a URL until it responds with a successful status, or fail after a timeout. + +inputs: + url: + description: URL to poll + required: true + timeout-seconds: + description: Maximum seconds to wait + required: false + default: "90" + +runs: + using: composite + steps: + - shell: bash + env: + URL: ${{ inputs.url }} + TIMEOUT: ${{ inputs.timeout-seconds }} + run: | + for _ in $(seq 1 "$TIMEOUT"); do + if curl -sfo /dev/null "$URL"; then + echo "Server is up at $URL" + exit 0 + fi + sleep 1 + done + echo "Server at $URL did not respond within ${TIMEOUT}s" + exit 1 diff --git a/.github/workflows/chromatic-pages.yml b/.github/workflows/chromatic-pages.yml deleted file mode 100644 index 5a9bedf6b91..00000000000 --- a/.github/workflows/chromatic-pages.yml +++ /dev/null @@ -1,90 +0,0 @@ -# Playwright + Chromatic full-page visual regression tests -# Separate from chromatic.yml (Storybook component snapshots) -name: "Chromatic: Page Visual Tests" - -on: - pull_request: - branches: [master, staging, "test/**"] - types: [opened, synchronize, ready_for_review] - workflow_dispatch: - -jobs: - playwright-visual: - name: Build & Capture Snapshots - runs-on: ubuntu-latest - container: - image: mcr.microsoft.com/playwright:v1.53.1-noble - steps: - - name: Checkout repo - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 20 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build Next.js with mock data (English only) - run: pnpm build - env: - USE_MOCK_DATA: "true" - IS_VISUAL_TEST: "true" - NEXT_PUBLIC_BUILD_LOCALES: "en" - - - name: Run visual tests - run: pnpm test:visual - env: - HOME: /root - USE_MOCK_DATA: "true" - IS_VISUAL_TEST: "true" - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: chromatic-archives - path: test-results/ - retention-days: 1 - - chromatic-upload: - name: Upload to Chromatic - needs: playwright-visual - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 20 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Download test results - uses: actions/download-artifact@v4 - with: - name: chromatic-archives - path: test-results/ - - - name: Publish to Chromatic - uses: chromaui/action@v16 - with: - projectToken: ${{ secrets.CHROMATIC_PAGES_TOKEN }} - playwright: true - exitZeroOnChanges: true diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml deleted file mode 100644 index 0fa6e38d083..00000000000 --- a/.github/workflows/chromatic.yml +++ /dev/null @@ -1,48 +0,0 @@ -# .github/workflows/chromatic.yml - -# Workflow name -name: Chromatic Publish and Testing - -# Event for the workflow -on: - pull_request: - branches: [master, staging, "test/**"] - types: [opened, synchronize, ready_for_review] - -# List of jobs -jobs: - chromatic-deployment: - # Operating System - runs-on: ubuntu-latest - # Job steps - steps: - - name: Checkout repo - uses: actions/checkout@v6 - with: - fetch-depth: 0 - # Tells the checkout which commit hash to reference - ref: ${{ github.event.pull_request.head.ref }} - env: - CHROMATIC_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }} - CHROMATIC_SHA: ${{ github.event.pull_request.head.sha || github.ref }} - CHROMATIC_SLUG: ${{ github.repository }} - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 20 - cache: "pnpm" - - - name: Install deps - run: pnpm install - - - name: Publish to Chromatic - uses: chromaui/action@v1 - with: - projectToken: fee8e66c9916 - # 👇 Only fail if Storybook contains stories that error - exitZeroOnChanges: true - onlyChanged: true # enables TurboSnap - zip: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..28664a3321f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,321 @@ +name: CI + +on: + pull_request: + branches: [dev, master, staging, "test/**"] + types: [opened, synchronize, ready_for_review] + workflow_dispatch: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Shared base URL for the production server we boot for e2e + lighthouse. + LOCAL_BASE_URL: http://localhost:3000 + +# Extended jobs (build / e2e / lighthouse / visual / page-visual) only run for +# PRs into master, staging, or test/** branches. Jobs downstream of `build` +# inherit gating automatically via `needs:` (skipped jobs propagate). +# +# `fetch-depth: 0` is set selectively: lint needs it for `git diff`, and the +# two Chromatic jobs need it for baseline detection. Other jobs use the default +# shallow clone for speed. +jobs: + lint: + name: Lint, type-check & markdown + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: ./.github/actions/setup-pnpm-node + + - name: ESLint + run: pnpm lint + + - name: TypeScript + run: pnpm type-check + + - name: Find changed English markdown + id: changed + run: | + FILES=$(git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }}...HEAD \ + | grep '^public/content/.*\.md$' \ + | grep -v '^public/content/translations/' \ + || true) + { + echo "files<> "$GITHUB_OUTPUT" + + - name: Lint changed markdown + if: steps.changed.outputs.files != '' + uses: DavidAnson/markdownlint-cli2-action@v22 + with: + globs: ${{ steps.changed.outputs.files }} + + unit-tests: + name: Unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-pnpm-node + - name: Run unit tests + run: pnpm test:unit + + visual-tests: + name: Visual regression (Chromatic) + if: | + github.base_ref == 'master' + || github.base_ref == 'staging' + || startsWith(github.base_ref, 'test/') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: ./.github/actions/setup-pnpm-node + + - name: Publish to Chromatic + uses: chromaui/action@v16 + with: + projectToken: fee8e66c9916 + exitZeroOnChanges: true + onlyChanged: true + zip: true + + build: + name: Build website (mock data) + if: | + github.base_ref == 'master' + || github.base_ref == 'staging' + || startsWith(github.base_ref, 'test/') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-pnpm-node + + - name: Build (mock data, deterministic, e2e locale subset) + run: pnpm build + env: + USE_MOCK_DATA: "true" + IS_VISUAL_TEST: "true" + # E2E specs reference en/es/zh/ar (404 i18n + language picker + RTL). + # Page-visual + lighthouse only need en. Build all four once. + NEXT_PUBLIC_BUILD_LOCALES: "en,es,zh,ar" + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: next-build + path: | + .next + !.next/cache + retention-days: 1 + include-hidden-files: true + + e2e-tests: + name: E2E tests + if: | + github.base_ref == 'master' + || github.base_ref == 'staging' + || startsWith(github.base_ref, 'test/') + runs-on: ubuntu-latest + env: + DEPLOY_PREVIEW_URL: https://deploy-preview-${{ github.event.pull_request.number }}--ethereumorg.netlify.app + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/setup-pnpm-node + + - name: Install Playwright with browser deps + run: npx playwright install --with-deps + + - name: Wait for Netlify deploy preview to be ready + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SHA: ${{ github.event.pull_request.head.sha }} + REPO: ${{ github.repository }} + CHECK_NAME: netlify/ethereumorg/deploy-preview + run: | + for _ in $(seq 1 60); do + STATE=$(curl -sf -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/commits/$SHA/statuses" \ + | python3 -c "import json,sys,os; d=json.load(sys.stdin); m=next((s for s in d if s['context']==os.environ['CHECK_NAME']), None); print(m['state'] if m else '')") + case "$STATE" in + "success") + echo "Deploy preview ready for $SHA" + exit 0 + ;; + "failure"|"error") + echo "Deploy preview failed: $STATE" + exit 1 + ;; + *) + echo "Deploy preview state='${STATE:-not yet posted}' — waiting 30s" + sleep 30 + ;; + esac + done + echo "Timed out waiting for deploy preview status" + exit 1 + + - name: Warm up Netlify edge cache + run: | + for path in / /wallets/find-wallet/ /staking/ /whitepaper/ /nft/ /developers/ /start/; do + curl -s -o /dev/null "${DEPLOY_PREVIEW_URL}${path}" & + done + wait + + - name: Run E2E tests + run: pnpm test:e2e + env: + PLAYWRIGHT_TEST_BASE_URL: ${{ env.DEPLOY_PREVIEW_URL }} + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: ./tests/__results__ + retention-days: 7 + + lighthouse: + name: Lighthouse audit + needs: build + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-pnpm-node + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: next-build + path: .next + + - name: Start production server + run: pnpm start & + + - uses: ./.github/actions/wait-for-server + with: + url: ${{ env.LOCAL_BASE_URL }} + + - name: Audit URLs using Lighthouse + id: lighthouse_audit + uses: treosh/lighthouse-ci-action@v11 + with: + urls: | + ${{ env.LOCAL_BASE_URL }}/en/ + ${{ env.LOCAL_BASE_URL }}/en/wallets/find-wallet/ + ${{ env.LOCAL_BASE_URL }}/en/staking/ + ${{ env.LOCAL_BASE_URL }}/en/whitepaper/ + ${{ env.LOCAL_BASE_URL }}/en/nft/ + ${{ env.LOCAL_BASE_URL }}/en/developers/docs/intro-to-ethereum/ + ${{ env.LOCAL_BASE_URL }}/en/developers/tutorials/creating-a-wagmi-ui-for-your-contract/ + runs: 3 + uploadArtifacts: true + temporaryPublicStorage: true + + - name: Format Lighthouse score + id: format_lighthouse_score + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const manifests = ${{ steps.lighthouse_audit.outputs.manifest }}; + const links = ${{ steps.lighthouse_audit.outputs.links }}; + const formatResult = (res) => Math.round((res * 100)); + + console.log('Total manifests:', manifests.length); + console.log('Manifests:', JSON.stringify(manifests, null, 2)); + console.log('Links:', JSON.stringify(links, null, 2)); + + let comment = [ + '| Page | Performance | Accessibility | Best practices | SEO | PWA |', + '| --- | --- | --- | --- | --- | --- |', + ]; + + Object.entries(links).forEach(([pageUrl, reportUrl]) => { + const relevantManifests = manifests.filter(manifest => manifest.url === pageUrl); + const results = relevantManifests.map(manifest => manifest.summary); + const averagedResults = {}; + + if (results.length > 0) { + Object.keys(results[0]).forEach(key => { + averagedResults[key] = formatResult( + results.reduce((acc, cur) => acc + cur[key], 0) / results.length + ); + }); + + const score = res => res >= 90 ? '🟢' : res >= 50 ? '🟠' : '🔴'; + const urlForTable = pageUrl.includes('/en/') ? pageUrl.substring(pageUrl.indexOf('/en/')) : pageUrl; + + comment.push( + `| [${urlForTable}](${reportUrl}) | ${score(averagedResults.performance)} ${averagedResults.performance} | ${score(averagedResults.accessibility)} ${averagedResults.accessibility} | ${score(averagedResults['best-practices'])} ${averagedResults['best-practices']} | ${score(averagedResults.seo)} ${averagedResults.seo} | ${score(averagedResults.pwa)} ${averagedResults.pwa} |` + ); + } else { + console.error('No results found for URL:', pageUrl); + } + }); + + comment.push( + ' ', + '*Lighthouse scores are calculated based on the latest audit results*' + ); + + comment = comment.join('\n'); + core.setOutput("comment", comment); + + - name: Add Lighthouse stats as comment + uses: marocchino/sticky-pull-request-comment@v2 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + number: ${{ github.event.pull_request.number }} + header: lighthouse + message: ${{ steps.format_lighthouse_score.outputs.comment }} + + page-visual-tests: + name: Page visual snapshots (Playwright + Chromatic) + needs: build + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.53.1-noble + env: + # Playwright image preinstalls browsers under /root; GitHub Actions + # otherwise overrides HOME to /github/home and Playwright re-downloads. + HOME: /root + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Mark workspace as safe for git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - uses: ./.github/actions/setup-pnpm-node + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: next-build + path: .next + + - name: Run visual tests + run: pnpm test:visual + + - name: Publish to Chromatic + if: always() + uses: chromaui/action@v16 + with: + projectToken: ${{ secrets.CHROMATIC_PAGES_TOKEN }} + playwright: true + exitZeroOnChanges: true + zip: true diff --git a/.github/workflows/lighthouse-ci.yml b/.github/workflows/lighthouse-ci.yml deleted file mode 100644 index ae87c9716b1..00000000000 --- a/.github/workflows/lighthouse-ci.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Lighthouse CI - -on: - push: - branches: - - master - - staging - - performance/** - -jobs: - lighthouse: - runs-on: ubuntu-latest - - permissions: - pull-requests: write - - steps: - - uses: actions/checkout@v6 - - name: Sleep for 60 minutes - run: sleep 3600 - - name: Wait for Netlify Deploy - id: netlify_deploy - uses: probablyup/wait-for-netlify-action@3.2.0 - with: - site_id: "e8f2e766-888b-4954-8500-1b647d84db99" - max_timeout: 900 - env: - NETLIFY_TOKEN: ${{ secrets.NETLIFY_TOKEN }} - - name: Audit URLs using Lighthouse - id: lighthouse_audit - uses: treosh/lighthouse-ci-action@v11 - with: - urls: | - ${{ steps.netlify_deploy.outputs.url }}/en/ - ${{ steps.netlify_deploy.outputs.url }}/en/wallets/find-wallet/ - ${{ steps.netlify_deploy.outputs.url }}/en/staking/ - ${{ steps.netlify_deploy.outputs.url }}/en/whitepaper/ - ${{ steps.netlify_deploy.outputs.url }}/en/nft/ - ${{ steps.netlify_deploy.outputs.url }}/en/developers/docs/intro-to-ethereum/ - ${{ steps.netlify_deploy.outputs.url }}/en/developers/tutorials/creating-a-wagmi-ui-for-your-contract/ - runs: 3 # run three times - uploadArtifacts: true # save results as an action artifacts - temporaryPublicStorage: true # upload lighthouse report to the temporary storage - - name: Format lighthouse score - id: format_lighthouse_score - uses: actions/github-script@v3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const manifests = ${{ steps.lighthouse_audit.outputs.manifest }}; - const links = ${{ steps.lighthouse_audit.outputs.links }}; - const formatResult = (res) => Math.round((res * 100)); - - console.log('Total manifests:', manifests.length); - console.log('Manifests:', JSON.stringify(manifests, null, 2)); - console.log('Links:', JSON.stringify(links, null, 2)); - - let comment = [ - '| Page | Performance | Accessibility | Best practices | SEO | PWA |', - '| --- | --- | --- | --- | --- | --- |', - ]; - - Object.entries(links).forEach(([pageUrl, reportUrl]) => { - const relevantManifests = manifests.filter(manifest => manifest.url === pageUrl); - const results = relevantManifests.map(manifest => manifest.summary); - const averagedResults = {}; - - if (results.length > 0) { - Object.keys(results[0]).forEach(key => { - averagedResults[key] = formatResult( - results.reduce((acc, cur) => acc + cur[key], 0) / results.length - ); - }); - - const score = res => res >= 90 ? '🟢' : res >= 50 ? '🟠' : '🔴'; - const urlForTable = pageUrl.includes('/en/') ? pageUrl.substring(pageUrl.indexOf('/en/')) : pageUrl; - - comment.push( - `| [${urlForTable}](${reportUrl}) | ${score(averagedResults.performance)} ${averagedResults.performance} | ${score(averagedResults.accessibility)} ${averagedResults.accessibility} | ${score(averagedResults['best-practices'])} ${averagedResults['best-practices']} | ${score(averagedResults.seo)} ${averagedResults.seo} | ${score(averagedResults.pwa)} ${averagedResults.pwa} |` - ); - } else { - console.error('No results found for URL:', pageUrl); - } - }); - - comment.push( - ' ', - '*Lighthouse scores are calculated based on the latest audit results*' - ); - - comment = comment.join('\n'); - core.setOutput("comment", comment); - - name: Find current PR # Find the PR associated with this push, if there is one. - uses: jwalton/gh-find-current-pr@v1.3.3 - id: findPr - with: - state: open - - name: Add Lighthouse stats as comment - id: comment_to_pr - uses: marocchino/sticky-pull-request-comment@v2.0.0 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - number: ${{ steps.findPr.outputs.number }} - header: lighthouse - message: ${{ steps.format_lighthouse_score.outputs.comment }} diff --git a/.github/workflows/lint-markdown.yml b/.github/workflows/lint-markdown.yml deleted file mode 100644 index fddf17b5c01..00000000000 --- a/.github/workflows/lint-markdown.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Lint Markdown Content - -on: - pull_request: - paths: - - "public/content/**/*.md" - - "!public/content/translations/**" - -jobs: - lint-markdown: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Get changed content files - id: changed - run: | - FILES=$(git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }}...HEAD \ - | grep '^public/content/.*\.md$' \ - | grep -v '^public/content/translations/' \ - || true) - echo "files<> "$GITHUB_OUTPUT" - echo "$FILES" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - - name: Lint changed markdown files - if: steps.changed.outputs.files != '' - uses: DavidAnson/markdownlint-cli2-action@v22 - with: - globs: ${{ steps.changed.outputs.files }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 217bceb76af..00000000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Tests -on: - pull_request: - branches: [dev, master, staging] - workflow_dispatch: - -jobs: - unit-tests: - runs-on: ubuntu-latest - env: - CI: true - steps: - - uses: actions/checkout@v6 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v6 - with: - node-version: lts/* - cache: pnpm - - - name: Install dependencies - run: pnpm install - - - name: Run Unit Tests - run: pnpm test:unit - - e2e-tests: - if: github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - env: - CI: true - steps: - - uses: actions/checkout@v6 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v6 - with: - node-version: lts/* - cache: pnpm - - - name: Install dependencies - run: pnpm install - - - name: Get Netlify Deploy URL for branch - id: netlify_deploy - run: | - npx ts-node -O '{ "module": "commonjs" }' src/scripts/get-netlify-branch-deploy.ts - env: - NETLIFY_SITE_ID: "e8f2e766-888b-4954-8500-1b647d84db99" - NETLIFY_TOKEN: ${{ secrets.NETLIFY_TOKEN }} - GITHUB_REF_NAME: ${{ github.ref_name }} - - - name: Install Playwright with all browsers - run: npx playwright install --with-deps - - - name: Run E2E Tests - run: pnpm test:e2e - env: - PLAYWRIGHT_TEST_BASE_URL: ${{ steps.netlify_deploy.outputs.url }} - - - uses: actions/upload-artifact@v4 - if: always() - with: - name: playwright-report - path: ./tests/__results__ - retention-days: 7 diff --git a/.gitignore b/.gitignore index 40a1c7f9ed1..1738dc4a373 100644 --- a/.gitignore +++ b/.gitignore @@ -40,7 +40,6 @@ pnpm-lock.yaml.bak # typescript *.tsbuildinfo -next-env.d.ts # rss feeds feed.xml diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 00000000000..7a70f65a1ee --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/types/routes.d.ts" + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index 3e458e2e5eb..6562a886091 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "start": "next start", "lint": "eslint .", "lint:fix": "eslint . --fix", + "type-check": "tsc --noEmit", "format": "prettier \"**/*.{js,jsx,ts,tsx}\" --write", "preversion": "bash ./src/scripts/updatePublishDate.sh", "storybook": "storybook dev -p 6006",