Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 131 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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' }}
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@ USER nextjs

EXPOSE 3000

CMD ["sh", "-c", "pnpm db:migrate && pnpm newsletter-db:migrate && exec pnpm start"]
CMD ["pnpm", "start"]
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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`.

Expand Down
Loading