diff --git a/.github/workflows/auto-promote.yml b/.github/workflows/auto-promote.yml new file mode 100644 index 0000000..96e8747 --- /dev/null +++ b/.github/workflows/auto-promote.yml @@ -0,0 +1,216 @@ +name: Auto-Promote Pipeline + +on: + workflow_run: + workflows: ["Deploy to Development"] + types: + - completed + branches: [ main ] + +jobs: + test-and-promote: + name: Test Dev and Auto-Promote + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Wait for dev deployment to settle + run: | + echo "โณ Waiting for dev deployment to fully settle..." + sleep 60 + + - name: Run comprehensive dev tests + run: | + echo "๐Ÿงช Running comprehensive tests on dev environment..." + + # Test canonical domain first (primary test) + echo "Testing canonical domain: game-2048-dev.game-2048-dev.dev.wa.darknex.us" + canonical_response=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 https://game-2048-dev.game-2048-dev.dev.wa.darknex.us/) + if [ "$canonical_response" != "200" ]; then + echo "โŒ Canonical domain returned HTTP $canonical_response" + exit 1 + fi + echo "โœ… Canonical domain accessible" + + # Test SSL certificate on custom domain + echo "Testing SSL certificate on custom domain..." + cert_expiry=$(echo | openssl s_client -servername 2048-dev.wa.darknex.us -connect 2048-dev.wa.darknex.us:443 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2) + expiry_epoch=$(date -d "$cert_expiry" +%s) + current_epoch=$(date +%s) + days_until_expiry=$(( (expiry_epoch - current_epoch) / 86400 )) + + if [ $days_until_expiry -lt 30 ]; then + echo "โŒ SSL certificate expires in less than 30 days!" + exit 1 + fi + echo "โœ… SSL certificate valid for $days_until_expiry days" + + # Test custom domain accessibility + echo "Testing custom domain accessibility..." + response_code=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 https://2048-dev.wa.darknex.us/) + if [ "$response_code" != "200" ]; then + echo "โŒ Custom domain returned HTTP $response_code" + exit 1 + fi + echo "โœ… Custom domain accessible" + + # Test content validation on canonical domain + echo "Testing content validation on canonical domain..." + content=$(curl -s -L --max-time 30 https://game-2048-dev.game-2048-dev.dev.wa.darknex.us/) + + if ! echo "$content" | grep -q "2048"; then + echo "โŒ Content missing 2048 title" + exit 1 + fi + + if ! echo "$content" | grep -q "HOW TO PLAY"; then + echo "โŒ Content missing game instructions" + exit 1 + fi + + if ! echo "$content" | grep -q "style.css"; then + echo "โŒ CSS file not referenced" + exit 1 + fi + + if ! echo "$content" | grep -q "script.js"; then + echo "โŒ JavaScript file not referenced" + exit 1 + fi + + echo "โœ… All content validation tests passed" + + # Test performance on canonical domain + echo "Testing performance on canonical domain..." + response_time=$(curl -s -o /dev/null -w "%{time_total}" -L --max-time 30 https://game-2048-dev.game-2048-dev.dev.wa.darknex.us/) + if (( $(echo "$response_time > 10.0" | bc -l) )); then + echo "โŒ Response time too slow: ${response_time}s" + exit 1 + fi + echo "โœ… Performance test passed: ${response_time}s" + + - name: Auto-promote to staging + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + console.log('๐Ÿš€ All dev tests passed! Auto-promoting to staging...'); + + const response = await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'deploy-staging.yml', + ref: 'main', + inputs: { + image_tag: 'latest' + } + }); + + console.log('โœ… Staging deployment triggered'); + + return response; + + - name: Create promotion summary + run: | + echo "## ๐ŸŽฏ Auto-Promotion Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Stage | Status | Action |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Dev Tests | โœ… Passed | Comprehensive validation completed |" >> $GITHUB_STEP_SUMMARY + echo "| Staging | ๐Ÿš€ Triggered | Auto-promotion initiated |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ“‹ Tests Performed" >> $GITHUB_STEP_SUMMARY + echo "- SSL certificate validation" >> $GITHUB_STEP_SUMMARY + echo "- Domain accessibility check" >> $GITHUB_STEP_SUMMARY + echo "- Content and functionality validation" >> $GITHUB_STEP_SUMMARY + echo "- Performance testing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ”— Next Steps" >> $GITHUB_STEP_SUMMARY + echo "- Monitor staging deployment progress" >> $GITHUB_STEP_SUMMARY + echo "- Staging tests will run automatically" >> $GITHUB_STEP_SUMMARY + echo "- Production promotion requires manual approval" >> $GITHUB_STEP_SUMMARY + + promote-to-production: + name: Test Staging and Promote to Production + runs-on: ubuntu-latest + needs: test-and-promote + if: success() + environment: production-approval # This requires manual approval + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Wait for staging deployment + run: | + echo "โณ Waiting for staging deployment to complete..." + sleep 120 # Give staging time to deploy + + - name: Test staging environment + run: | + echo "๐Ÿงช Running staging tests..." + + # Test canonical staging domain first + echo "Testing canonical staging domain: game-2048-staging.game-2048-staging.staging.wa.darknex.us" + canonical_response=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 https://game-2048-staging.game-2048-staging.staging.wa.darknex.us/) + if [ "$canonical_response" != "200" ]; then + echo "โŒ Staging canonical domain returned HTTP $canonical_response" + exit 1 + fi + echo "โœ… Staging canonical domain accessible" + + # Test custom staging domain + echo "Testing custom staging domain: 2048-staging.wa.darknex.us" + response_code=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 https://2048-staging.wa.darknex.us/) + if [ "$response_code" != "200" ]; then + echo "โŒ Staging custom domain returned HTTP $response_code" + exit 1 + fi + echo "โœ… Staging custom domain accessible" + + # Test staging content on canonical domain + echo "Testing staging content..." + content=$(curl -s -L --max-time 30 https://game-2048-staging.game-2048-staging.staging.wa.darknex.us/) + if ! echo "$content" | grep -q "2048"; then + echo "โŒ Staging content validation failed" + exit 1 + fi + echo "โœ… Staging content validation passed" + + - name: Auto-promote to production + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + console.log('๐ŸŽฏ Staging tests passed! Promoting to production...'); + + const response = await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'deploy-prod.yml', + ref: 'main', + inputs: { + image_tag: 'latest', + confirmation: 'DEPLOY' + } + }); + + console.log('๐Ÿš€ Production deployment triggered'); + + return response; + + - name: Create final summary + run: | + echo "## ๐ŸŽ‰ Full Pipeline Completion" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Environment | Status | URL |" >> $GITHUB_STEP_SUMMARY + echo "|-------------|--------|-----|" >> $GITHUB_STEP_SUMMARY + echo "| Development | โœ… Tested & Live | https://2048-dev.wa.darknex.us |" >> $GITHUB_STEP_SUMMARY + echo "| Staging | โœ… Tested & Live | https://2048-staging.wa.darknex.us |" >> $GITHUB_STEP_SUMMARY + echo "| Production | ๐Ÿš€ Deploying | https://2048.wa.darknex.us |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐ŸŽฎ Your 2048 Game is Live!" >> $GITHUB_STEP_SUMMARY + echo "All environments have been automatically tested and promoted successfully." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 0000000..6cd70b6 --- /dev/null +++ b/.github/workflows/build-image.yml @@ -0,0 +1,64 @@ +name: Build and Push Container Image + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ghndrx/k8s-game-2048 + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push container image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Create build summary + run: | + echo "## ๐Ÿ“ฆ Container Image Build Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Registry | ${{ env.REGISTRY }} |" >> $GITHUB_STEP_SUMMARY + echo "| Image | ${{ env.IMAGE_NAME }} |" >> $GITHUB_STEP_SUMMARY + echo "| Tags | $(echo '${{ steps.meta.outputs.tags }}' | tr '\n' ', ') |" >> $GITHUB_STEP_SUMMARY + echo "| Trigger | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Next Steps" >> $GITHUB_STEP_SUMMARY + echo "- ๐Ÿงช Development deployment will trigger automatically" >> $GITHUB_STEP_SUMMARY + echo "- ๐ŸŽญ [Deploy to staging manually](https://github.com/${{ github.repository }}/actions/workflows/deploy-staging.yml)" >> $GITHUB_STEP_SUMMARY + echo "- ๐Ÿš€ [Deploy to production manually](https://github.com/${{ github.repository }}/actions/workflows/deploy-prod.yml)" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..db8e3c6 --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,217 @@ +name: Deploy to Development + +on: + workflow_run: + workflows: ["Build and Push Container Image"] + types: + - completed + branches: [ main ] + push: + branches: [ main ] + workflow_dispatch: + inputs: + image_tag: + description: 'Image tag to deploy (default: latest)' + required: false + default: 'latest' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ghndrx/k8s-game-2048 + +jobs: + deploy-dev: + name: Deploy to Development + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} + environment: development + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up kubectl + uses: azure/setup-kubectl@v3 + with: + version: 'latest' + + - name: Configure kubectl + run: | + mkdir -p ~/.kube + echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + - name: Set image tag + run: | + IMAGE_TAG="${{ github.event.inputs.image_tag || 'latest' }}" + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV + echo "Deploying image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$IMAGE_TAG" + + - name: Deploy to development + run: | + echo "๐Ÿš€ Deploying to development environment..." + + # Apply namespace + kubectl apply -f manifests/dev/namespace.yml + + # Ensure GHCR secret exists + if kubectl get secret ghcr-secret -n default &>/dev/null; then + echo "๐Ÿ” Copying GHCR secret to dev namespace..." + kubectl get secret ghcr-secret -o yaml | \ + sed 's/namespace: default/namespace: game-2048-dev/' | \ + sed '/resourceVersion:/d' | \ + sed '/uid:/d' | \ + sed '/creationTimestamp:/d' | \ + kubectl apply -f - + fi + + # Update image in service and deploy + kubectl patch ksvc game-2048-dev -n game-2048-dev --type merge -p '{"spec":{"template":{"spec":{"containers":[{"image":"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}","imagePullPolicy":"Always"}]}}}}' + + echo "โณ Waiting for deployment to be ready..." + kubectl wait --for=condition=Ready ksvc/game-2048-dev -n game-2048-dev --timeout=300s || echo "โš ๏ธ Service may still be starting" + + - name: Verify deployment + run: | + echo "๐Ÿ“Š Deployment status:" + kubectl get ksvc -n game-2048-dev + + echo "" + echo "โœ… Development deployment completed!" + echo "๐ŸŒ Available at: https://2048-dev.wa.darknex.us" + + - name: Run smoke test + run: | + echo "๐Ÿงช Running smoke test..." + sleep 30 + + for i in {1..5}; do + echo "Attempt $i/5..." + # Test canonical domain first + if curl -s --max-time 30 https://game-2048-dev.game-2048-dev.dev.wa.darknex.us/ | grep -q "2048"; then + echo "โœ… Canonical domain smoke test passed!" + break + # Fallback to custom domain + elif curl -s --max-time 30 https://2048-dev.wa.darknex.us/ | grep -q "2048"; then + echo "โœ… Custom domain smoke test passed!" + break + elif [ $i -eq 5 ]; then + echo "โš ๏ธ Smoke test failed after 5 attempts" + exit 1 + else + echo "Retrying in 30 seconds..." + sleep 30 + fi + done + uses: azure/setup-kubectl@v3 + with: + version: 'v1.28.0' + + - name: Configure kubectl + run: | + echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig + export KUBECONFIG=kubeconfig + + - name: Update image in manifests + run: | + sed -i "s|ghcr.io/ghndrx/k8s-game-2048:latest|${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}|g" manifests/dev/service.yml + + - name: Deploy to development + run: | + export KUBECONFIG=kubeconfig + kubectl apply -f manifests/dev/ + + - name: Wait for deployment + run: | + export KUBECONFIG=kubeconfig + kubectl wait --for=condition=Ready ksvc/game-2048-dev -n game-2048-dev --timeout=300s + + - name: Get service URL + id: get-url + run: | + export KUBECONFIG=kubeconfig + SERVICE_URL=$(kubectl get ksvc game-2048-dev -n game-2048-dev -o jsonpath='{.status.url}') + echo "service_url=$SERVICE_URL" >> $GITHUB_OUTPUT + echo "๐Ÿš€ Development service deployed at: $SERVICE_URL" + + - name: Set up Node.js for testing + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: tests/package.json + + - name: Install Playwright dependencies + run: | + cd tests + npm install + npx playwright install --with-deps + + - name: Run Playwright tests + run: | + cd tests + BASE_URL=${{ steps.get-url.outputs.service_url }} npx playwright test + env: + CI: true + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-results-dev-${{ github.sha }}-${{ github.run_number }} + path: | + tests/playwright-report/ + tests/test-results/ + retention-days: 30 + + - name: Upload screenshots + uses: actions/upload-artifact@v4 + if: always() + with: + name: screenshots-dev-${{ github.sha }}-${{ github.run_number }} + path: tests/test-results/**/*.png + retention-days: 30 + + promote-to-staging: + needs: build-and-deploy + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create Pull Request to Staging + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + push-to-fork: false + branch: auto-promote/dev-to-staging-${{ github.sha }} + base: staging + title: "๐Ÿš€ Auto-promote: Deploy ${{ github.sha }} to staging" + body: | + ## ๐Ÿš€ Auto-promotion from Development + + **Source**: `develop` branch + **Commit**: ${{ github.sha }} + **Triggered by**: @${{ github.actor }} + + ### โœ… Development Tests Passed + - Basic functionality tests + - Gameplay mechanics tests + - Visual regression tests + - Environment validation tests + + ### ๐ŸŽฏ Changes in this promotion: + ${{ github.event.head_commit.message }} + + This PR was automatically created after successful deployment and testing in the development environment. + + **Development URL**: https://2048-dev.wa.darknex.us + **Will deploy to**: https://2048-staging.wa.darknex.us + labels: | + auto-promotion + staging + deploy diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 0000000..814a352 --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,262 @@ +name: Deploy to Production + +on: + workflow_dispatch: + inputs: + image_tag: + description: 'Image tag to deploy (default: latest)' + required: false + default: 'latest' + confirmation: + description: 'Type "DEPLOY" to confirm production deployment' + required: true + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ghndrx/k8s-game-2048 + +jobs: + deploy-prod: + name: Deploy to Production + runs-on: ubuntu-latest + environment: production + if: ${{ github.event.inputs.confirmation == 'DEPLOY' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up kubectl + uses: azure/setup-kubectl@v3 + with: + version: 'latest' + + - name: Configure kubectl + run: | + mkdir -p ~/.kube + echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + - name: Set image tag + run: | + IMAGE_TAG="${{ github.event.inputs.image_tag || 'latest' }}" + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV + echo "Deploying image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$IMAGE_TAG" + + - name: Deploy to production + run: | + echo "๐Ÿš€ Deploying to production environment..." + + # Apply namespace + kubectl apply -f manifests/prod/namespace.yml + + # Ensure GHCR secret exists + if kubectl get secret ghcr-secret -n default &>/dev/null; then + echo "๐Ÿ” Copying GHCR secret to prod namespace..." + kubectl get secret ghcr-secret -o yaml | \ + sed 's/namespace: default/namespace: game-2048-prod/' | \ + sed '/resourceVersion:/d' | \ + sed '/uid:/d' | \ + sed '/creationTimestamp:/d' | \ + kubectl apply -f - + fi + + # Update image in service and deploy + kubectl patch ksvc game-2048-prod -n game-2048-prod --type merge -p '{"spec":{"template":{"spec":{"containers":[{"image":"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}","imagePullPolicy":"Always"}]}}}}' + + echo "โณ Waiting for deployment to be ready..." + kubectl wait --for=condition=Ready ksvc/game-2048-prod -n game-2048-prod --timeout=300s || echo "โš ๏ธ Service may still be starting" + + - name: Verify deployment + run: | + echo "๐Ÿ“Š Deployment status:" + kubectl get ksvc -n game-2048-prod + + echo "" + echo "โœ… Production deployment completed!" + echo "๐ŸŒ Available at: https://2048.wa.darknex.us" + + - name: Run smoke test + run: | + echo "๐Ÿงช Running smoke test..." + sleep 30 + + for i in {1..5}; do + echo "Attempt $i/5..." + # Test canonical domain first + if curl -s --max-time 30 https://game-2048-prod.game-2048-prod.wa.darknex.us/ | grep -q "2048"; then + echo "โœ… Canonical domain smoke test passed!" + break + # Fallback to custom domain + elif curl -s --max-time 30 https://2048.wa.darknex.us/ | grep -q "2048"; then + echo "โœ… Custom domain smoke test passed!" + break + elif [ $i -eq 5 ]; then + echo "โš ๏ธ Smoke test failed after 5 attempts" + exit 1 + else + echo "Retrying in 30 seconds..." + sleep 30 + fi + done + + - name: Create production deployment summary + run: | + echo "## ๐Ÿš€ Production Deployment Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Environment | **Production** |" >> $GITHUB_STEP_SUMMARY + echo "| Image | \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Domain | https://2048.wa.darknex.us |" >> $GITHUB_STEP_SUMMARY + echo "| Status | โœ… **LIVE** |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐ŸŽ‰ Production is Live!" >> $GITHUB_STEP_SUMMARY + echo "- ๐ŸŽฎ [Play the game](https://2048.wa.darknex.us)" >> $GITHUB_STEP_SUMMARY + echo "- ๐Ÿงช [Run smoke tests](https://github.com/${{ github.repository }}/actions/workflows/smoke-test.yml)" >> $GITHUB_STEP_SUMMARY + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Set up kubectl + uses: azure/setup-kubectl@v3 + with: + version: 'v1.28.0' + + - name: Configure kubectl + run: | + echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig + export KUBECONFIG=kubeconfig + + - name: Update image in manifests + run: | + TAG="${{ github.event.release.tag_name || github.event.inputs.tag }}" + sed -i "s|ghcr.io/ghndrx/k8s-game-2048:v1.0.0|${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG}|g" manifests/prod/service.yml + + - name: Deploy to production with blue-green strategy + run: | + export KUBECONFIG=kubeconfig + + # Deploy new revision with 0% traffic + kubectl apply -f manifests/prod/ + + # Wait for new revision to be ready + kubectl wait --for=condition=Ready ksvc/game-2048-prod -n game-2048-prod --timeout=300s + + # Get the latest revision name + LATEST_REVISION=$(kubectl get ksvc game-2048-prod -n game-2048-prod -o jsonpath='{.status.latestReadyRevisionName}') + + # Gradually shift traffic (10%, 50%, 100%) + kubectl patch ksvc game-2048-prod -n game-2048-prod --type='merge' -p='{"spec":{"traffic":[{"revisionName":"'$LATEST_REVISION'","percent":10},{"latestRevision":false,"percent":90}]}}' + sleep 60 + + kubectl patch ksvc game-2048-prod -n game-2048-prod --type='merge' -p='{"spec":{"traffic":[{"revisionName":"'$LATEST_REVISION'","percent":50},{"latestRevision":false,"percent":50}]}}' + sleep 60 + + kubectl patch ksvc game-2048-prod -n game-2048-prod --type='merge' -p='{"spec":{"traffic":[{"latestRevision":true,"percent":100}]}}' + + - name: Run production health checks + run: | + # Wait for traffic to stabilize + sleep 60 + # Test the production URL + curl -f https://2048.wa.darknex.us/ || exit 1 + # Additional health checks can be added here + + - name: Get service URL + id: get-url + run: | + export KUBECONFIG=kubeconfig + SERVICE_URL=$(kubectl get ksvc game-2048-prod -n game-2048-prod -o jsonpath='{.status.url}') + echo "service_url=$SERVICE_URL" >> $GITHUB_OUTPUT + echo "๐Ÿš€ Production service deployed at: $SERVICE_URL" + + - name: Set up Node.js for testing + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: tests/package.json + + - name: Install Playwright dependencies + run: | + cd tests + npm install + npx playwright install --with-deps + + - name: Run production smoke tests + run: | + cd tests + BASE_URL=${{ steps.get-url.outputs.service_url }} npx playwright test environment.spec.ts + env: + CI: true + + - name: Run full test suite + run: | + cd tests + BASE_URL=${{ steps.get-url.outputs.service_url }} npx playwright test + env: + CI: true + + - name: Upload production test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-results-production-${{ github.sha }}-${{ github.run_number }} + path: | + tests/playwright-report/ + tests/test-results/ + retention-days: 90 + + - name: Upload production screenshots + uses: actions/upload-artifact@v4 + if: always() + with: + name: screenshots-production-${{ github.sha }}-${{ github.run_number }} + path: tests/test-results/**/*.png + retention-days: 90 + + - name: Production health validation + run: | + # Extended health checks for production + echo "๐Ÿ” Running production health checks..." + + # Test main URL + curl -f https://2048.wa.darknex.us/ || exit 1 + + # Test health endpoint + curl -f https://2048.wa.darknex.us/health || exit 1 + + # Check response times + RESPONSE_TIME=$(curl -o /dev/null -s -w '%{time_total}' https://2048.wa.darknex.us/) + echo "Response time: ${RESPONSE_TIME}s" + + # Fail if response time > 3 seconds + if (( $(echo "$RESPONSE_TIME > 3.0" | bc -l) )); then + echo "โŒ Response time too slow: ${RESPONSE_TIME}s" + exit 1 + fi + + echo "โœ… All production health checks passed!" diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 0000000..c0cd265 --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,273 @@ +name: Deploy to Staging + +on: + workflow_dispatch: + inputs: + image_tag: + description: 'Image tag to deploy (default: latest)' + required: false + default: 'latest' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ghndrx/k8s-game-2048 + +jobs: + deploy-staging: + name: Deploy to Staging + runs-on: ubuntu-latest + environment: staging + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up kubectl + uses: azure/setup-kubectl@v3 + with: + version: 'latest' + + - name: Configure kubectl + run: | + mkdir -p ~/.kube + echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + - name: Set image tag + run: | + IMAGE_TAG="${{ github.event.inputs.image_tag || 'latest' }}" + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV + echo "Deploying image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$IMAGE_TAG" + + - name: Deploy to staging + run: | + echo "๐Ÿš€ Deploying to staging environment..." + + # Apply namespace + kubectl apply -f manifests/staging/namespace.yml + + # Ensure GHCR secret exists + if kubectl get secret ghcr-secret -n default &>/dev/null; then + echo "๐Ÿ” Copying GHCR secret to staging namespace..." + kubectl get secret ghcr-secret -o yaml | \ + sed 's/namespace: default/namespace: game-2048-staging/' | \ + sed '/resourceVersion:/d' | \ + sed '/uid:/d' | \ + sed '/creationTimestamp:/d' | \ + kubectl apply -f - + fi + + # Update image in service and deploy + kubectl patch ksvc game-2048-staging -n game-2048-staging --type merge -p '{"spec":{"template":{"spec":{"containers":[{"image":"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}","imagePullPolicy":"Always"}]}}}}' + + echo "โณ Waiting for deployment to be ready..." + kubectl wait --for=condition=Ready ksvc/game-2048-staging -n game-2048-staging --timeout=300s || echo "โš ๏ธ Service may still be starting" + + - name: Verify deployment + run: | + echo "๐Ÿ“Š Deployment status:" + kubectl get ksvc -n game-2048-staging + + echo "" + echo "โœ… Staging deployment completed!" + echo "๐ŸŒ Available at: https://2048-staging.wa.darknex.us" + + - name: Run smoke test + run: | + echo "๐Ÿงช Running smoke test..." + sleep 30 + + for i in {1..5}; do + echo "Attempt $i/5..." + # Test canonical domain first + if curl -s --max-time 30 https://game-2048-staging.game-2048-staging.staging.wa.darknex.us/ | grep -q "2048"; then + echo "โœ… Canonical domain smoke test passed!" + break + # Fallback to custom domain + elif curl -s --max-time 30 https://2048-staging.wa.darknex.us/ | grep -q "2048"; then + echo "โœ… Custom domain smoke test passed!" + break + elif [ $i -eq 5 ]; then + echo "โš ๏ธ Smoke test failed after 5 attempts" + exit 1 + else + echo "Retrying in 30 seconds..." + sleep 30 + fi + done + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=sha,prefix=staging- + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Set up kubectl + uses: azure/setup-kubectl@v3 + with: + version: 'v1.28.0' + + - name: Configure kubectl + run: | + echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig + export KUBECONFIG=kubeconfig + + - name: Update image in manifests + run: | + sed -i "s|ghcr.io/ghndrx/k8s-game-2048:staging|${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:staging-${{ github.sha }}|g" manifests/staging/service.yml + + - name: Deploy to staging + run: | + export KUBECONFIG=kubeconfig + kubectl apply -f manifests/staging/ + + - name: Wait for deployment + run: | + export KUBECONFIG=kubeconfig + kubectl wait --for=condition=Ready ksvc/game-2048-staging -n game-2048-staging --timeout=300s + + - name: Run smoke tests + run: | + # Wait a bit for the service to be fully ready + sleep 30 + # Test the staging URL + curl -f https://2048-staging.wa.darknex.us/ || exit 1 + + - name: Get service URL + id: get-url + run: | + export KUBECONFIG=kubeconfig + SERVICE_URL=$(kubectl get ksvc game-2048-staging -n game-2048-staging -o jsonpath='{.status.url}') + echo "service_url=$SERVICE_URL" >> $GITHUB_OUTPUT + echo "๐Ÿš€ Staging service deployed at: $SERVICE_URL" + + - name: Set up Node.js for testing + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: tests/package.json + + - name: Install Playwright dependencies + run: | + cd tests + npm install + npx playwright install --with-deps + + - name: Run comprehensive Playwright tests + run: | + cd tests + BASE_URL=${{ steps.get-url.outputs.service_url }} npx playwright test + env: + CI: true + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-results-staging-${{ github.sha }}-${{ github.run_number }} + path: | + tests/playwright-report/ + tests/test-results/ + retention-days: 30 + + - name: Upload screenshots + uses: actions/upload-artifact@v4 + if: always() + with: + name: screenshots-staging-${{ github.sha }}-${{ github.run_number }} + path: tests/test-results/**/*.png + retention-days: 30 + + promote-to-master: + needs: build-and-deploy + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/staging' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create Pull Request to Master + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + push-to-fork: false + branch: auto-promote/staging-to-master-${{ github.sha }} + base: master + title: "๐Ÿš€ Auto-promote: Deploy ${{ github.sha }} to production" + body: | + ## ๐Ÿš€ Auto-promotion from Staging + + **Source**: `staging` branch + **Commit**: ${{ github.sha }} + **Triggered by**: @${{ github.actor }} + + ### โœ… Staging Tests Passed + - Basic functionality tests + - Gameplay mechanics tests + - Visual regression tests + - Environment validation tests + + ### ๐ŸŽฏ Changes in this promotion: + ${{ github.event.head_commit.message }} + + This PR was automatically created after successful deployment and testing in the staging environment. + + **Staging URL**: https://2048-staging.wa.darknex.us + **Will deploy to**: https://2048.wa.darknex.us + + โš ๏ธ **Production Deployment** - Please review carefully before merging! + labels: | + auto-promotion + production + deploy + needs-review + + - name: Create Pull Request to Master + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + push-to-fork: false + branch: auto-promote/staging-to-master-${{ github.sha }} + base: master + title: "๐Ÿš€ Auto-promote: Deploy ${{ github.sha }} to production" + body: | + ## ๐Ÿš€ Auto-promotion from Staging + + **Source**: `staging` branch + **Commit**: ${{ github.sha }} + **Triggered by**: @${{ github.actor }} + + ### โœ… Staging Tests Passed + - Basic functionality tests + - Gameplay mechanics tests + - Visual regression tests + - Environment validation tests + - Performance tests + + ### ๐ŸŽฏ Changes in this promotion: + ${{ github.event.head_commit.message }} + + This PR was automatically created after successful deployment and testing in the staging environment. + + **Staging URL**: https://2048-staging.wa.darknex.us + **Will deploy to**: https://2048.wa.darknex.us + + โš ๏ธ **Production Deployment** - Please review carefully before merging! + labels: | + auto-promotion + production + deploy + needs-review diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml new file mode 100644 index 0000000..bd74f79 --- /dev/null +++ b/.github/workflows/pr-validation.yml @@ -0,0 +1,109 @@ +name: Pull Request Validation + +on: + pull_request: + branches: [ develop, staging, master ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ghndrx/k8s-game-2048 + +jobs: + validate: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: tests/package.json + + - name: Install Playwright dependencies + run: | + cd tests + npm install + npx playwright install --with-deps + + - name: Start local server + run: | + npm start & + sleep 5 + curl -f http://localhost:8080/ || exit 1 + env: + CI: true + + - name: Run Playwright tests locally + run: | + cd tests + BASE_URL=http://localhost:8080 npx playwright test + env: + CI: true + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: false + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:pr-${{ github.event.number }} + + - name: Validate Kubernetes manifests + run: | + # Install kubeval for validation + curl -L https://github.com/instrumenta/kubeval/releases/latest/download/kubeval-linux-amd64.tar.gz | tar xz + sudo mv kubeval /usr/local/bin + + # Validate all manifests + kubeval manifests/dev/*.yml + kubeval manifests/staging/*.yml + kubeval manifests/prod/*.yml + + - name: Upload PR test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: pr-test-results-${{ github.event.number }} + path: | + tests/playwright-report/ + tests/test-results/ + retention-days: 7 + + - name: Comment PR with test results + uses: actions/github-script@v7 + if: always() + with: + script: | + const { owner, repo } = context.repo; + const issue_number = context.payload.pull_request.number; + + const comment = `## ๐Ÿงช PR Validation Results + + **Tests Status**: ${{ job.status == 'success' && 'โœ… Passed' || 'โŒ Failed' }} + **Commit**: ${{ github.event.pull_request.head.sha }} + + ### Test Summary: + - โœ… Local server started successfully + - โœ… Playwright tests executed + - โœ… Docker image built + - โœ… Kubernetes manifests validated + + ### Artifacts: + - Test results and screenshots are available in the workflow artifacts + + ${{ job.status == 'success' && '๐Ÿš€ Ready for merge!' || 'โš ๏ธ Please check the failed tests and fix issues before merging.' }} + `; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: comment + }); diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml new file mode 100644 index 0000000..3a5a41c --- /dev/null +++ b/.github/workflows/smoke-test.yml @@ -0,0 +1,231 @@ +name: Smoke Tests + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + schedule: + # Run smoke tests every 6 hours + - cron: '0 */6 * * *' + workflow_dispatch: + inputs: + environment: + description: 'Environment to test (dev, staging, prod, all)' + required: false + default: 'all' + type: choice + options: + - all + - dev + - staging + - prod + +jobs: + smoke-tests: + name: Smoke Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + environment: + - ${{ github.event.inputs.environment == 'all' && 'dev' || github.event.inputs.environment || 'dev' }} + - ${{ github.event.inputs.environment == 'all' && 'staging' || '' }} + - ${{ github.event.inputs.environment == 'all' && 'prod' || '' }} + exclude: + - environment: '' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set environment variables + run: | + case "${{ matrix.environment }}" in + dev) + echo "CANONICAL_DOMAIN=game-2048-dev.game-2048-dev.dev.wa.darknex.us" >> $GITHUB_ENV + echo "ENV_NAME=development" >> $GITHUB_ENV + ;; + staging) + echo "CANONICAL_DOMAIN=game-2048-staging.game-2048-staging.staging.wa.darknex.us" >> $GITHUB_ENV + echo "ENV_NAME=staging" >> $GITHUB_ENV + ;; + prod) + echo "CANONICAL_DOMAIN=game-2048-prod.game-2048-prod.wa.darknex.us" >> $GITHUB_ENV + echo "ENV_NAME=production" >> $GITHUB_ENV + ;; + esac + + - name: Test Canonical Domain Accessibility + run: | + echo "๐ŸŽฏ Testing canonical Knative domain: ${{ env.CANONICAL_DOMAIN }}" + + # Test HTTPS access to canonical domain + response_code=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 https://${{ env.CANONICAL_DOMAIN }}/) + echo "Canonical domain HTTP response code: $response_code" + + if [ "$response_code" != "200" ]; then + echo "โŒ Canonical domain ${{ env.CANONICAL_DOMAIN }} returned HTTP $response_code" + exit 1 + else + echo "โœ… Canonical domain ${{ env.CANONICAL_DOMAIN }} is accessible" + fi + + - name: Test Content Validation + run: | + echo "๐Ÿ“„ Testing content validation on canonical domain: ${{ env.CANONICAL_DOMAIN }}" + + # Download the page content from canonical domain + content=$(curl -s -L --max-time 30 https://${{ env.CANONICAL_DOMAIN }}/) + + # Check if it contains expected 2048 game elements + if echo "$content" | grep -q "2048"; then + echo "โœ… Page contains '2048' title" + else + echo "โŒ Page does not contain '2048' title" + exit 1 + fi + + if echo "$content" | grep -q "HOW TO PLAY"; then + echo "โœ… Page contains game instructions" + else + echo "โŒ Page does not contain game instructions" + exit 1 + fi + + if echo "$content" | grep -q "Environment.*${{ env.ENV_NAME }}"; then + echo "โœ… Page shows correct environment: ${{ env.ENV_NAME }}" + else + echo "โš ๏ธ Environment indicator not found or incorrect" + # Don't fail on this, just warn + fi + + # Check if CSS and JS files are referenced + if echo "$content" | grep -q "style.css"; then + echo "โœ… CSS file is referenced" + else + echo "โŒ CSS file is not referenced" + exit 1 + fi + + if echo "$content" | grep -q "script.js"; then + echo "โœ… JavaScript file is referenced" + else + echo "โŒ JavaScript file is not referenced" + exit 1 + fi + + - name: Test Performance + run: | + echo "โšก Testing performance for canonical domain: ${{ env.CANONICAL_DOMAIN }}" + + # Measure response time on canonical domain + response_time=$(curl -s -o /dev/null -w "%{time_total}" -L --max-time 30 https://${{ env.CANONICAL_DOMAIN }}/) + echo "Canonical domain response time: ${response_time}s" + + # Check if response time is reasonable (under 10 seconds) + if (( $(echo "$response_time < 10.0" | bc -l) )); then + echo "โœ… Canonical domain response time is acceptable" + else + echo "โš ๏ธ Canonical domain response time is slow: ${response_time}s" + fi + + # Check content size + content_size=$(curl -s -L --max-time 30 https://${{ env.CANONICAL_DOMAIN }}/ | wc -c) + echo "Content size: $content_size bytes" + + if [ $content_size -gt 1000 ]; then + echo "โœ… Content size is reasonable" + else + echo "โŒ Content size is too small: $content_size bytes" + exit 1 + fi + + test-canonical-domains: + name: Canonical Domain Infrastructure Tests + runs-on: ubuntu-latest + if: github.event.inputs.environment == 'all' || github.event.inputs.environment == '' || github.event_name != 'workflow_dispatch' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Test Canonical Domain DNS Resolution + run: | + echo "๐ŸŒ Testing canonical domain DNS resolution" + + canonical_domains=( + "game-2048-dev.game-2048-dev.dev.wa.darknex.us" + "game-2048-staging.game-2048-staging.staging.wa.darknex.us" + "game-2048-prod.game-2048-prod.wa.darknex.us" + ) + + for domain in "${canonical_domains[@]}"; do + echo "Testing DNS for canonical domain: $domain" + ip=$(dig +short $domain) + if [ -n "$ip" ]; then + echo "โœ… $domain resolves to: $ip" + else + echo "โŒ $domain does not resolve" + exit 1 + fi + done + + - name: Test Canonical Domain SSL Certificate Chain + run: | + echo "๐Ÿ” Testing SSL certificate chains for canonical domains" + + canonical_domains=( + "game-2048-dev.game-2048-dev.dev.wa.darknex.us" + "game-2048-staging.game-2048-staging.staging.wa.darknex.us" + "game-2048-prod.game-2048-prod.wa.darknex.us" + ) + + for domain in "${canonical_domains[@]}"; do + echo "Testing SSL chain for canonical domain: $domain" + + # Test certificate chain + chain_result=$(echo | openssl s_client -servername $domain -connect $domain:443 -verify_return_error 2>&1) + + if echo "$chain_result" | grep -q "Verify return code: 0"; then + echo "โœ… $domain has valid SSL certificate chain" + else + echo "โŒ $domain has invalid SSL certificate chain" + echo "$chain_result" + exit 1 + fi + done + + summary: + name: Test Summary + runs-on: ubuntu-latest + needs: [smoke-tests, test-canonical-domains] + if: always() + + steps: + - name: Check test results + run: | + if [ "${{ needs.smoke-tests.result }}" = "success" ] && [ "${{ needs.test-canonical-domains.result }}" = "success" ]; then + echo "โœ… All tests passed successfully!" + echo "๐ŸŽฎ 2048 game is working correctly across all environments using canonical Knative domains" + else + echo "โŒ Some tests failed" + echo "Smoke tests: ${{ needs.smoke-tests.result }}" + echo "Canonical domain tests: ${{ needs.test-canonical-domains.result }}" + exit 1 + fi + + - name: Post summary + if: always() + run: | + echo "## Test Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Test Type | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-----------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Smoke Tests | ${{ needs.smoke-tests.result == 'success' && 'โœ… Passed' || 'โŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Canonical Domain Tests | ${{ needs.test-canonical-domains.result == 'success' && 'โœ… Passed' || 'โŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Tested Canonical Domains" >> $GITHUB_STEP_SUMMARY + echo "- ๐Ÿงช Development: https://game-2048-dev.game-2048-dev.dev.wa.darknex.us" >> $GITHUB_STEP_SUMMARY + echo "- ๐ŸŽญ Staging: https://game-2048-staging.game-2048-staging.staging.wa.darknex.us" >> $GITHUB_STEP_SUMMARY + echo "- ๐Ÿš€ Production: https://game-2048-prod.game-2048-prod.wa.darknex.us" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36fdcba --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Docker +.dockerignore + +# Kubernetes +*.bak + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# Build outputs +dist/ +build/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5da0d1c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,161 @@ +# Contributing to K8s Game 2048 + +Thank you for considering contributing to this project! This guide will help you get started. + +## Development Setup + +1. **Clone the repository** + ```bash + git clone https://github.com/your-username/k8s-game-2048.git + cd k8s-game-2048 + ``` + +2. **Local Development** + ```bash + # Start local development server + npm start + # Or with Python + python3 -m http.server 8080 --directory src + ``` + +3. **Build Docker Image** + ```bash + npm run build + # Or + docker build -t k8s-game-2048 . + ``` + +## Git Workflow + +We use a GitFlow-inspired workflow: + +- **`master`** - Production-ready code, deployed to production automatically +- **`staging`** - Staging branch, deployed to staging environment automatically +- **`develop`** - Development branch, deployed to dev environment automatically +- **`feature/*`** - Feature branches, create PR to develop +- **`hotfix/*`** - Hotfix branches, create PR to master +- **`release/*`** - Release branches for production deployment + +### Branch Protection Rules + +- **`master`**: Requires PR review, all checks must pass +- **`staging`**: Requires PR review, all checks must pass +- **`develop`**: Requires PR review, all checks must pass + +## Deployment Environments + +| Environment | Branch | Domain | Auto-Deploy | +|-------------|--------|---------|------------| +| Development | `develop` | `2048-dev.wa.darknex.us` | โœ… | +| Staging | `staging` | `2048-staging.wa.darknex.us` | โœ… | +| Production | `master` | `2048.wa.darknex.us` | โœ… | + +## Making Changes + +### For New Features + +1. Create a feature branch from `develop`: + ```bash + git checkout develop + git pull origin develop + git checkout -b feature/your-feature-name + ``` + +2. Make your changes and commit: + ```bash + git add . + git commit -m "feat: add your feature description" + ``` + +3. Push and create a PR to `develop`: + ```bash + git push origin feature/your-feature-name + ``` + +### For Bug Fixes + +1. Create a hotfix branch from `master`: + ```bash + git checkout master + git pull origin master + git checkout -b hotfix/fix-description + ``` + +2. Make your changes and create PR to `master` + +## Commit Convention + +We use [Conventional Commits](https://www.conventionalcommits.org/): + +- `feat:` - New features +- `fix:` - Bug fixes +- `docs:` - Documentation changes +- `style:` - Code style changes (formatting, etc.) +- `refactor:` - Code refactoring +- `test:` - Adding or updating tests +- `chore:` - Maintenance tasks + +## Testing + +### Local Testing +```bash +# Test the game locally +npm start +open http://localhost:8080 +``` + +### Kubernetes Testing +```bash +# Deploy to development environment +kubectl apply -f manifests/dev/ + +# Check deployment status +kubectl get ksvc -n game-2048-dev + +# Test the deployed service +curl -f https://2048-dev.wa.darknex.us/ +``` + +## Code Style + +- Use 2 spaces for indentation +- Use meaningful variable and function names +- Add comments for complex logic +- Keep functions small and focused + +## Pull Request Process + +1. **Title**: Use conventional commit format +2. **Description**: + - What changes were made? + - Why were they made? + - How to test the changes? +3. **Testing**: Ensure all environments work correctly +4. **Documentation**: Update README if needed + +## Release Process + +1. Create a release branch from `master`: + ```bash + git checkout master + git pull origin master + git checkout -b release/v1.1.0 + ``` + +2. Update version in `package.json` + +3. Create PR to `master` + +4. After merge, create a GitHub release with tag + +5. Production deployment will trigger automatically + +## Getting Help + +- Open an issue for bugs or feature requests +- Start a discussion for questions +- Check existing issues before creating new ones + +## Code of Conduct + +Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..358d4e2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM nginx:alpine + +# Copy the 2048 game files +COPY src/ /usr/share/nginx/html/ + +# Copy custom nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port 8080 (Knative standard) +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/ || exit 1 + +# Run nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/LICENSE b/LICENSE index 34d9000..a1a6d2d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Greg Hendrickson +Copyright (c) 2025 K8s Game 2048 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee889d0 --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +# K8s Game 2048 + +A Kubernetes deployment of the classic 2048 game using Knative Serving with Istio service mesh and nginx ingress for SSL termination. + +## Features + +- **Knative Serving**: Serverless deployment with scale-to-zero capability +- **Istio Service Mesh**: Advanced traffic management and observability +- **nginx Ingress**: SSL termination and traffic routing +- **Multi-environment**: Development, Staging, and Production deployments +- **Custom Domains with SSL**: Environment-specific HTTPS domains +- **GitOps Workflow**: Complete CI/CD pipeline with GitHub Actions + +## Environments + +- **Development**: `https://2048-dev.wa.darknex.us` +- **Staging**: `https://2048-staging.wa.darknex.us` +- **Production**: `https://2048.wa.darknex.us` + +## Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Internet โ”‚ โ”‚ nginx โ”‚ โ”‚ Istio โ”‚ โ”‚ Knative โ”‚ +โ”‚ โ”‚โ”€โ”€โ”€โ–ถโ”‚ Ingress โ”‚โ”€โ”€โ”€โ–ถโ”‚ Gateway โ”‚โ”€โ”€โ”€โ–ถโ”‚ Service โ”‚ +โ”‚ โ”‚ โ”‚ (SSL Term.) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ–ผ โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ cert-managerโ”‚ โ”‚ 2048 Game โ”‚ + โ”‚ Let's Encryptโ”‚ โ”‚ Container โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## Quick Start + +### Prerequisites + +- Kubernetes cluster (1.21+) with k3s or similar +- Knative Serving installed +- Istio service mesh installed +- nginx ingress controller installed +- cert-manager for SSL certificates +- kubectl configured +- Domain DNS configured to point to your cluster IP + +### Installation + +1. Clone the repository: +```bash +git clone https://github.com/ghndrx/k8s-game-2048.git +cd k8s-game-2048 +``` + +2. Deploy all environments: +```bash +./scripts/deploy.sh all +``` + +3. Or deploy a specific environment: +```bash +./scripts/deploy.sh dev # Development only +./scripts/deploy.sh staging # Staging only +./scripts/deploy.sh prod # Production only +``` + +3. Deploy to staging: +```bash +kubectl apply -f manifests/staging/ +``` + +4. Deploy to production: +```bash +kubectl apply -f manifests/prod/ +``` + +## Project Structure + +``` +k8s-game-2048/ +โ”œโ”€โ”€ README.md +โ”œโ”€โ”€ Dockerfile +โ”œโ”€โ”€ .github/ +โ”‚ โ””โ”€โ”€ workflows/ +โ”‚ โ”œโ”€โ”€ deploy-dev.yml +โ”‚ โ”œโ”€โ”€ deploy-staging.yml +โ”‚ โ””โ”€โ”€ deploy-prod.yml +โ”œโ”€โ”€ manifests/ +โ”‚ โ”œโ”€โ”€ dev/ +โ”‚ โ”‚ โ”œโ”€โ”€ namespace.yml +โ”‚ โ”‚ โ”œโ”€โ”€ service.yml +โ”‚ โ”‚ โ””โ”€โ”€ domain-mapping.yml +โ”‚ โ”œโ”€โ”€ staging/ +โ”‚ โ”‚ โ”œโ”€โ”€ namespace.yml +โ”‚ โ”‚ โ”œโ”€โ”€ service.yml +โ”‚ โ”‚ โ””โ”€โ”€ domain-mapping.yml +โ”‚ โ””โ”€โ”€ prod/ +โ”‚ โ”œโ”€โ”€ namespace.yml +โ”‚ โ”œโ”€โ”€ service.yml +โ”‚ โ””โ”€โ”€ domain-mapping.yml +โ”œโ”€โ”€ scripts/ +โ”‚ โ”œโ”€โ”€ setup-knative.sh +โ”‚ โ”œโ”€โ”€ setup-kourier.sh +โ”‚ โ””โ”€โ”€ deploy.sh +โ””โ”€โ”€ src/ + โ””โ”€โ”€ (2048 game files) +``` + +## Deployment + +The application uses Knative Serving with the following features: + +- **Scale to Zero**: Automatically scales down to 0 when not in use +- **Auto-scaling**: Scales up based on incoming requests +- **Blue-Green Deployments**: Safe deployment strategy with traffic splitting +- **Custom Domains**: Environment-specific domain mapping + +## Monitoring + +Each environment includes: + +- Knative Service status monitoring +- Request metrics via Knative +- Custom domain health checks + +## Contributing + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/curl-format.txt b/curl-format.txt new file mode 100644 index 0000000..b10dc8c --- /dev/null +++ b/curl-format.txt @@ -0,0 +1,10 @@ + time_namelookup: %{time_namelookup}\n + time_connect: %{time_connect}\n + time_appconnect: %{time_appconnect}\n + time_pretransfer: %{time_pretransfer}\n + time_redirect: %{time_redirect}\n + time_starttransfer: %{time_starttransfer}\n + ----------\n + time_total: %{time_total}\n + response_code: %{response_code}\n + size_download: %{size_download}\n diff --git a/docs/BRANCHING.md b/docs/BRANCHING.md new file mode 100644 index 0000000..048a8cb --- /dev/null +++ b/docs/BRANCHING.md @@ -0,0 +1,154 @@ +# Branch Strategy & Deployment Flow + +## Branch Structure + +``` +master (production) +โ”œโ”€โ”€ staging (staging environment) +โ””โ”€โ”€ develop (development environment) + โ”œโ”€โ”€ feature/feature-name + โ”œโ”€โ”€ feature/another-feature + โ””โ”€โ”€ hotfix/urgent-fix +``` + +## Deployment Flow + +### ๐ŸŸข Development Environment +- **Branch**: `develop` +- **Domain**: `2048-dev.wa.darknex.us` +- **Trigger**: Push to `develop` branch +- **Auto-deploy**: โœ… Yes +- **Purpose**: Latest development features, may be unstable + +### ๐ŸŸก Staging Environment +- **Branch**: `staging` +- **Domain**: `2048-staging.wa.darknex.us` +- **Trigger**: Push to `staging` branch +- **Auto-deploy**: โœ… Yes +- **Purpose**: Pre-production testing, stable features + +### ๐Ÿ”ด Production Environment +- **Branch**: `master` +- **Domain**: `2048.wa.darknex.us` +- **Trigger**: Push to `master` branch OR GitHub Release +- **Auto-deploy**: โœ… Yes +- **Purpose**: Live production environment + +## Workflow Examples + +### Adding a New Feature + +```bash +# 1. Start from develop +git checkout develop +git pull origin develop + +# 2. Create feature branch +git checkout -b feature/awesome-new-feature + +# 3. Make changes and commit +git add . +git commit -m "feat: add awesome new feature" + +# 4. Push and create PR to develop +git push origin feature/awesome-new-feature +# Create PR: feature/awesome-new-feature โ†’ develop +``` + +### Promoting to Staging + +```bash +# 1. Merge feature to develop (via PR) +# 2. Test in dev environment: 2048-dev.wa.darknex.us + +# 3. Promote to staging +git checkout staging +git pull origin staging +git merge develop +git push origin staging + +# 4. Test in staging: 2048-staging.wa.darknex.us +``` + +### Deploying to Production + +```bash +# 1. After staging testing passes +git checkout master +git pull origin master +git merge staging +git push origin master + +# 2. Optionally create a release tag +git tag -a v1.0.0 -m "Release version 1.0.0" +git push origin v1.0.0 + +# 3. Production deploys automatically: 2048.wa.darknex.us +``` + +### Hotfix Flow + +```bash +# 1. Create hotfix from master +git checkout master +git pull origin master +git checkout -b hotfix/critical-bug-fix + +# 2. Fix the issue +git add . +git commit -m "fix: resolve critical bug" + +# 3. Push and create PR to master +git push origin hotfix/critical-bug-fix +# Create PR: hotfix/critical-bug-fix โ†’ master + +# 4. After merge, also merge back to develop +git checkout develop +git merge master +git push origin develop +``` + +## CI/CD Pipeline + +### Development Pipeline (`develop` branch) +1. โœ… Build Docker image +2. โœ… Push to GHCR with `develop-{sha}` tag +3. โœ… Deploy to dev namespace +4. โœ… Run basic health checks + +### Staging Pipeline (`staging` branch) +1. โœ… Build Docker image +2. โœ… Push to GHCR with `staging-{sha}` tag +3. โœ… Deploy to staging namespace +4. โœ… Run smoke tests +5. โœ… Health check staging URL + +### Production Pipeline (`master` branch) +1. โœ… Build Docker image +2. โœ… Push to GHCR with version tag +3. โœ… Blue-green deployment to production +4. โœ… Gradual traffic shifting (10% โ†’ 50% โ†’ 100%) +5. โœ… Production health checks +6. โœ… Rollback capability + +## Environment Configuration + +| Environment | Namespace | Min Scale | Max Scale | Scale Down Delay | +|-------------|-----------|-----------|-----------|------------------| +| Development | `game-2048-dev` | 0 | 10 | 30s | +| Staging | `game-2048-staging` | 0 | 20 | 60s | +| Production | `game-2048-prod` | 0 | 50 | 300s | + +## Monitoring & Alerts + +- **Development**: Basic logging, fast iteration +- **Staging**: Full monitoring, performance testing +- **Production**: Full observability, alerting, SLA monitoring + +## Security + +- All images are scanned for vulnerabilities +- Secrets managed via GitHub Secrets +- RBAC configured per namespace +- TLS termination at Kourier gateway +- Auto-TLS via cert-manager and Let's Encrypt diff --git a/docs/SETUP.md b/docs/SETUP.md new file mode 100644 index 0000000..45b6609 --- /dev/null +++ b/docs/SETUP.md @@ -0,0 +1,236 @@ +# Knative & Kourier Setup Guide + +This guide will help you set up Knative Serving with Kourier networking layer on your Kubernetes cluster. + +## Prerequisites + +- Kubernetes cluster (v1.21+) +- kubectl configured and working +- Cluster admin permissions +- LoadBalancer support (cloud provider or MetalLB) + +## Quick Setup + +Run the provided scripts in order: + +```bash +# 1. Install Knative Serving +./scripts/setup-knative.sh + +# 2. Install Kourier networking layer +./scripts/setup-kourier.sh +``` + +## Manual Setup + +If you prefer to install manually: + +### 1. Install Knative Serving + +```bash +# Install CRDs +kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-crds.yaml + +# Install core components +kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-core.yaml + +# Install HPA autoscaler +kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-hpa.yaml +``` + +### 2. Install Kourier + +```bash +# Install Kourier +kubectl apply -f https://github.com/knative/net-kourier/releases/download/knative-v1.12.0/kourier.yaml + +# Configure Knative to use Kourier +kubectl patch configmap/config-network \ + --namespace knative-serving \ + --type merge \ + --patch '{"data":{"ingress-class":"kourier.ingress.networking.knative.dev"}}' +``` + +### 3. Configure Domain + +```bash +# Set your custom domain +kubectl patch configmap/config-domain \ + --namespace knative-serving \ + --type merge \ + --patch '{"data":{"wa.darknex.us":""}}' +``` + +### 4. Set up TLS (Optional but Recommended) + +```bash +# Install cert-manager +kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml + +# Install Knative cert-manager integration +kubectl apply -f https://github.com/knative/net-certmanager/releases/download/knative-v1.12.0/release.yaml + +# Create Let's Encrypt ClusterIssuer +kubectl apply -f - < LoadBalancer IP + 2048-staging.wa.darknex.us -> LoadBalancer IP + 2048.wa.darknex.us -> LoadBalancer IP + *.wa.darknex.us -> LoadBalancer IP (wildcard) + ``` + +## Verification + +Test your setup: + +```bash +# Check Knative Serving +kubectl get pods -n knative-serving + +# Check Kourier +kubectl get pods -n kourier-system + +# Check cert-manager (if installed) +kubectl get pods -n cert-manager + +# Deploy a test service +kubectl apply -f manifests/dev/ + +# Check service status +kubectl get ksvc -n game-2048-dev +``` + +## Troubleshooting + +### Common Issues + +1. **Pods stuck in Pending**: + - Check node resources: `kubectl describe nodes` + - Check PVC status: `kubectl get pvc -A` + +2. **LoadBalancer IP not assigned**: + - Ensure your cluster supports LoadBalancer services + - For local clusters, consider using MetalLB + +3. **TLS certificates not issued**: + - Check cert-manager logs: `kubectl logs -n cert-manager -l app=cert-manager` + - Verify DNS propagation: `dig 2048-dev.wa.darknex.us` + +4. **Service not accessible**: + - Check Kourier gateway logs: `kubectl logs -n kourier-system -l app=3scale-kourier-gateway` + - Verify domain mapping: `kubectl get domainmapping -A` + +### Useful Commands + +```bash +# Check Knative service status +kubectl get ksvc -A + +# Check revisions +kubectl get rev -A + +# Check domain mappings +kubectl get domainmapping -A + +# Check Kourier configuration +kubectl get svc kourier -n kourier-system -o yaml + +# Check Knative configuration +kubectl get cm -n knative-serving + +# Debug service logs +kubectl logs -n -l serving.knative.dev/service= +``` + +## Advanced Configuration + +### Custom Autoscaling + +```yaml +# Add to service annotations +autoscaling.knative.dev/minScale: "0" +autoscaling.knative.dev/maxScale: "100" +autoscaling.knative.dev/target: "70" +autoscaling.knative.dev/scaleDownDelay: "30s" +autoscaling.knative.dev/window: "60s" +``` + +### Traffic Splitting + +```yaml +# In Knative Service spec +traffic: +- percent: 90 + revisionName: myapp-00001 +- percent: 10 + revisionName: myapp-00002 +``` + +### Custom Resource Limits + +```yaml +# In container spec +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 1000m + memory: 512Mi +``` + +## Monitoring + +Consider installing these additional tools: + +- **Knative Monitoring**: `kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/monitoring.yaml` +- **Prometheus**: For metrics collection +- **Grafana**: For visualization +- **Jaeger**: For distributed tracing + +## Next Steps + +1. Deploy the 2048 game: `kubectl apply -f manifests/dev/` +2. Set up monitoring and alerting +3. Configure backup and disaster recovery +4. Implement proper RBAC policies +5. Set up GitOps with ArgoCD or Flux diff --git a/manifests/dev/namespace.yml b/manifests/dev/namespace.yml new file mode 100644 index 0000000..54e4953 --- /dev/null +++ b/manifests/dev/namespace.yml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: game-2048-dev + labels: + environment: development + app: game-2048 diff --git a/manifests/dev/service.yml b/manifests/dev/service.yml new file mode 100644 index 0000000..c762bb3 --- /dev/null +++ b/manifests/dev/service.yml @@ -0,0 +1,26 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: game-2048-dev + namespace: game-2048-dev + labels: + app: game-2048 + environment: development +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/minScale: "0" + autoscaling.knative.dev/maxScale: "10" + autoscaling.knative.dev/target: "100" + spec: + containers: + - image: ghcr.io/ghndrx/k8s-game-2048:latest + imagePullPolicy: Always + ports: + - containerPort: 80 + env: + - name: ENVIRONMENT + value: "development" + imagePullSecrets: + - name: ghcr-secret diff --git a/manifests/istio-gateway.yaml b/manifests/istio-gateway.yaml new file mode 100644 index 0000000..04724b7 --- /dev/null +++ b/manifests/istio-gateway.yaml @@ -0,0 +1,20 @@ +apiVersion: networking.istio.io/v1beta1 +kind: Gateway +metadata: + name: knative-ingress-gateway + namespace: knative-serving + labels: + app.kubernetes.io/component: net-istio + app.kubernetes.io/name: knative-serving + app.kubernetes.io/version: 1.12.0 + networking.knative.dev/ingress-provider: istio +spec: + selector: + istio: ingressgateway + servers: + - hosts: + - '*' + port: + name: http + number: 80 + protocol: HTTP diff --git a/manifests/knative-domain-config.yaml b/manifests/knative-domain-config.yaml new file mode 100644 index 0000000..bf31a33 --- /dev/null +++ b/manifests/knative-domain-config.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-domain + namespace: knative-serving +data: + dev.wa.darknex.us: | + selector: + environment: development + staging.wa.darknex.us: | + selector: + environment: staging + wa.darknex.us: | + selector: + environment: production + autocreate-cluster-domain-claims: "true" diff --git a/manifests/nginx-certificate.yaml b/manifests/nginx-certificate.yaml new file mode 100644 index 0000000..a648690 --- /dev/null +++ b/manifests/nginx-certificate.yaml @@ -0,0 +1,40 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: game-2048-dev-nginx-cert + namespace: default +spec: + secretName: game-2048-dev-nginx-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - "2048-dev.wa.darknex.us" + +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: game-2048-staging-nginx-cert + namespace: default +spec: + secretName: game-2048-staging-nginx-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - "2048-staging.wa.darknex.us" + +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: game-2048-prod-nginx-cert + namespace: default +spec: + secretName: game-2048-prod-nginx-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - "2048.wa.darknex.us" diff --git a/manifests/nginx-to-istio-proxy.yaml b/manifests/nginx-to-istio-proxy.yaml new file mode 100644 index 0000000..720556a --- /dev/null +++ b/manifests/nginx-to-istio-proxy.yaml @@ -0,0 +1,119 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: game-2048-dev-proxy + namespace: default + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_set_header Host game-2048-dev.game-2048-dev.dev.wa.darknex.us; +spec: + ingressClassName: nginx + tls: + - hosts: + - 2048-dev.wa.darknex.us + secretName: game-2048-dev-nginx-tls + rules: + - host: 2048-dev.wa.darknex.us + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: istio-nodeport-service + port: + number: 80 + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: game-2048-staging-proxy + namespace: default + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_set_header Host game-2048-staging.game-2048-staging.staging.wa.darknex.us; +spec: + ingressClassName: nginx + tls: + - hosts: + - 2048-staging.wa.darknex.us + secretName: game-2048-staging-nginx-tls + rules: + - host: 2048-staging.wa.darknex.us + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: istio-nodeport-service + port: + number: 80 + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: game-2048-prod-proxy + namespace: default + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_set_header Host game-2048-prod.game-2048-prod.wa.darknex.us; +spec: + ingressClassName: nginx + tls: + - hosts: + - 2048.wa.darknex.us + secretName: game-2048-prod-nginx-tls + rules: + - host: 2048.wa.darknex.us + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: istio-nodeport-service + port: + number: 80 + +--- +apiVersion: v1 +kind: Service +metadata: + name: istio-nodeport-service + namespace: default +spec: + ports: + - name: http + port: 80 + targetPort: 32135 + protocol: TCP + clusterIP: None + +--- +apiVersion: v1 +kind: Endpoints +metadata: + name: istio-nodeport-service + namespace: default +subsets: +- addresses: + - ip: 192.168.4.134 # Your k3s node IP + ports: + - name: http + port: 32135 diff --git a/manifests/prod/namespace.yml b/manifests/prod/namespace.yml new file mode 100644 index 0000000..465c0b6 --- /dev/null +++ b/manifests/prod/namespace.yml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: game-2048-prod + labels: + environment: production + app: game-2048 diff --git a/manifests/prod/service.yml b/manifests/prod/service.yml new file mode 100644 index 0000000..2d063fe --- /dev/null +++ b/manifests/prod/service.yml @@ -0,0 +1,26 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: game-2048-prod + namespace: game-2048-prod + labels: + app: game-2048 + environment: production +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/minScale: "0" + autoscaling.knative.dev/maxScale: "10" + autoscaling.knative.dev/target: "100" + spec: + containers: + - image: ghcr.io/ghndrx/k8s-game-2048:latest + imagePullPolicy: Always + ports: + - containerPort: 80 + env: + - name: ENVIRONMENT + value: "production" + imagePullSecrets: + - name: ghcr-secret diff --git a/manifests/ssl-certificate.yaml b/manifests/ssl-certificate.yaml new file mode 100644 index 0000000..3daa342 --- /dev/null +++ b/manifests/ssl-certificate.yaml @@ -0,0 +1,14 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: admin@hndrx.co + privateKeySecretRef: + name: letsencrypt-prod + solvers: + - http01: + ingress: + class: nginx diff --git a/manifests/staging/namespace.yml b/manifests/staging/namespace.yml new file mode 100644 index 0000000..11a7855 --- /dev/null +++ b/manifests/staging/namespace.yml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: game-2048-staging + labels: + environment: staging + app: game-2048 diff --git a/manifests/staging/service.yml b/manifests/staging/service.yml new file mode 100644 index 0000000..d9ca382 --- /dev/null +++ b/manifests/staging/service.yml @@ -0,0 +1,26 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: game-2048-staging + namespace: game-2048-staging + labels: + app: game-2048 + environment: staging +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/minScale: "0" + autoscaling.knative.dev/maxScale: "10" + autoscaling.knative.dev/target: "100" + spec: + containers: + - image: ghcr.io/ghndrx/k8s-game-2048:latest + imagePullPolicy: Always + ports: + - containerPort: 80 + env: + - name: ENVIRONMENT + value: "staging" + imagePullSecrets: + - name: ghcr-secret diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..d29470a --- /dev/null +++ b/nginx.conf @@ -0,0 +1,38 @@ +server { + listen 8080; + server_name localhost; + + root /usr/share/nginx/html; + index index.html index.htm; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Main location + location / { + try_files $uri $uri/ /index.html; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..909faf0 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "k8s-game-2048", + "version": "1.0.0", + "description": "2048 game deployed on Kubernetes using Knative Serving with Kourier", + "main": "src/index.html", + "scripts": { + "start": "python3 -m http.server 8080 --directory src", + "build": "docker build -t k8s-game-2048 .", + "deploy:dev": "./scripts/deploy.sh dev", + "deploy:staging": "./scripts/deploy.sh staging", + "deploy:prod": "./scripts/deploy.sh prod", + "setup:knative": "./scripts/setup-knative.sh", + "setup:kourier": "./scripts/setup-kourier.sh" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ghndrx/k8s-game-2048.git" + }, + "keywords": [ + "2048", + "game", + "kubernetes", + "knative", + "kourier", + "serverless", + "scale-to-zero" + ], + "author": "Your Name", + "license": "MIT", + "bugs": { + "url": "https://github.com/ghndrx/k8s-game-2048/issues" + }, + "homepage": "https://github.com/ghndrx/k8s-game-2048#readme", + "devDependencies": {}, + "dependencies": {} +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..ed0418f --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,49 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [ + ['html'], + ['json', { outputFile: 'test-results.json' }], + ['junit', { outputFile: 'test-results.xml' }] + ], + use: { + baseURL: process.env.BASE_URL || 'http://localhost:8080', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + { + name: 'mobile-chrome', + use: { ...devices['Pixel 5'] }, + }, + { + name: 'mobile-safari', + use: { ...devices['iPhone 12'] }, + }, + ], + + webServer: process.env.CI ? undefined : { + command: 'npm start', + port: 8080, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..c7ad294 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# Deployment script for 2048 game with Istio + nginx SSL setup +# Usage: ./deploy.sh [env] where env = dev|staging|prod|all + +set -e + +ENVIRONMENT=${1:-all} +REGISTRY="ghcr.io/ghndrx/k8s-game-2048" + +echo "๐Ÿš€ Deploying 2048 game with Istio + nginx SSL..." +echo "Environment: $ENVIRONMENT" + +# Validate environment +case $ENVIRONMENT in + dev|staging|prod|all) + echo "โœ… Valid environment: $ENVIRONMENT" + ;; + *) + echo "โŒ Invalid environment. Use: dev, staging, prod, or all" + exit 1 + ;; +esac + +# Check dependencies +if ! command -v kubectl &> /dev/null; then + echo "โŒ kubectl is not installed" + exit 1 +fi + +if ! kubectl cluster-info &> /dev/null; then + echo "โŒ Cannot access Kubernetes cluster" + exit 1 +fi + +# Deploy function for a single environment +deploy_env() { + local env=$1 + echo "๐Ÿ“ฆ Deploying $env environment..." + + # Apply namespace + kubectl apply -f manifests/$env/namespace.yml + + # Ensure GHCR secret exists in the namespace + echo "๐Ÿ” Setting up GHCR secret for $env..." + if kubectl get secret ghcr-secret -n default &>/dev/null; then + kubectl get secret ghcr-secret -o yaml | \ + sed "s/namespace: default/namespace: game-2048-$env/" | \ + sed '/resourceVersion:/d' | \ + sed '/uid:/d' | \ + sed '/creationTimestamp:/d' | \ + kubectl apply -f - + else + echo "โš ๏ธ Warning: No GHCR secret found in default namespace" + fi + + # Apply service + kubectl apply -f manifests/$env/service.yml + + # Wait for service to be ready + echo "โณ Waiting for $env service to be ready..." + kubectl wait --for=condition=Ready ksvc/game-2048-$env -n game-2048-$env --timeout=300s || echo "Warning: Service may still be starting" +} + +# Deploy infrastructure (certificates, gateways, etc.) +echo "๐Ÿ—๏ธ Setting up infrastructure..." +kubectl apply -f manifests/ssl-certificate.yaml +kubectl apply -f manifests/nginx-certificate.yaml +kubectl apply -f manifests/knative-domain-config.yaml +kubectl apply -f manifests/istio-gateway.yaml +kubectl apply -f manifests/nginx-to-istio-proxy.yaml + +# Deploy environments +if [ "$ENVIRONMENT" = "all" ]; then + deploy_env "dev" + deploy_env "staging" + deploy_env "prod" +else + deploy_env "$ENVIRONMENT" +fi + +echo "" +echo "โœ… Deployment completed!" +echo "" +echo "๏ฟฝ Your 2048 game is available at:" +if [ "$ENVIRONMENT" = "all" ] || [ "$ENVIRONMENT" = "dev" ]; then + echo " Development: https://2048-dev.wa.darknex.us" +fi +if [ "$ENVIRONMENT" = "all" ] || [ "$ENVIRONMENT" = "staging" ]; then + echo " Staging: https://2048-staging.wa.darknex.us" +fi +if [ "$ENVIRONMENT" = "all" ] || [ "$ENVIRONMENT" = "prod" ]; then + echo " Production: https://2048.wa.darknex.us" +fi +echo "" +echo "๐Ÿ”ง Check status with:" +echo " kubectl get ksvc -A" +echo " kubectl get certificates -A" +echo " kubectl get ingress -A" +echo "" +echo "๐Ÿ“ Architecture: Internet โ†’ nginx (SSL) โ†’ Istio โ†’ Knative" diff --git a/scripts/setup-environments.sh b/scripts/setup-environments.sh new file mode 100644 index 0000000..2b7475f --- /dev/null +++ b/scripts/setup-environments.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Setup GitHub Environments for Protection Rules +# This script documents the manual steps needed in GitHub UI + +echo "๐Ÿ”ง Setting up GitHub Environments for Auto-Promotion Pipeline" +echo "" +echo "๐Ÿ“‹ Manual Steps Required in GitHub Repository Settings:" +echo "" +echo "1. Go to Settings โ†’ Environments" +echo "2. Create 'production-approval' environment" +echo "3. Add required reviewers (yourself)" +echo "4. Set deployment protection rules" +echo "" +echo "Environment Configuration:" +echo "- Environment Name: production-approval" +echo "- Required Reviewers: Repository admins" +echo "- Wait Timer: 0 minutes (immediate on approval)" +echo "- Deployment Branches: main branch only" +echo "" +echo "This ensures production deployments require manual approval" +echo "while dev โ†’ staging promotion happens automatically on successful tests." +echo "" +echo "๐Ÿ”— Navigate to: https://github.com/YOUR_USERNAME/k8s-game-2048/settings/environments" diff --git a/scripts/setup-knative.sh b/scripts/setup-knative.sh new file mode 100755 index 0000000..5437520 --- /dev/null +++ b/scripts/setup-knative.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Setup script for Knative Serving installation +# This script installs Knative Serving on a Kubernetes cluster + +set -e + +echo "๐Ÿš€ Setting up Knative Serving..." + +# Check if kubectl is available +if ! command -v kubectl &> /dev/null; then + echo "โŒ kubectl is not installed. Please install kubectl first." + exit 1 +fi + +# Check if cluster is accessible +if ! kubectl cluster-info &> /dev/null; then + echo "โŒ Cannot access Kubernetes cluster. Please check your kubeconfig." + exit 1 +fi + +# Install Knative Serving CRDs +echo "๐Ÿ“ฆ Installing Knative Serving CRDs..." +kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-crds.yaml + +# Wait for CRDs to be established +echo "โณ Waiting for CRDs to be established..." +kubectl wait --for condition=established --timeout=120s crd/configurations.serving.knative.dev +kubectl wait --for condition=established --timeout=120s crd/revisions.serving.knative.dev +kubectl wait --for condition=established --timeout=120s crd/routes.serving.knative.dev +kubectl wait --for condition=established --timeout=120s crd/services.serving.knative.dev + +# Install Knative Serving core +echo "๐Ÿ“ฆ Installing Knative Serving core..." +kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-core.yaml + +# Wait for Knative Serving to be ready +echo "โณ Waiting for Knative Serving to be ready..." +kubectl wait --for=condition=Ready pod -l app=controller -n knative-serving --timeout=300s +kubectl wait --for=condition=Ready pod -l app=webhook -n knative-serving --timeout=300s + +# Install Knative Serving HPA (Horizontal Pod Autoscaler) +echo "๐Ÿ“ฆ Installing Knative Serving HPA..." +kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-hpa.yaml + +# Configure domain +echo "๐ŸŒ Configuring domain..." +kubectl patch configmap/config-domain \ + --namespace knative-serving \ + --type merge \ + --patch '{"data":{"wa.darknex.us":""}}' + +echo "โœ… Knative Serving installation completed!" +echo "" +echo "Next steps:" +echo "1. Install Kourier as the networking layer: ./setup-kourier.sh" +echo "2. Configure DNS to point your domain to the Kourier LoadBalancer" +echo "3. Deploy your applications using the manifests in this repository" diff --git a/scripts/setup-kourier.sh b/scripts/setup-kourier.sh new file mode 100755 index 0000000..e3c1a47 --- /dev/null +++ b/scripts/setup-kourier.sh @@ -0,0 +1,109 @@ +#!/bin/bash + +# Setup script for Kourier networking layer +# This script installs Kourier as the Knative networking layer + +set -e + +echo "๐Ÿš€ Setting up Kourier networking layer..." + +# Check if kubectl is available +if ! command -v kubectl &> /dev/null; then + echo "โŒ kubectl is not installed. Please install kubectl first." + exit 1 +fi + +# Check if cluster is accessible +if ! kubectl cluster-info &> /dev/null; then + echo "โŒ Cannot access Kubernetes cluster. Please check your kubeconfig." + exit 1 +fi + +# Check if Knative Serving is installed +if ! kubectl get namespace knative-serving &> /dev/null; then + echo "โŒ Knative Serving is not installed. Please run ./setup-knative.sh first." + exit 1 +fi + +# Install Kourier +echo "๐Ÿ“ฆ Installing Kourier..." +kubectl apply -f https://github.com/knative/net-kourier/releases/download/knative-v1.12.0/kourier.yaml + +# Wait for Kourier to be ready +echo "โณ Waiting for Kourier to be ready..." +kubectl wait --for=condition=Ready pod -l app=3scale-kourier-gateway -n kourier-system --timeout=300s + +# Configure Knative to use Kourier +echo "๐Ÿ”ง Configuring Knative to use Kourier..." +kubectl patch configmap/config-network \ + --namespace knative-serving \ + --type merge \ + --patch '{"data":{"ingress-class":"kourier.ingress.networking.knative.dev"}}' + +# Get the external IP of Kourier +echo "๐Ÿ” Getting Kourier LoadBalancer details..." +kubectl get svc kourier -n kourier-system + +# Configure auto-TLS (optional) +echo "๐Ÿ” Configuring auto-TLS..." +kubectl patch configmap/config-network \ + --namespace knative-serving \ + --type merge \ + --patch '{"data":{"autoTLS":"Enabled","httpProtocol":"Redirected"}}' + +# Install cert-manager for TLS (optional but recommended) +echo "๐Ÿ“ฆ Installing cert-manager for TLS..." +kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml + +# Wait for cert-manager to be ready +echo "โณ Waiting for cert-manager to be ready..." +kubectl wait --for=condition=Ready pod -l app=cert-manager -n cert-manager --timeout=300s +kubectl wait --for=condition=Ready pod -l app=cainjector -n cert-manager --timeout=300s +kubectl wait --for=condition=Ready pod -l app=webhook -n cert-manager --timeout=300s + +# Install Knative cert-manager integration +echo "๐Ÿ“ฆ Installing Knative cert-manager integration..." +kubectl apply -f https://github.com/knative/net-certmanager/releases/download/knative-v1.12.0/release.yaml + +# Create ClusterIssuer for Let's Encrypt +echo "๐Ÿ” Creating Let's Encrypt ClusterIssuer..." +cat </dev/null || echo "000") + + if [ "$response" = "$expected_status" ]; then + echo -e "${GREEN}โœ… PASS${NC} (HTTP $response)" + return 0 + else + echo -e "${RED}โŒ FAIL${NC} (HTTP $response, expected $expected_status)" + return 1 + fi +} + +# Function to test SSL certificate +test_ssl_cert() { + local domain=$1 + echo -n " Testing SSL certificate for $domain... " + + if echo | openssl s_client -servername "$domain" -connect "$domain:443" -verify_return_error &>/dev/null; then + echo -e "${GREEN}โœ… VALID${NC}" + return 0 + else + echo -e "${RED}โŒ INVALID${NC}" + return 1 + fi +} + +# Function to test content +test_content() { + local url=$1 + local expected_text=$2 + local test_name=$3 + + echo -n " Testing $test_name content... " + + if curl -s --max-time $TIMEOUT "$url" | grep -q "$expected_text"; then + echo -e "${GREEN}โœ… FOUND${NC}" + return 0 + else + echo -e "${RED}โŒ NOT FOUND${NC}" + return 1 + fi +} + +# Function to test Kubernetes resources +test_k8s_resources() { + local env=$1 + echo -e "${YELLOW}๐Ÿ“‹ Testing Kubernetes Resources for $env${NC}" + + # Test namespace + echo -n " Checking namespace game-2048-$env... " + if kubectl get namespace "game-2048-$env" &>/dev/null; then + echo -e "${GREEN}โœ… EXISTS${NC}" + else + echo -e "${RED}โŒ MISSING${NC}" + return 1 + fi + + # Test Knative service + echo -n " Checking Knative service... " + if kubectl get ksvc "game-2048-$env" -n "game-2048-$env" &>/dev/null; then + local status=$(kubectl get ksvc "game-2048-$env" -n "game-2048-$env" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}') + if [ "$status" = "True" ]; then + echo -e "${GREEN}โœ… READY${NC}" + else + echo -e "${YELLOW}โณ NOT READY${NC} (Status: $status)" + fi + else + echo -e "${RED}โŒ MISSING${NC}" + return 1 + fi + + # Test GHCR secret + echo -n " Checking GHCR secret... " + if kubectl get secret ghcr-secret -n "game-2048-$env" &>/dev/null; then + echo -e "${GREEN}โœ… EXISTS${NC}" + else + echo -e "${RED}โŒ MISSING${NC}" + return 1 + fi +} + +# Function to test ingress +test_ingress() { + echo -e "${YELLOW}๐ŸŒ Testing Ingress Configuration${NC}" + + # Test nginx ingress controller + echo -n " Checking nginx ingress controller... " + if kubectl get pods -n ingress-nginx | grep -q "ingress-nginx-controller.*Running"; then + echo -e "${GREEN}โœ… RUNNING${NC}" + else + echo -e "${RED}โŒ NOT RUNNING${NC}" + return 1 + fi + + # Test Istio ingress gateway + echo -n " Checking Istio ingress gateway... " + if kubectl get pods -n istio-system | grep -q "istio-ingressgateway.*Running"; then + echo -e "${GREEN}โœ… RUNNING${NC}" + else + echo -e "${RED}โŒ NOT RUNNING${NC}" + return 1 + fi + + # Test cert-manager + echo -n " Checking cert-manager... " + if kubectl get pods -n cert-manager | grep -q "cert-manager.*Running"; then + echo -e "${GREEN}โœ… RUNNING${NC}" + else + echo -e "${RED}โŒ NOT RUNNING${NC}" + return 1 + fi +} + +# Function to test certificates +test_certificates() { + echo -e "${YELLOW}๐Ÿ”’ Testing SSL Certificates${NC}" + + for i in "${!ENVIRONMENTS[@]}"; do + local env="${ENVIRONMENTS[$i]}" + local domain="${DOMAINS[$i]}" + + echo -n " Checking certificate for $domain... " + local cert_status=$(kubectl get certificate "game-2048-$env-nginx-cert" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "Unknown") + + if [ "$cert_status" = "True" ]; then + echo -e "${GREEN}โœ… READY${NC}" + test_ssl_cert "$domain" + else + echo -e "${RED}โŒ NOT READY${NC} (Status: $cert_status)" + fi + done +} + +# Main test execution +main() { + local total_tests=0 + local passed_tests=0 + + # Test infrastructure + test_ingress + test_certificates + + # Test each environment + for i in "${!ENVIRONMENTS[@]}"; do + local env="${ENVIRONMENTS[$i]}" + local domain="${DOMAINS[$i]}" + local canonical_domain="${CANONICAL_DOMAINS[$i]}" + + echo "" + echo -e "${BLUE}๐ŸŽฎ Testing $env Environment${NC}" + echo "Domain: https://$domain" + echo "Canonical: https://$canonical_domain" + echo "----------------------------------------" + + # Test Kubernetes resources + if test_k8s_resources "$env"; then + ((total_tests++)) + ((passed_tests++)) + else + ((total_tests++)) + fi + + # Test custom domain accessibility + if test_http_response "https://$domain" "200\|301\|302" "custom domain"; then + ((total_tests++)) + ((passed_tests++)) + else + ((total_tests++)) + fi + + # Test canonical domain accessibility + if test_http_response "https://$canonical_domain" "200" "canonical domain"; then + ((total_tests++)) + ((passed_tests++)) + else + ((total_tests++)) + fi + + # Test content + if test_content "https://$canonical_domain" "2048" "game content"; then + ((total_tests++)) + ((passed_tests++)) + else + ((total_tests++)) + fi + + # Test environment-specific content + local env_name="" + case $env in + "dev") env_name="development" ;; + "staging") env_name="staging" ;; + "prod") env_name="Production" ;; + esac + + if test_content "https://$canonical_domain" "$env_name" "environment detection"; then + ((total_tests++)) + ((passed_tests++)) + else + ((total_tests++)) + fi + done + + echo "" + echo "==================================" + echo -e "${BLUE}๐Ÿ“Š Test Summary${NC}" + echo "Total Tests: $total_tests" + echo -e "Passed: ${GREEN}$passed_tests${NC}" + echo -e "Failed: ${RED}$((total_tests - passed_tests))${NC}" + + if [ $passed_tests -eq $total_tests ]; then + echo -e "${GREEN}๐ŸŽ‰ All tests passed!${NC}" + exit 0 + else + echo -e "${RED}โŒ Some tests failed${NC}" + exit 1 + fi +} + +# Run main function +main "$@" diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..0e21715 --- /dev/null +++ b/src/index.html @@ -0,0 +1,82 @@ + + + + + + 2048 Game - Knative Edition + + + + +
+
+

2048

+
+
+
+
SCORE
+
0
+
+
+
BEST
+
0
+
+
+
+ +
+

+ HOW TO PLAY: Use your arrow keys to move the tiles. + When two tiles with the same number touch, they merge into one! +

+ +
+ +
+
+

+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+

Knative Edition: This game is deployed using Knative Serving with scale-to-zero capabilities on Kubernetes!

+

Environment: Production

+
+
+ + + + diff --git a/src/script.js b/src/script.js new file mode 100644 index 0000000..73ff3df --- /dev/null +++ b/src/script.js @@ -0,0 +1,348 @@ +// 2048 Game JavaScript - Knative Edition + +class Game2048 { + constructor() { + this.grid = []; + this.score = 0; + this.best = localStorage.getItem('best2048') || 0; + this.gameWon = false; + this.gameOver = false; + this.keepPlaying = false; + + this.init(); + this.setupEventListeners(); + this.setEnvironment(); + } + + init() { + this.grid = [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0] + ]; + + this.score = 0; + this.gameWon = false; + this.gameOver = false; + this.keepPlaying = false; + + this.updateScore(); + this.addRandomTile(); + this.addRandomTile(); + this.updateDisplay(); + } + + setEnvironment() { + const envElement = document.getElementById('environment'); + const envBadge = document.getElementById('env-badge'); + + // Try to detect environment from hostname + const hostname = window.location.hostname; + let environment = 'production'; + + if (hostname.includes('dev')) { + environment = 'development'; + } else if (hostname.includes('staging')) { + environment = 'staging'; + } + + envElement.textContent = environment.charAt(0).toUpperCase() + environment.slice(1); + envBadge.textContent = environment; + envBadge.className = `environment-badge ${environment}`; + } + + setupEventListeners() { + document.addEventListener('keydown', (e) => this.handleKeyPress(e)); + document.getElementById('restart-button').addEventListener('click', () => this.restart()); + document.getElementById('keep-playing-button').addEventListener('click', () => this.keepPlayingGame()); + document.getElementById('retry-button').addEventListener('click', () => this.restart()); + + // Touch/swipe support for mobile + let startX, startY; + + document.addEventListener('touchstart', (e) => { + startX = e.touches[0].clientX; + startY = e.touches[0].clientY; + }); + + document.addEventListener('touchend', (e) => { + if (!startX || !startY) return; + + const endX = e.changedTouches[0].clientX; + const endY = e.changedTouches[0].clientY; + + const diffX = startX - endX; + const diffY = startY - endY; + + if (Math.abs(diffX) > Math.abs(diffY)) { + if (diffX > 0) { + this.move('left'); + } else { + this.move('right'); + } + } else { + if (diffY > 0) { + this.move('up'); + } else { + this.move('down'); + } + } + }); + } + + handleKeyPress(e) { + if (this.gameOver && !this.keepPlaying) return; + + switch (e.code) { + case 'ArrowUp': + e.preventDefault(); + this.move('up'); + break; + case 'ArrowDown': + e.preventDefault(); + this.move('down'); + break; + case 'ArrowLeft': + e.preventDefault(); + this.move('left'); + break; + case 'ArrowRight': + e.preventDefault(); + this.move('right'); + break; + } + } + + move(direction) { + const previousGrid = this.grid.map(row => [...row]); + let moved = false; + + switch (direction) { + case 'left': + moved = this.moveLeft(); + break; + case 'right': + moved = this.moveRight(); + break; + case 'up': + moved = this.moveUp(); + break; + case 'down': + moved = this.moveDown(); + break; + } + + if (moved) { + this.addRandomTile(); + this.updateDisplay(); + this.checkGameState(); + } + } + + moveLeft() { + let moved = false; + for (let row = 0; row < 4; row++) { + const newRow = this.slideArray(this.grid[row]); + if (!this.arraysEqual(this.grid[row], newRow)) { + moved = true; + this.grid[row] = newRow; + } + } + return moved; + } + + moveRight() { + let moved = false; + for (let row = 0; row < 4; row++) { + const reversed = [...this.grid[row]].reverse(); + const newRow = this.slideArray(reversed).reverse(); + if (!this.arraysEqual(this.grid[row], newRow)) { + moved = true; + this.grid[row] = newRow; + } + } + return moved; + } + + moveUp() { + let moved = false; + for (let col = 0; col < 4; col++) { + const column = [this.grid[0][col], this.grid[1][col], this.grid[2][col], this.grid[3][col]]; + const newColumn = this.slideArray(column); + if (!this.arraysEqual(column, newColumn)) { + moved = true; + for (let row = 0; row < 4; row++) { + this.grid[row][col] = newColumn[row]; + } + } + } + return moved; + } + + moveDown() { + let moved = false; + for (let col = 0; col < 4; col++) { + const column = [this.grid[0][col], this.grid[1][col], this.grid[2][col], this.grid[3][col]]; + const reversed = [...column].reverse(); + const newColumn = this.slideArray(reversed).reverse(); + if (!this.arraysEqual(column, newColumn)) { + moved = true; + for (let row = 0; row < 4; row++) { + this.grid[row][col] = newColumn[row]; + } + } + } + return moved; + } + + slideArray(arr) { + const filtered = arr.filter(val => val !== 0); + const missing = 4 - filtered.length; + const zeros = Array(missing).fill(0); + const newArray = filtered.concat(zeros); + + for (let i = 0; i < 3; i++) { + if (newArray[i] !== 0 && newArray[i] === newArray[i + 1]) { + newArray[i] *= 2; + newArray[i + 1] = 0; + this.score += newArray[i]; + } + } + + const filtered2 = newArray.filter(val => val !== 0); + const missing2 = 4 - filtered2.length; + const zeros2 = Array(missing2).fill(0); + return filtered2.concat(zeros2); + } + + arraysEqual(a, b) { + return JSON.stringify(a) === JSON.stringify(b); + } + + addRandomTile() { + const emptyCells = []; + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + if (this.grid[row][col] === 0) { + emptyCells.push({row, col}); + } + } + } + + if (emptyCells.length > 0) { + const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)]; + this.grid[randomCell.row][randomCell.col] = Math.random() < 0.9 ? 2 : 4; + } + } + + updateDisplay() { + const container = document.getElementById('tile-container'); + container.innerHTML = ''; + + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + if (this.grid[row][col] !== 0) { + const tile = document.createElement('div'); + tile.className = `tile tile-${this.grid[row][col]}`; + tile.textContent = this.grid[row][col]; + tile.style.left = `${col * 121.25}px`; + tile.style.top = `${row * 121.25}px`; + + if (this.grid[row][col] > 2048) { + tile.className = 'tile tile-super'; + } + + container.appendChild(tile); + } + } + } + } + + updateScore() { + document.getElementById('score').textContent = this.score; + + if (this.score > this.best) { + this.best = this.score; + localStorage.setItem('best2048', this.best); + } + + document.getElementById('best').textContent = this.best; + } + + checkGameState() { + this.updateScore(); + + // Check for 2048 tile (game won) + if (!this.gameWon && !this.keepPlaying) { + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + if (this.grid[row][col] === 2048) { + this.gameWon = true; + this.showMessage('You Win!', 'game-won'); + return; + } + } + } + } + + // Check for game over + if (this.isGameOver()) { + this.gameOver = true; + this.showMessage('Game Over!', 'game-over'); + } + } + + isGameOver() { + // Check for empty cells + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + if (this.grid[row][col] === 0) { + return false; + } + } + } + + // Check for possible merges + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + const current = this.grid[row][col]; + if ( + (row < 3 && current === this.grid[row + 1][col]) || + (col < 3 && current === this.grid[row][col + 1]) + ) { + return false; + } + } + } + + return true; + } + + showMessage(text, className) { + const messageElement = document.getElementById('game-message'); + messageElement.querySelector('p').textContent = text; + messageElement.className = `game-message ${className}`; + messageElement.style.display = 'block'; + } + + hideMessage() { + const messageElement = document.getElementById('game-message'); + messageElement.style.display = 'none'; + } + + restart() { + this.hideMessage(); + this.init(); + } + + keepPlayingGame() { + this.hideMessage(); + this.keepPlaying = true; + } +} + +// Initialize the game when the page loads +document.addEventListener('DOMContentLoaded', () => { + new Game2048(); +}); diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..4434194 --- /dev/null +++ b/src/style.css @@ -0,0 +1,382 @@ +/* 2048 Game CSS - Knative Edition */ + +html, body { + margin: 0; + padding: 20px; + background: #faf8ef; + color: #776e65; + font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif; + font-size: 18px; +} + +body { + margin: 80px 0; +} + +.heading { + margin-bottom: 30px; +} + +h1.title { + font-size: 80px; + font-weight: bold; + margin: 0; + display: inline-block; +} + +.container { + width: 500px; + margin: 0 auto; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.header h1 { + color: #776e65; + font-size: 80px; + font-weight: bold; + margin: 0; +} + +.environment-badge { + padding: 8px 16px; + border-radius: 20px; + font-size: 12px; + font-weight: bold; + text-transform: uppercase; + color: white; + margin-left: 20px; +} + +.environment-badge.development { + background: #ff6b6b; +} + +.environment-badge.staging { + background: #ffa726; +} + +.environment-badge.production { + background: #66bb6a; +} + +.scores-container { + display: flex; + gap: 10px; +} + +.score-container { + position: relative; + display: inline-block; + background: #bbada0; + padding: 10px 20px; + font-size: 25px; + height: 60px; + line-height: 47px; + font-weight: bold; + border-radius: 3px; + color: white; + text-align: center; + min-width: 80px; +} + +.score-title { + position: absolute; + width: 100%; + top: 10px; + left: 0; + text-transform: uppercase; + font-size: 13px; + line-height: 13px; + text-align: center; + color: #eee4da; +} + +.score { + font-size: 25px; +} + +.above-game { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.game-intro { + line-height: 1.65; + margin: 0; + flex: 1; + margin-right: 20px; +} + +.restart-button { + display: inline-block; + background: #8f7a66; + border-radius: 3px; + padding: 0 20px; + text-decoration: none; + color: #f9f6f2; + height: 40px; + line-height: 42px; + border: none; + cursor: pointer; + font-size: 18px; +} + +.restart-button:hover { + background: #9f8a76; +} + +.game-container { + position: relative; + padding: 15px; + cursor: default; + user-select: none; + touch-action: none; + background: #bbada0; + border-radius: 10px; + width: 500px; + height: 500px; + box-sizing: border-box; +} + +.game-message { + display: none; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: rgba(255, 255, 255, 0.73); + z-index: 100; + text-align: center; + border-radius: 10px; +} + +.game-message p { + font-size: 60px; + font-weight: bold; + height: 60px; + line-height: 60px; + margin-top: 150px; +} + +.game-message .lower { + display: block; + margin-top: 30px; +} + +.game-message a { + display: inline-block; + background: #8f7a66; + border-radius: 3px; + padding: 0 20px; + text-decoration: none; + color: #f9f6f2; + height: 40px; + line-height: 42px; + margin-left: 9px; +} + +.game-won { + background: rgba(237, 194, 46, 0.5); + color: #f9f6f2; +} + +.game-won .game-message p { + color: #f9f6f2; +} + +.game-over { + background: rgba(238, 228, 218, 0.73); + color: #776e65; +} + +.game-over .game-message p { + color: #776e65; +} + +.grid-container { + position: absolute; + z-index: 1; +} + +.grid-row { + margin-bottom: 15px; +} + +.grid-row:last-child { + margin-bottom: 0; +} + +.grid-cell { + width: 106.25px; + height: 106.25px; + background: rgba(238, 228, 218, 0.35); + border-radius: 6px; + margin-right: 15px; + float: left; +} + +.grid-cell:last-child { + margin-right: 0; +} + +.tile-container { + position: absolute; + z-index: 2; +} + +.tile { + width: 106.25px; + height: 106.25px; + background: #eee4da; + color: #776e65; + border-radius: 6px; + font-weight: bold; + text-align: center; + vertical-align: middle; + line-height: 106.25px; + font-size: 55px; + position: absolute; + transition: 0.15s ease-in-out; + transform-origin: center center; +} + +.tile-2 { background: #eee4da; color: #776e65; } +.tile-4 { background: #ede0c8; color: #776e65; } +.tile-8 { color: #f9f6f2; background: #f2b179; } +.tile-16 { color: #f9f6f2; background: #f59563; } +.tile-32 { color: #f9f6f2; background: #f67c5f; } +.tile-64 { color: #f9f6f2; background: #f65e3b; } +.tile-128 { color: #f9f6f2; background: #edcf72; font-size: 45px; } +.tile-256 { color: #f9f6f2; background: #edcc61; font-size: 45px; } +.tile-512 { color: #f9f6f2; background: #edc850; font-size: 45px; } +.tile-1024 { color: #f9f6f2; background: #edc53f; font-size: 35px; } +.tile-2048 { color: #f9f6f2; background: #edc22e; font-size: 35px; } + +.tile-super { color: #f9f6f2; background: #3c3a32; font-size: 30px; } + +.tile-new { + animation: appear 200ms ease-in-out; + animation-fill-mode: backwards; +} + +.tile-merged { + z-index: 20; + animation: pop 200ms ease-in-out; + animation-fill-mode: backwards; +} + +@keyframes appear { + 0% { + opacity: 0; + transform: scale(0); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes pop { + 0% { + transform: scale(0); + } + 50% { + transform: scale(1.2); + } + 100% { + transform: scale(1); + } +} + +.game-explanation { + margin-top: 30px; + text-align: center; + color: #776e65; +} + +.game-explanation p { + margin: 10px 0; +} + +.keep-playing-button, .retry-button { + display: inline-block; + background: #8f7a66; + border-radius: 3px; + padding: 0 20px; + text-decoration: none; + color: #f9f6f2; + height: 40px; + line-height: 42px; + border: none; + cursor: pointer; + font-size: 18px; + margin: 0 5px; +} + +.keep-playing-button:hover, .retry-button:hover { + background: #9f8a76; +} + +/* Responsive design */ +@media screen and (max-width: 520px) { + .container { + width: 280px; + margin: 0 auto; + } + + .header h1 { + font-size: 50px; + } + + .scores-container { + flex-direction: column; + gap: 5px; + } + + .above-game { + flex-direction: column; + align-items: stretch; + gap: 15px; + } + + .game-container { + width: 280px; + height: 280px; + padding: 10px; + } + + .grid-cell { + width: 60px; + height: 60px; + margin-right: 10px; + margin-bottom: 10px; + } + + .tile { + width: 60px; + height: 60px; + line-height: 60px; + font-size: 35px; + } + + .tile-128, .tile-256, .tile-512 { + font-size: 25px; + } + + .tile-1024, .tile-2048 { + font-size: 20px; + } + + .tile-super { + font-size: 18px; + } +} diff --git a/tests/basic.spec.ts b/tests/basic.spec.ts new file mode 100644 index 0000000..f94e8c1 --- /dev/null +++ b/tests/basic.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; + +test.describe('2048 Game - Basic Functionality', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.waitForLoadState('networkidle'); + }); + + test('should load the game successfully', async ({ page }) => { + // Check title + await expect(page).toHaveTitle(/2048/); + + // Check main elements are present + await expect(page.locator('h1')).toContainText('2048'); + await expect(page.locator('.game-container')).toBeVisible(); + await expect(page.locator('.grid-container')).toBeVisible(); + + // Check score displays + await expect(page.locator('#score')).toBeVisible(); + await expect(page.locator('#best')).toBeVisible(); + }); + + test('should show environment badge', async ({ page }) => { + const envBadge = page.locator('#env-badge'); + await expect(envBadge).toBeVisible(); + + // Should have one of the environment classes + const badgeClass = await envBadge.getAttribute('class'); + expect(badgeClass).toMatch(/(development|staging|production)/); + }); + + test('should have initial tiles on game start', async ({ page }) => { + // Should have at least 2 tiles initially + const tiles = page.locator('.tile'); + await expect(tiles).toHaveCount(2); + + // Tiles should have values 2 or 4 + const tileTexts = await tiles.allTextContents(); + tileTexts.forEach(text => { + expect(['2', '4']).toContain(text); + }); + }); + + test('should restart game when restart button is clicked', async ({ page }) => { + const restartButton = page.locator('#restart-button'); + await expect(restartButton).toBeVisible(); + + // Click restart + await restartButton.click(); + + // Should have exactly 2 tiles after restart + const tiles = page.locator('.tile'); + await expect(tiles).toHaveCount(2); + + // Score should be reset to 0 + await expect(page.locator('#score')).toHaveText('0'); + }); +}); diff --git a/tests/environment.spec.ts b/tests/environment.spec.ts new file mode 100644 index 0000000..ea72d76 --- /dev/null +++ b/tests/environment.spec.ts @@ -0,0 +1,75 @@ +import { test, expect } from '@playwright/test'; + +test.describe('2048 Game - Environment Tests', () => { + test('should display correct environment in development', async ({ page }) => { + // This test will run when BASE_URL contains 'dev' + const baseUrl = process.env.BASE_URL || ''; + test.skip(!baseUrl.includes('dev'), 'Development environment test'); + + await page.goto('/'); + + const envElement = page.locator('#environment'); + await expect(envElement).toContainText('Development'); + + const envBadge = page.locator('#env-badge'); + await expect(envBadge).toHaveClass(/development/); + }); + + test('should display correct environment in staging', async ({ page }) => { + const baseUrl = process.env.BASE_URL || ''; + test.skip(!baseUrl.includes('staging'), 'Staging environment test'); + + await page.goto('/'); + + const envElement = page.locator('#environment'); + await expect(envElement).toContainText('Staging'); + + const envBadge = page.locator('#env-badge'); + await expect(envBadge).toHaveClass(/staging/); + }); + + test('should display correct environment in production', async ({ page }) => { + const baseUrl = process.env.BASE_URL || ''; + test.skip(baseUrl.includes('dev') || baseUrl.includes('staging'), 'Production environment test'); + + await page.goto('/'); + + const envElement = page.locator('#environment'); + await expect(envElement).toContainText('Production'); + + const envBadge = page.locator('#env-badge'); + await expect(envBadge).toHaveClass(/production/); + }); + + test('should have working health endpoint', async ({ request }) => { + const response = await request.get('/health'); + expect(response.status()).toBe(200); + + const text = await response.text(); + expect(text).toContain('healthy'); + }); + + test('should load all assets successfully', async ({ page }) => { + const responses: any[] = []; + + page.on('response', response => { + responses.push(response); + }); + + await page.goto('/'); + await page.waitForLoadState('networkidle'); + + // Check that all resources loaded successfully + const failedResponses = responses.filter(response => response.status() >= 400); + expect(failedResponses.length).toBe(0); + }); + + test('should have correct security headers', async ({ request }) => { + const response = await request.get('/'); + + // Check for basic security headers + expect(response.headers()['x-frame-options']).toBeDefined(); + expect(response.headers()['x-content-type-options']).toBeDefined(); + expect(response.headers()['x-xss-protection']).toBeDefined(); + }); +}); diff --git a/tests/gameplay.spec.ts b/tests/gameplay.spec.ts new file mode 100644 index 0000000..7c20ff6 --- /dev/null +++ b/tests/gameplay.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@playwright/test'; + +test.describe('2048 Game - Gameplay Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.waitForLoadState('networkidle'); + }); + + test('should move tiles with arrow keys', async ({ page }) => { + // Get initial tile positions + const initialTiles = await page.locator('.tile').all(); + const initialPositions = []; + + for (const tile of initialTiles) { + const style = await tile.getAttribute('style'); + initialPositions.push(style); + } + + // Press arrow key to move tiles + await page.keyboard.press('ArrowRight'); + await page.waitForTimeout(500); // Wait for animation + + // Check that tiles moved or new tile appeared + const newTiles = await page.locator('.tile').all(); + expect(newTiles.length).toBeGreaterThanOrEqual(2); + + // At least one tile should have moved or new tile should appear + let tilesChanged = newTiles.length > initialTiles.length; + + if (!tilesChanged) { + for (let i = 0; i < Math.min(newTiles.length, initialPositions.length); i++) { + const newStyle = await newTiles[i].getAttribute('style'); + if (newStyle !== initialPositions[i]) { + tilesChanged = true; + break; + } + } + } + + expect(tilesChanged).toBe(true); + }); + + test('should handle touch/swipe on mobile', async ({ page, isMobile }) => { + test.skip(!isMobile, 'Touch test only for mobile'); + + const gameContainer = page.locator('.game-container'); + + // Simulate swipe right + await gameContainer.touchStart([{ x: 100, y: 200 }]); + await gameContainer.touchEnd([{ x: 300, y: 200 }]); + + await page.waitForTimeout(500); + + // Should have tiles after swipe + const tiles = page.locator('.tile'); + await expect(tiles).toHaveCount.atLeast(2); + }); + + test('should update score when tiles merge', async ({ page }) => { + // This test might need multiple moves to get mergeable tiles + // For now, just verify score element updates + const scoreElement = page.locator('#score'); + const initialScore = await scoreElement.textContent(); + + // Try multiple moves to potentially trigger a merge + for (let i = 0; i < 10; i++) { + await page.keyboard.press('ArrowRight'); + await page.waitForTimeout(200); + await page.keyboard.press('ArrowDown'); + await page.waitForTimeout(200); + await page.keyboard.press('ArrowLeft'); + await page.waitForTimeout(200); + await page.keyboard.press('ArrowUp'); + await page.waitForTimeout(200); + + const currentScore = await scoreElement.textContent(); + if (currentScore !== initialScore) { + expect(parseInt(currentScore || '0')).toBeGreaterThan(parseInt(initialScore || '0')); + break; + } + } + }); +}); diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 0000000..7bf20b0 --- /dev/null +++ b/tests/package.json @@ -0,0 +1,14 @@ +{ + "name": "playwright-tests", + "version": "1.0.0", + "description": "Playwright tests for 2048 game", + "scripts": { + "test": "playwright test", + "test:headed": "playwright test --headed", + "test:debug": "playwright test --debug", + "test:ui": "playwright test --ui" + }, + "devDependencies": { + "@playwright/test": "^1.40.0" + } +} diff --git a/tests/visual.spec.ts b/tests/visual.spec.ts new file mode 100644 index 0000000..811ac6a --- /dev/null +++ b/tests/visual.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '@playwright/test'; + +test.describe('2048 Game - Visual Tests & Screenshots', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.waitForLoadState('networkidle'); + }); + + test('should match initial game state screenshot', async ({ page }) => { + // Wait for game to fully load + await page.waitForSelector('.tile', { timeout: 5000 }); + + // Take screenshot of initial game state + await expect(page).toHaveScreenshot('initial-game-state.png', { + fullPage: true, + animations: 'disabled' + }); + }); + + test('should match game container layout', async ({ page }) => { + const gameContainer = page.locator('.game-container'); + await expect(gameContainer).toHaveScreenshot('game-container.png', { + animations: 'disabled' + }); + }); + + test('should match header with scores', async ({ page }) => { + const header = page.locator('.header'); + await expect(header).toHaveScreenshot('header-scores.png', { + animations: 'disabled' + }); + }); + + test('should match environment badge', async ({ page }) => { + const envBadge = page.locator('#env-badge'); + await expect(envBadge).toHaveScreenshot('environment-badge.png', { + animations: 'disabled' + }); + }); + + test('should display correctly on mobile', async ({ page, isMobile }) => { + test.skip(!isMobile, 'Mobile test only'); + + await expect(page).toHaveScreenshot('mobile-game.png', { + fullPage: true, + animations: 'disabled' + }); + }); + + test('should show game over state', async ({ page }) => { + // Try to fill the board quickly (this is a simplified approach) + // In a real scenario, we might need to manipulate the game state directly + + // Take screenshot of current state for comparison + await expect(page).toHaveScreenshot('game-in-progress.png', { + fullPage: true, + animations: 'disabled' + }); + }); +});