diff --git a/.github/workflows/a11y-scan.yml b/.github/workflows/a11y-scan.yml index 17f89f859..b3e69e8bb 100644 --- a/.github/workflows/a11y-scan.yml +++ b/.github/workflows/a11y-scan.yml @@ -25,8 +25,20 @@ jobs: - run: npm ci + - run: npm run sync + - run: npm run build - - run: npx playwright install --with-deps chromium + - uses: actions/cache@v6 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + + - if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright install --with-deps chromium + + - if: steps.playwright-cache.outputs.cache-hit == 'true' + run: npx playwright install-deps chromium - run: npm run test:e2e diff --git a/.github/workflows/add-discussion-url.yml b/.github/workflows/add-discussion-url.yml index 9bb8c8ef7..d6aeba10d 100644 --- a/.github/workflows/add-discussion-url.yml +++ b/.github/workflows/add-discussion-url.yml @@ -102,8 +102,8 @@ jobs: DISCUSSION_URL: ${{ inputs.discussion_url }} run: node scripts/set-discussion-url.mjs - - name: Regenerate TypeScript - run: node scripts/generate-adventures.mjs + - name: Validate adventure YAML (Zod content schema) + run: npm run sync - name: Create pull request env: @@ -125,9 +125,6 @@ jobs: git add \ "src/data/adventures/${ADVENTURE_ID}/adventure.yaml" \ "src/data/adventures/${ADVENTURE_ID}/${LEVEL_ID}-posts.json" \ - "src/data/adventures/${ADVENTURE_ID}.generated.ts" \ - "src/data/adventures/index.ts" \ - "src/data/adventures/summaries.ts" \ "scripts/refresh-leaderboard.mjs" HAS_CHANGES=false @@ -154,8 +151,8 @@ jobs: - \`adventure.yaml\` — \`community_url\` set on the \`${LEVEL_ID}\` level; \`community_category_id\` set at adventure root if it was missing - \`${LEVEL_ID}-posts.json\` — \`discussionUrl\` set and initial posts fetched from Discourse - - Generated TypeScript updated (\`${ADVENTURE_ID}.generated.ts\`, \`index.ts\`, \`summaries.ts\`) - \`scripts/refresh-leaderboard.mjs\` — \`ADVENTURE_CATEGORIES\` patched with the resolved category ID + - (Astro reads the YAML directly — no generated TypeScript to update) ### Before merging diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1cd256da4..9cb1bed73 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,15 +31,18 @@ jobs: - run: npm ci - - run: npm run build + - run: npm run sync + - run: npm run lint - - run: cp dist/client/404/index.html dist/client/404.html + # Astro static build → dist/ (not dist/client/). Astro emits dist/404.html + # natively, so the old `cp .../404/index.html 404.html` step is gone. + - run: npm run build - name: Deploy to gh-pages uses: JamesIves/github-pages-deploy-action@v4 with: branch: gh-pages - folder: dist/client + folder: dist force: false clean: true clean-exclude: | diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index a175cb7b3..a04fcb1eb 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -12,7 +12,7 @@ env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: - lint: + check: if: github.event.action != 'closed' runs-on: ubuntu-latest steps: @@ -32,6 +32,9 @@ jobs: - if: steps.node-cache.outputs.cache-hit != 'true' run: npm ci + # Content validation gate: astro sync runs the Zod collection schema and + # fails on invalid adventure YAML. + - run: npm run sync - run: npm run lint build: @@ -56,12 +59,10 @@ jobs: - run: npm run build - - run: npm test - - uses: actions/upload-artifact@v7 with: - name: dist-client - path: dist/client/ + name: dist + path: dist/ retention-days: 1 e2e: @@ -89,10 +90,10 @@ jobs: - if: steps.node-cache.outputs.cache-hit != 'true' run: npm ci - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@v7 with: - name: dist-client - path: dist/client/ + name: dist + path: dist/ - uses: actions/cache@v6 id: playwright-cache @@ -106,13 +107,12 @@ jobs: - if: steps.playwright-cache.outputs.cache-hit == 'true' run: npx playwright install-deps chromium + # playwright.config webServer runs `astro preview` (serves dist/) itself. - run: npm run test:e2e -- --shard=${{ matrix.shard }}/3 preview: - needs: [lint, e2e] - # Run when both passed (normal PR update), or when build was skipped (PR closed, cleanup). - # Never run when tests failed. - if: always() && ((needs.lint.result == 'success' && needs.e2e.result == 'success') || needs.lint.result == 'skipped') + needs: [check, e2e] + if: always() && ((needs.check.result == 'success' && needs.e2e.result == 'success') || needs.check.result == 'skipped') runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -132,27 +132,13 @@ jobs: - if: github.event.action != 'closed' && steps.node-cache.outputs.cache-hit != 'true' run: npm ci + # Astro's `base` (from VITE_BASE_PATH) prefixes every asset/link, and public/ + # assets are copied into dist/ automatically — so the whole dist/ deploys as + # the preview with no per-directory asset-copy step. Layout.astro marks + # /pr-preview/ builds noindex. - if: github.event.action != 'closed' run: VITE_BASE_PATH=/pr-preview/pr-${{ github.event.number }}/ npm run build - - if: github.event.action != 'closed' - run: | - PREVIEW_DIR="dist/client/pr-preview/pr-${{ github.event.number }}" - cp -r dist/client/assets "${PREVIEW_DIR}/" - cp -r dist/client/fonts "${PREVIEW_DIR}/" - cp -r dist/client/reveal "${PREVIEW_DIR}/" - cp -r dist/client/team "${PREVIEW_DIR}/" - cp -r dist/client/speakers "${PREVIEW_DIR}/" - cp -r dist/client/brand "${PREVIEW_DIR}/" - cp -r dist/client/solutions "${PREVIEW_DIR}/" - cp -r dist/client/qr "${PREVIEW_DIR}/" - cp -r dist/client/downloads "${PREVIEW_DIR}/" - cp -r dist/client/screenshots "${PREVIEW_DIR}/" - cp -r dist/client/deck "${PREVIEW_DIR}/" - cp -r dist/client/deck-template "${PREVIEW_DIR}/" - find dist/client -maxdepth 1 -type f \( -name "*.svg" -o -name "*.png" -o -name "*.ico" -o -name "*.webmanifest" -o -name "*.webp" \) -exec cp {} "${PREVIEW_DIR}/" \; - cp "${PREVIEW_DIR}/index.html" "${PREVIEW_DIR}/404.html" - - uses: rossjrw/pr-preview-action@v1 with: - source-dir: dist/client/pr-preview/pr-${{ github.event.number }}/ + source-dir: dist/ diff --git a/.github/workflows/sync-adventure.yml b/.github/workflows/sync-adventure.yml index 6c9406682..f7d42cc2c 100644 --- a/.github/workflows/sync-adventure.yml +++ b/.github/workflows/sync-adventure.yml @@ -88,19 +88,20 @@ jobs: LEVELS_TO_SYNC: ${{ inputs.levels }} run: node scripts/sync-adventure.mjs - - name: Validate and regenerate TypeScript - run: node scripts/generate-adventures.mjs + - name: Validate adventure YAML (Zod content schema) + run: npm run sync - name: Create pull request env: GH_TOKEN: ${{ steps.app-token.outputs.token }} + ADVENTURE_URL: ${{ inputs.adventure_url }} run: | SLUG=$(cat /tmp/adventure-slug) NAME=$(cat /tmp/adventure-name) LEVELS=$(cat /tmp/adventure-levels) MODE=$(cat /tmp/adventure-mode) BRANCH="feat/adventure-${SLUG}" - SOURCE_URL="${{ inputs.adventure_url }}" + SOURCE_URL="$ADVENTURE_URL" git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -111,19 +112,13 @@ jobs: git fetch origin "$BRANCH" 2>/dev/null || true git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" + # Astro reads the YAML directly (Zod content collection) — no generated + # TS, prerender array, or sitemap region to commit. Routes come from + # getStaticPaths at build time. git add \ src/data/adventures/${SLUG}/ \ - src/data/adventures/${SLUG}.generated.ts \ - src/data/adventures/index.ts \ - src/data/adventures/summaries.ts \ - src/data/adventures/types.ts \ src/assets/diagrams/ \ - public/sitemap.xml \ - react-router.config.ts \ - scripts/refresh-leaderboard.mjs \ - e2e/smoke.spec.ts \ - src/test/seo.test.ts \ - src/test/prerender.test.ts + scripts/refresh-leaderboard.mjs git commit -m "feat: sync adventure ${SLUG} from challenges repo (${MODE})" # The branch is owned by this workflow, so a plain --force is safe here and @@ -156,23 +151,18 @@ jobs: ### Manual steps (only if needed) - - [ ] If a level's \`architecture_diagram\` SVG was not auto-fetched (check the sync logs for a warning), add the SVG manually to \`src/assets/diagrams/\` and add \`architecture_diagram: .svg\` to that level in \`adventure.yaml\`, then run \`npm run generate\` - - [ ] If a level has an \`architecture_diagram\` but no \`diagram_alt\`, add \`diagram_alt:\` to that level in \`adventure.yaml\` with a one-sentence description of the diagram, then run \`npm run generate\` - - [ ] Run \`node scripts/refresh-discussions.mjs\` after \`discussionUrl\` values are set + - [ ] If a level's \`architecture_diagram\` SVG was not auto-fetched (check the sync logs for a warning), add the SVG manually to \`src/assets/diagrams/\` and add \`architecture_diagram: .svg\` to that level in \`adventure.yaml\` - [ ] If a level has an \`architecture_diagram\` but no \`diagram_alt\`, add \`diagram_alt:\` to that level in \`adventure.yaml\` with a one-sentence description of the diagram - [ ] Run \`node scripts/refresh-discussions.mjs\` after \`discussionUrl\` values are set - ### Auto-generated (do not edit by hand) + ### Routes & sitemap - The following are kept in sync with \`adventure.yaml\` by \`npm run generate\` (prebuild hook): - - \`public/sitemap.xml\` (GENERATED:adventures region) - - \`react-router.config.ts\` prerender (GENERATED:adventures region) - - \`e2e/smoke.spec.ts\` and \`src/test/seo.test.ts\` route arrays - - \`src/test/prerender.test.ts\` pages array - - \`scripts/refresh-leaderboard.mjs\` ADVENTURE_CATEGORIES + Routes are generated from \`adventure.yaml\` by the Astro content collection + at build time (\`getStaticPaths\`) — there is nothing to regenerate or commit. + Only \`scripts/refresh-leaderboard.mjs\` ADVENTURE_CATEGORIES is hand-maintained. ### Checks \`\`\`sh - npm run lint && npm test && npm run build && npm run test:e2e + npm run sync && npm run build && npm run test:e2e \`\`\` EOF else @@ -197,32 +187,26 @@ jobs: about: "One sentence bio." \`\`\` - [ ] Confirm \`month:\` is correct for the planned release - - [ ] Set \`community_category_id:\` in \`src/data/adventures/${SLUG}/adventure.yaml\` (look up at https://community.offon.dev/categories.json), then run \`npm run generate\` - - [ ] Update \`rewards.deadline:\` from \`TODO\` to ISO 8601 (e.g. \`2026-07-01T23:59:00+01:00\`) + - [ ] Set \`community_category_id:\` in \`src/data/adventures/${SLUG}/adventure.yaml\` (look up at https://community.offon.dev/categories.json) - [ ] Update \`rewards.deadline:\` from \`TODO\` to ISO 8601 (e.g. \`2026-07-01T23:59:00+01:00\`) - [ ] Review \`topics:\` on each level, auto-set to all adventure tags, refine to level-specific subset if needed - [ ] Update \`discussion_url:\` in each level once the Discourse threads are created - [ ] Update \`discussionUrl\` in each \`*-posts.json\` stub ### Manual steps (only if needed) - - [ ] If a level's \`architecture_diagram\` SVG was not auto-fetched (check the sync logs for a warning), add the SVG manually to \`src/assets/diagrams/\` and add \`architecture_diagram: .svg\` to that level in \`adventure.yaml\`, then run \`npm run generate\` - - [ ] If a level has an \`architecture_diagram\` but no \`diagram_alt\`, add \`diagram_alt:\` to that level in \`adventure.yaml\` with a one-sentence description of the diagram, then run \`npm run generate\` - - [ ] Run \`node scripts/refresh-leaderboard.mjs\` after \`community_category_id\` is set + - [ ] If a level's \`architecture_diagram\` SVG was not auto-fetched (check the sync logs for a warning), add the SVG manually to \`src/assets/diagrams/\` and add \`architecture_diagram: .svg\` to that level in \`adventure.yaml\` - [ ] If a level has an \`architecture_diagram\` but no \`diagram_alt\`, add \`diagram_alt:\` to that level in \`adventure.yaml\` with a one-sentence description of the diagram - [ ] Run \`node scripts/refresh-leaderboard.mjs\` after \`community_category_id\` is set - [ ] Run \`node scripts/refresh-discussions.mjs\` after \`discussionUrl\` values are set - ### Auto-generated (do not edit by hand) + ### Routes & sitemap - The following are kept in sync with \`adventure.yaml\` by \`npm run generate\` (prebuild hook): - - \`public/sitemap.xml\` (GENERATED:adventures region) - - \`react-router.config.ts\` prerender (GENERATED:adventures region) - - \`e2e/smoke.spec.ts\` and \`src/test/seo.test.ts\` route arrays - - \`src/test/prerender.test.ts\` pages array - - \`scripts/refresh-leaderboard.mjs\` ADVENTURE_CATEGORIES + Routes are generated from \`adventure.yaml\` by the Astro content collection + at build time (\`getStaticPaths\`) — there is nothing to regenerate or commit. + Only \`scripts/refresh-leaderboard.mjs\` ADVENTURE_CATEGORIES is hand-maintained. ### Checks \`\`\`sh - npm run lint && npm test && npm run build && npm run test:e2e + npm run sync && npm run build && npm run test:e2e \`\`\` EOF fi diff --git a/.github/workflows/validate-adventures.yml b/.github/workflows/validate-adventures.yml index d29a4cd70..988c4e5d7 100644 --- a/.github/workflows/validate-adventures.yml +++ b/.github/workflows/validate-adventures.yml @@ -5,9 +5,7 @@ on: paths: - 'src/data/adventures/**' - 'src/data/solutions/**' - - 'react-router.config.ts' - - 'public/sitemap.xml' - - 'schemas/adventure.schema.json' + - 'src/content.config.ts' permissions: contents: read @@ -37,110 +35,39 @@ jobs: - if: steps.node-cache.outputs.cache-hit != 'true' run: npm ci - - name: Validate YAML schema - run: node scripts/generate-adventures.mjs --validate-only - - - name: Validate solution index is up-to-date - run: node scripts/generate-solutions.mjs --validate-only - - - name: Verify generated files are up-to-date - run: | - node scripts/generate-adventures.mjs - node scripts/generate-solutions.mjs - GENERATED_PATHS=( - src/data/adventures/ - src/data/solutions/index.ts - src/data/solutions/manifest.ts - public/llms.txt - public/sitemap.xml - react-router.config.ts - scripts/refresh-leaderboard.mjs - e2e/smoke.spec.ts - src/test/seo.test.ts - src/test/prerender.test.ts - ) - if ! git diff --quiet "${GENERATED_PATHS[@]}"; then - echo "❌ Generated files are out of date. Run 'npm run generate' and 'npm run generate:solutions' and commit the result." - git diff --stat "${GENERATED_PATHS[@]}" - exit 1 - fi + # astro sync loads the content collection and runs the Zod schema over every + # adventure.yaml. Invalid YAML (unknown field, bad enum, missing required) + # fails here — this replaces the old ajv `generate-adventures.mjs --validate-only`. + - name: Validate adventure YAML (Zod content schema) + run: npm run sync - name: Validate adventure consistency run: | set -euo pipefail - ERRORS=0 - - # Extract adventure IDs from YAML files for adventure_dir in src/data/adventures/*/; do adventure_id=$(basename "$adventure_dir") - - # Skip if no adventure.yaml - if [[ ! -f "$adventure_dir/adventure.yaml" ]]; then - continue - fi - + [[ -f "$adventure_dir/adventure.yaml" ]] || continue echo "Checking adventure: $adventure_id" - # Check generated TS file exists - if [[ ! -f "src/data/adventures/$adventure_id.generated.ts" ]]; then - echo " ❌ Missing generated file: src/data/adventures/$adventure_id.generated.ts" - ERRORS=$((ERRORS + 1)) - fi - - # Check adventure is imported in index.ts - if ! grep -q "from \"./$adventure_id.generated\"" src/data/adventures/index.ts; then - echo " ❌ Not imported in index.ts" - ERRORS=$((ERRORS + 1)) - fi - - # Extract live level IDs only (under `levels:`, not `upcoming_levels:`) - level_ids=$(awk '/^levels:/{s=1;next} /^[a-z]/{s=0} s && /- level:/{gsub(/.*- level: /,""); gsub(/"/,""); print}' "$adventure_dir/adventure.yaml" || true) + # Live level IDs parsed via the yaml package (avoids fragile awk line-matching) + level_ids=$(YAML_PATH="$adventure_dir/adventure.yaml" node -e "const y=require('yaml'),f=require('fs');(y.parse(f.readFileSync(process.env.YAML_PATH,'utf8')).levels||[]).forEach(l=>console.log(l.level));") - # Check adventure landing page in prerender - if ! grep -q "\"/adventures/$adventure_id\"" react-router.config.ts; then - echo " ❌ /adventures/$adventure_id missing from prerender in react-router.config.ts" - ERRORS=$((ERRORS + 1)) - fi - - # Check adventure landing page in sitemap - if ! grep -q "offon.dev/adventures/$adventure_id/" public/sitemap.xml; then - echo " ❌ /adventures/$adventure_id/ missing from sitemap.xml" - ERRORS=$((ERRORS + 1)) - fi - - # Check each level for level_id in $level_ids; do - # Discussion JSON exists - if [[ ! -f "src/data/adventures/$adventure_id/$level_id-posts.json" ]]; then - echo " ❌ Missing discussion JSON: src/data/adventures/$adventure_id/$level_id-posts.json" - ERRORS=$((ERRORS + 1)) - fi - - # Prerender entry exists - if ! grep -q "\"/adventures/$adventure_id/levels/$level_id\"" react-router.config.ts; then - echo " ❌ /adventures/$adventure_id/levels/$level_id missing from prerender" - ERRORS=$((ERRORS + 1)) - fi - - # Sitemap entry exists - if ! grep -q "offon.dev/adventures/$adventure_id/levels/$level_id/" public/sitemap.xml; then - echo " ❌ /adventures/$adventure_id/levels/$level_id/ missing from sitemap.xml" + # A discussion JSON file must exist for each live level. + if [[ ! -f "$adventure_dir/$level_id-posts.json" ]]; then + echo " ❌ Missing discussion JSON: $adventure_dir$level_id-posts.json" ERRORS=$((ERRORS + 1)) fi done - echo " ✓ Done" done - if [[ $ERRORS -gt 0 ]]; then echo "" - echo "❌ Found $ERRORS consistency error(s). Run 'npm run generate' to regenerate route/sitemap/prerender regions, or re-run the Sync Adventure workflow." + echo "❌ Found $ERRORS consistency error(s)." exit 1 - else - echo "" - echo "✓ All adventures are consistent." fi + echo "✓ All adventures are consistent." - name: Verify all adventures are registered in ADVENTURE_CATEGORIES run: | @@ -148,9 +75,7 @@ jobs: ERRORS=0 for adventure_dir in src/data/adventures/*/; do adventure_id=$(basename "$adventure_dir") - if [[ ! -f "$adventure_dir/adventure.yaml" ]]; then - continue - fi + [[ -f "$adventure_dir/adventure.yaml" ]] || continue if ! grep -q "\"$adventure_id\"" scripts/refresh-leaderboard.mjs; then echo "❌ '$adventure_id' is missing from ADVENTURE_CATEGORIES in scripts/refresh-leaderboard.mjs" ERRORS=$((ERRORS + 1)) @@ -160,6 +85,5 @@ jobs: echo "" echo "❌ Add missing adventure IDs to ADVENTURE_CATEGORIES in scripts/refresh-leaderboard.mjs before merging." exit 1 - else - echo "✓ All adventure IDs are registered in ADVENTURE_CATEGORIES." fi + echo "✓ All adventure IDs are registered in ADVENTURE_CATEGORIES." diff --git a/.github/workflows/validate-docs.yml b/.github/workflows/validate-docs.yml index 0a0b35cfe..be9cfd3cb 100644 --- a/.github/workflows/validate-docs.yml +++ b/.github/workflows/validate-docs.yml @@ -4,9 +4,10 @@ on: pull_request: paths: - 'src/components/**' - - 'src/hooks/**' - 'src/lib/**' - - 'src/data/constants.ts' + - 'src/stores/**' + - 'src/pages/**' + - 'src/lib/site.ts' - 'package.json' - '.github/workflows/*.yml' diff --git a/.gitignore b/.gitignore index 74caff3bb..906f82ce8 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,9 @@ tmp/ *.njsproj *.sln *.sw? + +# Astro +.astro/ +test-results/ +playwright-report/ +.vite/ diff --git a/ACCESSIBILITY.md b/ACCESSIBILITY.md index beab826f3..017622def 100644 --- a/ACCESSIBILITY.md +++ b/ACCESSIBILITY.md @@ -30,8 +30,9 @@ The following WCAG 2.2 Level AAA criteria are actively targeted on this site: - Skip-to-content link as the first focusable element on every page. - Visible focus rings on all interactive elements, in both light and dark mode. -- Semantic landmarks: one `
`, plus `