diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e16102..4a0a8cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,33 @@ jobs: exit 1 fi + - name: Ensure handoff artifacts are not tracked + run: | + patterns=( + 'HANDOFF.md' + 'HANDOFF_*.md' + 'handoff.md' + 'handoff_*.md' + 'PR_INFO.md' + 'PR_DESCRIPTION.md' + 'PR_BODY.md' + 'pr-info.md' + 'pr_info.md' + 'pr-description.md' + 'pr_description.md' + 'pr-body.md' + 'pr_body.md' + ) + found=0 + for p in "${patterns[@]}"; do + matches=$(git ls-files "$p" 2>/dev/null) + if [ -n "$matches" ]; then + echo "::error::Handoff artifact must not be committed: $matches" + found=1 + fi + done + if [ "$found" -eq 1 ]; then exit 1; fi + serverless-smoke: name: Serverless Smoke if: github.event_name == 'pull_request' diff --git a/PR_INFO.md b/PR_INFO.md deleted file mode 100644 index 1ab5237..0000000 --- a/PR_INFO.md +++ /dev/null @@ -1,145 +0,0 @@ -# Pull Request Information - -## Summary - -Successfully completed issue #134: Added Portuguese (PT) locale and established translation contribution workflow. - -## PR Details - -**Title:** `feat(i18n): add Portuguese locale and translation contribution flow` - -**Base Branch:** `develop` (in wraith-protocol/www) -**Head Branch:** `feat/add-portuguese-locale` (in code3ks/www) - -**PR Body:** - -````markdown -## Summary - -This PR adds Portuguese (PT) as a third locale to the website and establishes a documented, repeatable translation contribution workflow. - -## Changes - -### Core Implementation - -- ✅ Added `src/i18n/pt.json` with complete Portuguese translations (241 keys) -- ✅ Extended `scripts/lint-i18n.mjs` to enforce parity across all locales - - Validates all locale files have identical key structures - - Fails CI on missing or extraneous keys - - Detects hardcoded strings in components -- ✅ Updated `src/i18n/index.ts` to register Portuguese locale -- ✅ Language switcher automatically picks up PT from locale list - -### Documentation - -- ✅ Created `TRANSLATIONS.md` at repo root documenting: - - How to add a new locale - - How to keep translations in sync - - What strings not to translate (brand names, code snippets) - - Review process and contributor recognition - -### Contributors - -- ✅ Created `src/data/team.json` for contributor credits - -## Testing - -### Locale Parity Validation - -```bash -pnpm run lint:i18n -``` -```` - -Output: - -``` -✅ All locales have matching key structures! -✅ No hardcoded JSX strings found in 14 component(s). -✅ All i18n checks passed! -``` - -### Build Verification - -```bash -pnpm run build -``` - -- ✅ TypeScript compilation passes -- ✅ Vite build successful -- ✅ All assets generated correctly - -### Automated Tests - -```bash -pnpm test -``` - -- ✅ All i18n-related tests pass -- ✅ Footer, NotFound, and A11y tests pass (11 total) -- ⚠️ 2 pre-existing Stellar page test failures (unrelated to i18n) - -## Screenshots - -The Portuguese locale is automatically available in the language switcher and can be selected by users with PT browser settings or manually via the header dropdown. - -## Acceptance Criteria - -- [x] Every page renders in EN, ES, PT with no missing-key fallbacks -- [x] CI parity gate fails on introduced drift -- [x] TRANSLATIONS.md covers the contribution loop end-to-end -- [x] Build passes -- [x] Lighthouse Perf/SEO remain high (no regressions) - -Closes #134 - -```` - -## How to Create the PR - -1. Go to: https://github.com/wraith-protocol/www/compare/develop...code3ks:www:feat/add-portuguese-locale - -2. Click "Create pull request" - -3. Fill in the title and description from above - -4. Submit the PR - -## Changes Made - -### Files Added -- `src/i18n/pt.json` - Complete Portuguese translations -- `TRANSLATIONS.md` - Translation contribution guide -- `src/data/team.json` - Contributor credits - -### Files Modified -- `src/i18n/index.ts` - Registered Portuguese locale -- `scripts/lint-i18n.mjs` - Enhanced to enforce locale parity - -## Verification Commands - -Run these to verify the implementation: - -```bash -# Check locale parity -pnpm run lint:i18n - -# Build the project -pnpm run build - -# Run tests -pnpm test -```` - -## Next Steps - -1. Create the PR on GitHub using the link above -2. Wait for CI to run and verify all checks pass -3. Request review from maintainers -4. Get native Portuguese speaker review (optional but recommended) -5. Address any feedback -6. Get PR merged - -## Issue Link - -Closes: https://github.com/wraith-protocol/www/issues/134 diff --git a/package.json b/package.json index a92f3dd..43e20ac 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "test:a11y": "vitest run src/__tests__/a11y.test.tsx", "test:a11y:playwright": "playwright test tests/a11y", "test:link-check": "vitest run tests/link-check", + "test:gate": "bash tests/handoff-gate.test.sh", "check:links": "tsx scripts/check-links.ts", "format": "prettier --write .", "format:check": "prettier --check .", diff --git a/tests/handoff-gate.test.sh b/tests/handoff-gate.test.sh new file mode 100755 index 0000000..6620ee3 --- /dev/null +++ b/tests/handoff-gate.test.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# Tests for the "Ensure handoff artifacts are not tracked" CI gate. +# +# Strategy: initialise a throw-away git repo in a temp directory, stage one +# artifact at a time, run the gate logic, and assert it exits non-zero. +# Then verify that the matching lowercase / hyphen / underscore variants that +# were historically missing also trigger the gate. + +set -euo pipefail + +PASS=0 +FAIL=0 + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +# Colours (suppressed when not a TTY) +if [ -t 1 ]; then + GREEN='\033[0;32m'; RED='\033[0;31m'; RESET='\033[0m' +else + GREEN=''; RED=''; RESET='' +fi + +ok() { echo -e "${GREEN} PASS${RESET} $1"; PASS=$((PASS+1)); } +fail() { echo -e "${RED} FAIL${RESET} $1"; FAIL=$((FAIL+1)); } + +# Run the gate logic against a git repo that has $1 tracked. +# Returns the gate's exit code (0 = clean, 1 = blocked). +run_gate_with_file() { + local file="$1" + local tmpdir + tmpdir=$(mktemp -d) + # shellcheck disable=SC2064 + trap "rm -rf '$tmpdir'" RETURN + + git -C "$tmpdir" init -q + git -C "$tmpdir" config user.email "test@example.com" + git -C "$tmpdir" config user.name "Test" + + # Create parent dirs if the path contains a slash (subdirectory variant) + mkdir -p "$tmpdir/$(dirname "$file")" + echo "stub" > "$tmpdir/$file" + git -C "$tmpdir" add "$file" + + # Replicate the exact gate logic from ci.yml + ( + cd "$tmpdir" + patterns=( + 'HANDOFF.md' + 'HANDOFF_*.md' + 'handoff.md' + 'handoff_*.md' + 'PR_INFO.md' + 'PR_DESCRIPTION.md' + 'PR_BODY.md' + 'pr-info.md' + 'pr_info.md' + 'pr-description.md' + 'pr_description.md' + 'pr-body.md' + 'pr_body.md' + ) + found=0 + for p in "${patterns[@]}"; do + matches=$(git ls-files "$p" 2>/dev/null) + if [ -n "$matches" ]; then + found=1 + fi + done + exit "$found" + ) + return $? +} + +# Assert that staging $1 causes the gate to block (exit 1). +assert_blocked() { + local file="$1" + if run_gate_with_file "$file"; then + fail "gate should block '$file' but it passed" + else + ok "gate blocks '$file'" + fi +} + +# Assert that staging $1 does NOT trigger the gate (exit 0). +assert_allowed() { + local file="$1" + if run_gate_with_file "$file"; then + ok "gate allows '$file'" + else + fail "gate should allow '$file' but it blocked" + fi +} + +# --------------------------------------------------------------------------- +# HANDOFF family +# --------------------------------------------------------------------------- +echo +echo "--- HANDOFF family ---" +assert_blocked "HANDOFF.md" +assert_blocked "HANDOFF_v2.md" +assert_blocked "HANDOFF_final.md" +assert_blocked "handoff.md" +assert_blocked "handoff_v2.md" +assert_blocked "handoff_final.md" + +# --------------------------------------------------------------------------- +# PR_INFO / pr-info / pr_info family +# --------------------------------------------------------------------------- +echo +echo "--- PR_INFO family ---" +assert_blocked "PR_INFO.md" +assert_blocked "pr-info.md" +assert_blocked "pr_info.md" + +# --------------------------------------------------------------------------- +# PR_DESCRIPTION / pr-description / pr_description family +# --------------------------------------------------------------------------- +echo +echo "--- PR_DESCRIPTION family ---" +assert_blocked "PR_DESCRIPTION.md" +assert_blocked "pr-description.md" +assert_blocked "pr_description.md" + +# --------------------------------------------------------------------------- +# PR_BODY / pr-body / pr_body family +# --------------------------------------------------------------------------- +echo +echo "--- PR_BODY family ---" +assert_blocked "PR_BODY.md" +assert_blocked "pr-body.md" +assert_blocked "pr_body.md" + +# --------------------------------------------------------------------------- +# Negative cases — files that must NOT be blocked +# --------------------------------------------------------------------------- +echo +echo "--- Negative cases (should pass the gate) ---" +assert_allowed "README.md" +assert_allowed "docs/CONTRIBUTING.md" +assert_allowed "CHANGELOG.md" +assert_allowed "src/handoff-utils.ts" # not a .md at root +assert_allowed "notes/handoff-notes.txt" # wrong extension + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +echo +echo "Results: ${PASS} passed, ${FAIL} failed" +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi