diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..ffb7254 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,220 @@ +# Deploy Pipeline +name: Automated Deployment + +on: + push: + branches: + - main + +# Ensure only one deployment runs at a time +concurrency: + group: deployment + cancel-in-progress: false + +env: + REGISTRY: ghcr.io + BACKEND_IMAGE: ghcr.io/${{ github.repository_owner }}/pixel-to-pattern-backend + FRONTEND_IMAGE: ghcr.io/${{ github.repository_owner }}/pixel-to-pattern-frontend + +jobs: + + # Build and Push Docker Images to GHCR + build-and-push: + name: Build and Push Docker Images + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + outputs: + backend_image: ${{ steps.meta-backend.outputs.tags }} + frontend_image: ${{ steps.meta-frontend.outputs.tags }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GHCR_TOKEN }} + + # Backend Image + - name: Extract metadata for backend + id: meta-backend + uses: docker/metadata-action@v5 + with: + images: ${{ env.BACKEND_IMAGE }} + tags: | + type=raw,value=latest + type=sha,prefix= + + - name: Build and push backend image + uses: docker/build-push-action@v5 + with: + context: ./server + push: true + tags: ${{ steps.meta-backend.outputs.tags }} + labels: ${{ steps.meta-backend.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Frontend Image + - name: Extract metadata for frontend + id: meta-frontend + uses: docker/metadata-action@v5 + with: + images: ${{ env.FRONTEND_IMAGE }} + tags: | + type=raw,value=latest + type=sha,prefix= + + - name: Build and push frontend image + uses: docker/build-push-action@v5 + with: + context: ./client + push: true + tags: ${{ steps.meta-frontend.outputs.tags }} + labels: ${{ steps.meta-frontend.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Deploy to VM + deploy: + name: Deploy to VM + runs-on: ubuntu-latest + needs: build-and-push + environment: production + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Deploy to VM via SSH + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.VM_HOST }} + username: ${{ secrets.VM_USERNAME }} + password: ${{ secrets.VM_PASSWORD }} + port: ${{ secrets.VM_SSH_PORT || 22 }} + script: | + set -e + + echo "--- Starting deployment ---" + echo "Timestamp: $(date)" + + # Navigate to project directory + cd ${{ secrets.VM_PROJECT_PATH }} + + # Log in to GHCR + echo ${{ secrets.GHCR_TOKEN }} | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin + + # Pull latest images + echo "--- Pulling latest images ---" + docker compose -f docker-compose.deploy.yml pull + + # Stop current containers gracefully + echo "--- Stopping current containers ---" + docker compose -f docker-compose.deploy.yml down --timeout 30 + + # Start new containers + echo "--- Starting new containers ---" + docker compose -f docker-compose.deploy.yml up -d + + # Clean up old images + echo "--- Cleaning up old images ---" + docker image prune -f + + echo "--- Deployment complete ---" + + - name: Verify deployment + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.VM_HOST }} + username: ${{ secrets.VM_USERNAME }} + password: ${{ secrets.VM_PASSWORD }} + port: ${{ secrets.VM_SSH_PORT || 22 }} + script: | + set -e + + echo "--- Verifying deployment ---" + + # Wait for containers to be ready + sleep 10 + + # Check if containers are running + cd ${{ secrets.VM_PROJECT_PATH }} + + echo "--- Container status ---" + docker compose -f docker-compose.deploy.yml ps + + # Check container health + RUNNING_CONTAINERS=$(docker compose -f docker-compose.deploy.yml ps --status running -q | wc -l) + EXPECTED_CONTAINERS=$(docker compose -f docker-compose.deploy.yml config --services | wc -l) + + echo "Running containers: $RUNNING_CONTAINERS" + echo "Expected containers: $EXPECTED_CONTAINERS" + + if [ "$RUNNING_CONTAINERS" -lt "$EXPECTED_CONTAINERS" ]; then + echo "ERROR: Not all containers are running!" + docker compose -f docker-compose.deploy.yml logs --tail=50 + exit 1 + fi + + # Health check - verify frontend is responding + echo "--- Health check ---" + if curl -sSf http://localhost:3001 > /dev/null 2>&1; then + echo "PASSED! Frontend is responding" + else + echo "FAILED! Frontend health check failed (may still be starting)" + fi + + # Check if backend container is running (no endpoint check) + if docker compose -f docker-compose.deploy.yml ps backend | grep -q "Up"; then + echo "PASSED! Backend container is running" + else + echo "FAILED! Backend container is not running!" + exit 1 + fi + + echo "--- Deployment verified successfully ---" + + - name: Deployment summary + run: | + echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "PASSED! **Deployment completed successfully**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + echo "- **Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "- **Triggered by:** ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY + echo "- **Time:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY + + + # Rollback on Failure + rollback: + name: Rollback on Failure + runs-on: ubuntu-latest + needs: deploy + if: failure() + + steps: + - name: Notify about failed deployment + run: | + echo "## FAILED! Deployment Failed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The deployment to production has failed." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Manual intervention may be required.**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "To rollback manually:" >> $GITHUB_STEP_SUMMARY + echo "1. SSH into the VM" >> $GITHUB_STEP_SUMMARY + echo "2. Navigate to the project directory" >> $GITHUB_STEP_SUMMARY + echo "3. Run: \`docker compose -f docker-compose.deploy.yml down\`" >> $GITHUB_STEP_SUMMARY + echo "4. Pull the previous working image tag" >> $GITHUB_STEP_SUMMARY + echo "5. Run: \`docker compose -f docker-compose.deploy.yml up -d\`" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..22d31fa --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,175 @@ +# Testing Pipeline + +name: Automated Testing Pipeline + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + + + +jobs: + # BACKEND TESTS + backend-unit-tests: + name: Backend Unit tests + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Test Containers + run: docker compose -f docker-compose.test.yml build --no-cache backend-unit-tests + + - name: Run backend unit tests + run: | + docker compose -f docker-compose.test.yml up backend-unit-tests \ + --abort-on-container-exit \ + --exit-code-from backend-unit-tests + + - name: Cleanup + if: always() + run: docker compose -f docker-compose.test.yml down -v + + # FRONTEND TESTS + frontend-unit-tests: + name: Frontend Unit tests + runs-on: ubuntu-latest + + steps: + - name: Check repo + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build test containers (if needed) + run: docker compose -f docker-compose.test.yml build --no-cache db-test backend-integration + + - name: Run integration tests + run: | + docker compose -f docker-compose.test.yml up db-test backend-integration \ + --abort-on-container-exit \ + --exit-code-from backend-integration + + - name: Cleanup + if: always() + run: docker compose -f docker-compose.test.yml down -v + + # E2E Tests (Cypress) + e2e-tests: + name: End-to-End Tests (Cypress) + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Create .env file for Docker compose + run: | + cat << EOF > .env + DB_USER=test_user + DB_PASSWORD=test_password + DB_HOST=db + DB_DATABASE=pixel_to_pattern_test + DB_PORT=3306 + SERVER_PORT=3000 + EOF + + - name: Start app stack + run: | + docker compose up -d --build + # Wait for services to be healthy + echo "Waiting for services to start..." + sleep 30 + + - name: Check service health + run: | + docker compose ps + # Verify frontend is accessible + curl --retry 10 --retry-delay 5 --retry-connrefused http://localhost:3001 || true + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'npm' + + - name: Install dependencies + run: | + npm install cypress + npm run cypress:run + env: + CYPRESS_BASE_URL: http://localhost:3001 + + - name: Upload Cypress screenshots on failure + uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: cypress/screenshots + retention-days: 7 + + - name: Upload Cypress videos + uses: actions/upload-artifact@v4 + if: always() + with: + name: cypress-videos + path: cypress/videos + retention-days: 7 + + - name: Cleanup + if: always() + run: docker compose down -v + + # TEST SUMMARY + test-summary: + name: Test Summary + runs-on: ubuntu-latest + needs: [backend-unit-tests, frontend-unit-tests, e2e-tests] + + steps: + - name: Display test results + run: | + echo "## Test Results Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ needs.backend-unit-tests.result }}" == "success" ]; then + echo "PASSED! Backend Unit Tests: Passed" >> $GITHUB_STEP_SUMMARY + else + echo "FAILED! Backend Unit Tests: ${{ needs.backend-unit-tests.result }}" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.frontend-unit-tests.result }}" == "success" ]; then + echo "PASSED! Frontend Unit Tests: Passed" >> $GITHUB_STEP_SUMMARY + else + echo "FAILED! Frontend Unit Tests: ${{ needs.frontend-unit-tests.result }}" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.integration-tests.result }}" == "success" ]; then + echo "PASSED! Integration Tests: Passed" >> $GITHUB_STEP_SUMMARY + else + echo "FAILED! Integration Tests: ${{ needs.integration-tests.result }}" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.e2e-tests.result }}" == "success" ]; then + echo "PASSED! E2E Tests: Passed" >> $GITHUB_STEP_SUMMARY + else + echo "FAILED! E2E Tests: ${{ needs.e2e-tests.result }}" >> $GITHUB_STEP_SUMMARY + fi + + - name: Fail if any tests failed + if: | + needs.backend-unit-tests.result == 'failure' || + needs.frontend-unit-tests.result == 'failure' || + needs.integration-tests.result == 'failure' || + needs.e2e-tests.result == 'failure' + run: exit 1 \ No newline at end of file diff --git a/README.md b/README.md index 6392ff3..0ef5f63 100644 --- a/README.md +++ b/README.md @@ -263,3 +263,28 @@ npm run cypress:open 1. Select a browser to view the app in 1. Select a spec to run from the list, it will auto-run the tests anytime there are changes made to the spec ![cypress-spec-list](image.png) + +## GitHub Actions Setup +### Secrets +To access secrets inside of the repo: +1. Go to Settings +2. Scroll down to Security +3. Click on Secrets and Variables +4. Click on Actions +5. Click on New repository secret +6. Then Add Secret with the proper credentials. +7. Click Save. +8. Then Repeat for the rest of the following secrets below... + +### Required Secrets +The following GitHub Secrets need to be created for the CI/CD workflows to run successfully: +- `VM_HOST` - The VM's IP Address +- `VM_USERNAME` - The username to SSH into the VM. - put `root` +- `VM_PASSWORD` - Your VM's password +- `VM_PROJECT_PATH` - Full path to project directory on VM - put `/root/Pixel-To-Pattern` +- `GHCR_TOKEN` - GitHub Personal Access Token (Make sure to follow the steps above) +- `VM_SSH_PORT` - SSH port (only if not using default) - put `22` + +***Once All the Secrets are placed and you've go ahead and run this in your terminal..*** + +`bash git commit --allow-empty -m "Testing GitHub Actions Workflow..."` \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index ebd221c..433c91a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2525,9 +2525,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -6630,9 +6630,9 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -8238,9 +8238,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/docker-compose.deploy.yml b/docker-compose.deploy.yml index 433b13c..4206191 100644 --- a/docker-compose.deploy.yml +++ b/docker-compose.deploy.yml @@ -1,6 +1,6 @@ services: frontend: - image: ghcr.io/auglebobaugles/pixel-to-pattern-frontend:latest + image: ghcr.io/tgillysuit/pixel-to-pattern-frontend:latest container_name: frontend ports: - "80:3001" @@ -12,7 +12,7 @@ services: restart: unless-stopped backend: - image: ghcr.io/auglebobaugles/pixel-to-pattern-backend:latest + image: ghcr.io/tgillysuit/pixel-to-pattern-backend:latest container_name: backend ports: - "3000:3000" diff --git a/server/package-lock.json b/server/package-lock.json index 5cdf5d2..ea0a1fb 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -2921,9 +2921,9 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -4002,9 +4002,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": {