From 88bc5582d19657dfc870040e0fca246355593f2c Mon Sep 17 00:00:00 2001 From: suguanYang Date: Wed, 12 Aug 2026 11:57:52 +0800 Subject: [PATCH] gate dashboard migrations before staging deploy --- .github/workflows/deploy.yml | 132 ++++++++++++++++++++++++++++++++++- Dockerfile | 2 +- README.md | 4 +- 3 files changed, 135 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d166ea6..2349f65 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -35,6 +35,132 @@ env: DASHBOARD_POSTHOG_HOST: https://us.i.posthog.com jobs: + migrate: + name: Run main database migration + runs-on: ubuntu-latest + needs: build-and-publish + if: ${{ github.ref == 'refs/heads/staging' || github.event_name == 'workflow_dispatch' }} + permissions: + contents: read + env: + AWS_EKS_PROD_CLUSTER_NAME: ${{ secrets.AWS_EKS_PROD_CLUSTER_NAME }} + AWS_EKS_PROD_REGION: ${{ secrets.AWS_EKS_PROD_REGION }} + DASHBOARD_IMAGE_REGISTRY: ${{ secrets.DASHBOARD_IMAGE_REGISTRY }} + DASHBOARD_IMAGE_REPOSITORY: ${{ secrets.DASHBOARD_IMAGE_REPOSITORY }} + DASHBOARD_KUBE_CONTAINER: ${{ secrets.DASHBOARD_KUBE_CONTAINER }} + DASHBOARD_KUBE_DEPLOYMENT: ${{ secrets.DASHBOARD_KUBE_DEPLOYMENT }} + DASHBOARD_KUBE_NAMESPACE_PROD: ${{ secrets.DASHBOARD_KUBE_NAMESPACE_PROD }} + DASHBOARD_KUBE_NAMESPACE_STAGING: ${{ secrets.DASHBOARD_KUBE_NAMESPACE_STAGING }} + steps: + - name: Decide migration context + id: context + shell: bash + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "release" ]; then + environment="prod" + else + environment="staging" + fi + if [ "$environment" = "prod" ]; then + namespace="${DASHBOARD_KUBE_NAMESPACE_PROD}" + else + namespace="${DASHBOARD_KUBE_NAMESPACE_STAGING}" + fi + short_sha="${GITHUB_SHA::8}" + if [ "${{ github.event_name }}" = "release" ]; then + image_tag="${{ github.event.release.tag_name }}-${environment}" + else + image_tag="${environment}-${short_sha}" + fi + echo "environment=$environment" >> "$GITHUB_OUTPUT" + echo "namespace=$namespace" >> "$GITHUB_OUTPUT" + echo "image_uri=${DASHBOARD_IMAGE_REGISTRY}/${DASHBOARD_IMAGE_REPOSITORY}:${image_tag}" >> "$GITHUB_OUTPUT" + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_EKS_PROD_REGION }} + + - name: Validate migration configuration + shell: bash + run: | + set -euo pipefail + for variable_name in \ + AWS_EKS_PROD_CLUSTER_NAME \ + AWS_EKS_PROD_REGION \ + DASHBOARD_IMAGE_REGISTRY \ + DASHBOARD_IMAGE_REPOSITORY \ + DASHBOARD_KUBE_CONTAINER \ + DASHBOARD_KUBE_DEPLOYMENT \ + DASHBOARD_KUBE_NAMESPACE_STAGING + do + if [ -z "${!variable_name}" ]; then + echo "::error::Missing migration configuration: ${variable_name}" + exit 1 + fi + done + + - name: Setup kubectl + uses: azure/setup-kubectl@v3 + + - name: Update kubeconfig + shell: bash + run: | + aws eks update-kubeconfig \ + --name "${AWS_EKS_PROD_CLUSTER_NAME}" \ + --region "${AWS_EKS_PROD_REGION}" + + - name: Run main database migration job + shell: bash + env: + IMAGE_URI: ${{ steps.context.outputs.image_uri }} + NAMESPACE: ${{ steps.context.outputs.namespace }} + run: | + set -euo pipefail + job_name="knowhere-dashboard-migrate-${GITHUB_RUN_ID}" + + migration_manifest="$(kubectl get deployment/"${DASHBOARD_KUBE_DEPLOYMENT}" --namespace "$NAMESPACE" -o json \ + | jq --arg job_name "$job_name" --arg image_uri "$IMAGE_URI" --arg container_name "$DASHBOARD_KUBE_CONTAINER" ' + if ((.spec.template.spec.containers // []) | map(select(.name == $container_name)) | length) == 0 then + error("dashboard deployment container was not found") + else + { + apiVersion: "batch/v1", + kind: "Job", + metadata: {name: $job_name, namespace: .metadata.namespace}, + spec: { + backoffLimit: 0, + ttlSecondsAfterFinished: 300, + template: { + metadata: {labels: {"app": "knowhere-dashboard-migrate"}}, + spec: ( + .spec.template.spec + | .restartPolicy = "Never" + | .containers = [(.containers[] | select(.name == $container_name))] + | .containers[0].image = $image_uri + | .containers[0].command = ["pnpm", "db:migrate"] + | del(.containers[0].args) + ) + } + } + } + end + ')" + + printf '%s\n' "$migration_manifest" | kubectl apply -f - + + if ! kubectl wait --for=condition=complete "job/$job_name" \ + --namespace "$NAMESPACE" --timeout=900s; then + kubectl describe job "$job_name" --namespace "$NAMESPACE" || true + kubectl logs "job/$job_name" --namespace "$NAMESPACE" --all-containers=true || true + exit 1 + fi + + kubectl delete job "$job_name" --namespace "$NAMESPACE" --ignore-not-found + build-and-publish: name: Build and Publish runs-on: ubuntu-latest @@ -186,7 +312,11 @@ jobs: deploy: name: Deploy runs-on: ubuntu-latest - needs: build-and-publish + needs: [build-and-publish, migrate] + if: >- + ${{ always() && + needs.build-and-publish.result == 'success' && + (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') }} steps: - name: Validate semantic release tag if: ${{ github.event_name == 'release' }} diff --git a/Dockerfile b/Dockerfile index b26185c..35f77a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -75,4 +75,4 @@ USER nextjs EXPOSE 3000 -CMD ["sh", "-c", "pnpm db:migrate && pnpm newsletter-db:migrate && exec pnpm start"] +CMD ["pnpm", "start"] diff --git a/README.md b/README.md index c0566d7..41d7aee 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,8 @@ pnpm newsletter-db:migrate Production newsletter storage reads `NEWSLETTER_DATABASE_URL` from the optional Kubernetes Secret key `knowhere-secrets/newsletter-database-url` when it exists. If it is unset, newsletter storage falls back to `DATABASE_URL`. +The newsletter database migration is manual-only and is not run by container startup, deployment workflows, CI, or `workflow_dispatch`. Run `pnpm newsletter-db:migrate` only as an explicitly approved operator operation against the intended newsletter database. + ## Docker Build the image: @@ -124,7 +126,7 @@ Run the dashboard: docker run --rm -p 3000:3000 --env-file .env.local knowhere-dashboard ``` -The container runs `pnpm db:migrate` and `pnpm newsletter-db:migrate` before starting the Next.js server. If either command fails, the app server is not started. +The container starts the Next.js server directly with `pnpm start`. Database migrations are release-gated operations and must run explicitly before the deployment is rolled out. The image runs the standard Next.js Node server with `pnpm start`. Runtime configuration is injected through environment variables; the Docker build does not create or bake `.env.production`.