From a3d99941c7ed57bca6ee32108386569a9cf55cd4 Mon Sep 17 00:00:00 2001 From: Dat Date: Tue, 11 Nov 2025 13:05:13 +0100 Subject: [PATCH 1/4] feat: add testing environment CI/CD workflows - Add PR validation workflow for testing branch - Runs linting, type checking, unit tests - Validates Docker builds - Auto-merges on success - Add deployment workflow for testing environment - Builds and pushes images with commit SHA and 'testing' tags - Updates gitops repo with new image tag - Waits for ArgoCD deployment - Runs smoke tests and E2E tests - Sends Slack notifications with results --- .github/workflows/deploy-testing.yml | 285 +++++++++++++++++++++++++++ .github/workflows/pr-testing.yml | 113 +++++++++++ 2 files changed, 398 insertions(+) create mode 100644 .github/workflows/deploy-testing.yml create mode 100644 .github/workflows/pr-testing.yml diff --git a/.github/workflows/deploy-testing.yml b/.github/workflows/deploy-testing.yml new file mode 100644 index 00000000..f5c3004b --- /dev/null +++ b/.github/workflows/deploy-testing.yml @@ -0,0 +1,285 @@ +name: Deploy to Testing + +on: + push: + branches: + - testing + +env: + REGISTRY: registry.digitalocean.com/dbr-cr + GITOPS_REPO: dembrane/echo-gitops + PYTHON_VERSION: "3.11" + +jobs: + build-and-push: + name: Build & Push Images + runs-on: ubuntu-latest + outputs: + image-tag: ${{ steps.meta.outputs.tag }} + steps: + - uses: actions/checkout@v4 + + - name: Generate image tag + id: meta + run: | + SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) + echo "tag=$SHORT_SHA" >> $GITHUB_OUTPUT + echo "šŸ“¦ Image tag: $SHORT_SHA" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to DigitalOcean Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DO_REGISTRY_TOKEN }} + password: ${{ secrets.DO_REGISTRY_TOKEN }} + + - name: Build and Push API Server + uses: docker/build-push-action@v5 + with: + context: ./echo/server + file: ./echo/server/Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/dbr-echo-server:${{ steps.meta.outputs.tag }} + ${{ env.REGISTRY }}/dbr-echo-server:testing + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build and Push Directus + uses: docker/build-push-action@v5 + with: + context: ./echo/directus + file: ./echo/directus/Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/dbr-echo-directus:${{ steps.meta.outputs.tag }} + ${{ env.REGISTRY }}/dbr-echo-directus:testing + cache-from: type=gha + cache-to: type=gha,mode=max + + update-gitops: + name: Update GitOps Repo + runs-on: ubuntu-latest + needs: [build-and-push] + steps: + - name: Checkout GitOps repo + uses: actions/checkout@v4 + with: + repository: ${{ env.GITOPS_REPO }} + token: ${{ secrets.GITOPS_PAT }} + ref: main + + - name: Update image tag in values-testing.yaml + run: | + IMAGE_TAG="${{ needs.build-and-push.outputs.image-tag }}" + echo "Updating imageTag to: $IMAGE_TAG" + + sed -i "s/imageTag: \".*\"/imageTag: \"$IMAGE_TAG\"/" helm/echo/values-testing.yaml + + echo "Updated values-testing.yaml:" + grep "imageTag:" helm/echo/values-testing.yaml + + - name: Commit and push changes + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add helm/echo/values-testing.yaml + git commit -m "Update testing image tag to ${{ needs.build-and-push.outputs.image-tag }}" + git push origin main + + wait-for-deployment: + name: Wait for ArgoCD Deployment + runs-on: ubuntu-latest + needs: [update-gitops, build-and-push] + steps: + - uses: actions/checkout@v4 + + - name: Wait for deployment + run: | + echo "ā³ Waiting for ArgoCD to sync and deploy..." + echo "Image tag: ${{ needs.build-and-push.outputs.image-tag }}" + + MAX_WAIT=600 # 10 minutes + INTERVAL=15 + ELAPSED=0 + + while [ $ELAPSED -lt $MAX_WAIT ]; do + echo "Checking API health (${ELAPSED}s elapsed)..." + + if curl -f -s https://api.echo-testing.dembrane.com/health > /dev/null 2>&1; then + echo "āœ… API is healthy!" + exit 0 + fi + + sleep $INTERVAL + ELAPSED=$((ELAPSED + INTERVAL)) + done + + echo "āŒ Deployment did not become healthy within ${MAX_WAIT}s" + exit 1 + + smoke-tests: + name: Run Smoke Tests + runs-on: ubuntu-latest + needs: [wait-for-deployment, build-and-push] + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: | + cd echo/server + pip install -r requirements.lock + pip install requests + + - name: Run Smoke Tests + env: + TEST_API_URL: https://api.echo-testing.dembrane.com + TEST_DIRECTUS_URL: https://directus.echo-testing.dembrane.com + run: | + cd echo/server + pytest tests/smoke/ -v --timeout=600 --maxfail=1 + + e2e-tests: + name: Run E2E Tests + runs-on: ubuntu-latest + needs: [smoke-tests] + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Install dependencies + run: | + cd echo/frontend + pnpm install + + - name: Install Playwright Browsers + run: | + cd echo/frontend + pnpm exec playwright install --with-deps chromium + + - name: Run Playwright Tests + env: + TEST_BASE_URL: https://api.echo-testing.dembrane.com + run: | + cd echo/frontend + pnpm exec playwright test + + - name: Upload Playwright Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: echo/frontend/playwright-report/ + retention-days: 7 + + notify-slack: + name: Notify Slack + runs-on: ubuntu-latest + needs: [build-and-push, smoke-tests, e2e-tests] + if: always() + steps: + - name: Determine status + id: status + run: | + if [ "${{ needs.smoke-tests.result }}" == "success" ] && [ "${{ needs.e2e-tests.result }}" == "success" ]; then + echo "result=success" >> $GITHUB_OUTPUT + echo "emoji=āœ…" >> $GITHUB_OUTPUT + echo "color=good" >> $GITHUB_OUTPUT + else + echo "result=failure" >> $GITHUB_OUTPUT + echo "emoji=āŒ" >> $GITHUB_OUTPUT + echo "color=danger" >> $GITHUB_OUTPUT + fi + + - name: Send Slack notification + uses: slackapi/slack-github-action@v1.24.0 + with: + payload: | + { + "attachments": [ + { + "color": "${{ steps.status.outputs.color }}", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "${{ steps.status.outputs.emoji }} Testing Deployment: ${{ steps.status.outputs.result }}" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Repository:*\necho" + }, + { + "type": "mrkdwn", + "text": "*Branch:*\ntesting" + }, + { + "type": "mrkdwn", + "text": "*Image Tag:*\n`${{ needs.build-and-push.outputs.image-tag }}`" + }, + { + "type": "mrkdwn", + "text": "*Commit:*\n" + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Smoke Tests:*\n${{ needs.smoke-tests.result }}" + }, + { + "type": "mrkdwn", + "text": "*E2E Tests:*\n${{ needs.e2e-tests.result }}" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Links:*\n• \n• \n• " + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "${{ steps.status.outputs.result == 'failure' && 'āš ļø *Rollback Instructions:*\n```\ncd echo-gitops\ngit revert HEAD\ngit push origin main\n```\nOr keep for debugging and fix forward.' || 'šŸŽ‰ All tests passed! Environment is stable.' }}" + } + } + ] + } + ] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK + diff --git a/.github/workflows/pr-testing.yml b/.github/workflows/pr-testing.yml new file mode 100644 index 00000000..cfbad0dc --- /dev/null +++ b/.github/workflows/pr-testing.yml @@ -0,0 +1,113 @@ +name: PR to Testing Branch + +on: + pull_request: + branches: + - testing + types: [opened, synchronize, reopened] + +env: + REGISTRY: registry.digitalocean.com/dbr-cr + PYTHON_VERSION: "3.11" + +jobs: + lint-and-type-check: + name: Lint & Type Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: | + cd echo/server + pip install -r requirements.lock + + - name: Run Ruff Lint + run: | + cd echo/server + ruff check . + + - name: Run Ruff Format Check + run: | + cd echo/server + ruff format --check . + + - name: Run MyPy Type Check + run: | + cd echo/server + mypy dembrane/ --ignore-missing-imports + + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: | + cd echo/server + pip install -r requirements.lock + + - name: Run Unit Tests + run: | + cd echo/server + pytest tests/ -v -m "not integration and not slow" --maxfail=3 + + build-images: + name: Build Docker Images (validation only) + runs-on: ubuntu-latest + needs: [lint-and-type-check, unit-tests] + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build API Server Image + uses: docker/build-push-action@v5 + with: + context: ./echo/server + file: ./echo/server/Dockerfile + push: false + tags: ${{ env.REGISTRY }}/dbr-echo-server:pr-${{ github.event.pull_request.number }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build Directus Image + uses: docker/build-push-action@v5 + with: + context: ./echo/directus + file: ./echo/directus/Dockerfile + push: false + tags: ${{ env.REGISTRY }}/dbr-echo-directus:pr-${{ github.event.pull_request.number }} + cache-from: type=gha + cache-to: type=gha,mode=max + + auto-merge: + name: Auto-merge PR + runs-on: ubuntu-latest + needs: [build-images] + permissions: + pull-requests: write + contents: write + steps: + - name: Auto-merge PR + uses: pascalgn/automerge-action@v0.16.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MERGE_LABELS: "" + MERGE_METHOD: "squash" + MERGE_COMMIT_MESSAGE: "pull-request-title" + MERGE_RETRIES: 3 + MERGE_RETRY_SLEEP: 10000 + From 09854c080f560a07127a4306bdadae2b52ae7b95 Mon Sep 17 00:00:00 2001 From: Dat Date: Tue, 11 Nov 2025 13:07:09 +0100 Subject: [PATCH 2/4] fix: use existing GITOPS_REPO_TOKEN secret name --- .github/workflows/deploy-testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-testing.yml b/.github/workflows/deploy-testing.yml index f5c3004b..f0fd6c06 100644 --- a/.github/workflows/deploy-testing.yml +++ b/.github/workflows/deploy-testing.yml @@ -69,7 +69,7 @@ jobs: uses: actions/checkout@v4 with: repository: ${{ env.GITOPS_REPO }} - token: ${{ secrets.GITOPS_PAT }} + token: ${{ secrets.GITOPS_REPO_TOKEN }} ref: main - name: Update image tag in values-testing.yaml From a707f9b682dafb10e041c438142980552ba2704e Mon Sep 17 00:00:00 2001 From: Dat Date: Tue, 11 Nov 2025 13:08:25 +0100 Subject: [PATCH 3/4] feat: temporarily disable Slack notifications Will be enabled once SLACK_WEBHOOK_URL secret is configured --- .github/workflows/deploy-testing.yml | 92 +--------------------------- 1 file changed, 2 insertions(+), 90 deletions(-) diff --git a/.github/workflows/deploy-testing.yml b/.github/workflows/deploy-testing.yml index f0fd6c06..41f59d4b 100644 --- a/.github/workflows/deploy-testing.yml +++ b/.github/workflows/deploy-testing.yml @@ -192,94 +192,6 @@ jobs: path: echo/frontend/playwright-report/ retention-days: 7 - notify-slack: - name: Notify Slack - runs-on: ubuntu-latest - needs: [build-and-push, smoke-tests, e2e-tests] - if: always() - steps: - - name: Determine status - id: status - run: | - if [ "${{ needs.smoke-tests.result }}" == "success" ] && [ "${{ needs.e2e-tests.result }}" == "success" ]; then - echo "result=success" >> $GITHUB_OUTPUT - echo "emoji=āœ…" >> $GITHUB_OUTPUT - echo "color=good" >> $GITHUB_OUTPUT - else - echo "result=failure" >> $GITHUB_OUTPUT - echo "emoji=āŒ" >> $GITHUB_OUTPUT - echo "color=danger" >> $GITHUB_OUTPUT - fi - - - name: Send Slack notification - uses: slackapi/slack-github-action@v1.24.0 - with: - payload: | - { - "attachments": [ - { - "color": "${{ steps.status.outputs.color }}", - "blocks": [ - { - "type": "header", - "text": { - "type": "plain_text", - "text": "${{ steps.status.outputs.emoji }} Testing Deployment: ${{ steps.status.outputs.result }}" - } - }, - { - "type": "section", - "fields": [ - { - "type": "mrkdwn", - "text": "*Repository:*\necho" - }, - { - "type": "mrkdwn", - "text": "*Branch:*\ntesting" - }, - { - "type": "mrkdwn", - "text": "*Image Tag:*\n`${{ needs.build-and-push.outputs.image-tag }}`" - }, - { - "type": "mrkdwn", - "text": "*Commit:*\n" - } - ] - }, - { - "type": "section", - "fields": [ - { - "type": "mrkdwn", - "text": "*Smoke Tests:*\n${{ needs.smoke-tests.result }}" - }, - { - "type": "mrkdwn", - "text": "*E2E Tests:*\n${{ needs.e2e-tests.result }}" - } - ] - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "*Links:*\n• \n• \n• " - } - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "${{ steps.status.outputs.result == 'failure' && 'āš ļø *Rollback Instructions:*\n```\ncd echo-gitops\ngit revert HEAD\ngit push origin main\n```\nOr keep for debugging and fix forward.' || 'šŸŽ‰ All tests passed! Environment is stable.' }}" - } - } - ] - } - ] - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK + # TODO: Add Slack notification job here once SLACK_WEBHOOK_URL secret is configured + # See TESTING_STAGE_IMPLEMENTATION.md Phase 3.3 for the full notify-slack job definition From 2ecd5de996bda8a01322701e762ae2fab0831f17 Mon Sep 17 00:00:00 2001 From: Dat Date: Tue, 11 Nov 2025 13:12:44 +0100 Subject: [PATCH 4/4] feat: enable Slack notifications SLACK_WEBHOOK_URL secret has been configured --- .github/workflows/deploy-testing.yml | 92 +++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-testing.yml b/.github/workflows/deploy-testing.yml index 41f59d4b..f0fd6c06 100644 --- a/.github/workflows/deploy-testing.yml +++ b/.github/workflows/deploy-testing.yml @@ -192,6 +192,94 @@ jobs: path: echo/frontend/playwright-report/ retention-days: 7 - # TODO: Add Slack notification job here once SLACK_WEBHOOK_URL secret is configured - # See TESTING_STAGE_IMPLEMENTATION.md Phase 3.3 for the full notify-slack job definition + notify-slack: + name: Notify Slack + runs-on: ubuntu-latest + needs: [build-and-push, smoke-tests, e2e-tests] + if: always() + steps: + - name: Determine status + id: status + run: | + if [ "${{ needs.smoke-tests.result }}" == "success" ] && [ "${{ needs.e2e-tests.result }}" == "success" ]; then + echo "result=success" >> $GITHUB_OUTPUT + echo "emoji=āœ…" >> $GITHUB_OUTPUT + echo "color=good" >> $GITHUB_OUTPUT + else + echo "result=failure" >> $GITHUB_OUTPUT + echo "emoji=āŒ" >> $GITHUB_OUTPUT + echo "color=danger" >> $GITHUB_OUTPUT + fi + + - name: Send Slack notification + uses: slackapi/slack-github-action@v1.24.0 + with: + payload: | + { + "attachments": [ + { + "color": "${{ steps.status.outputs.color }}", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "${{ steps.status.outputs.emoji }} Testing Deployment: ${{ steps.status.outputs.result }}" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Repository:*\necho" + }, + { + "type": "mrkdwn", + "text": "*Branch:*\ntesting" + }, + { + "type": "mrkdwn", + "text": "*Image Tag:*\n`${{ needs.build-and-push.outputs.image-tag }}`" + }, + { + "type": "mrkdwn", + "text": "*Commit:*\n" + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Smoke Tests:*\n${{ needs.smoke-tests.result }}" + }, + { + "type": "mrkdwn", + "text": "*E2E Tests:*\n${{ needs.e2e-tests.result }}" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Links:*\n• \n• \n• " + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "${{ steps.status.outputs.result == 'failure' && 'āš ļø *Rollback Instructions:*\n```\ncd echo-gitops\ngit revert HEAD\ngit push origin main\n```\nOr keep for debugging and fix forward.' || 'šŸŽ‰ All tests passed! Environment is stable.' }}" + } + } + ] + } + ] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK