diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2349f65..387dcd8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -13,6 +13,7 @@ permissions: contents: read env: + NODE_VERSION: "22" 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 }} @@ -39,127 +40,38 @@ jobs: name: Run main database migration runs-on: ubuntu-latest needs: build-and-publish - if: ${{ github.ref == 'refs/heads/staging' || github.event_name == 'workflow_dispatch' }} + if: ${{ github.event_name == 'release' || 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 + - name: Checkout + uses: actions/checkout@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 }} + persist-credentials: false - - 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: Enable pnpm + run: corepack enable - - name: Setup kubectl - uses: azure/setup-kubectl@v3 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm - - name: Update kubeconfig - shell: bash - run: | - aws eks update-kubeconfig \ - --name "${AWS_EKS_PROD_CLUSTER_NAME}" \ - --region "${AWS_EKS_PROD_REGION}" + - name: Install dependencies + run: pnpm install --frozen-lockfile - - name: Run main database migration job - shell: bash + - name: Run main database migration env: - IMAGE_URI: ${{ steps.context.outputs.image_uri }} - NAMESPACE: ${{ steps.context.outputs.namespace }} + DATABASE_URL: ${{ secrets.DATABASE_MIGRATION_URL }} + shell: bash 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 + if [ -z "${DATABASE_URL}" ]; then + echo "::error::Missing DATABASE_MIGRATION_URL repository secret" exit 1 fi - - kubectl delete job "$job_name" --namespace "$NAMESPACE" --ignore-not-found + pnpm db:migrate build-and-publish: name: Build and Publish diff --git a/lib/dashboard-deploy-workflow.test.ts b/lib/dashboard-deploy-workflow.test.ts new file mode 100644 index 0000000..7cd7846 --- /dev/null +++ b/lib/dashboard-deploy-workflow.test.ts @@ -0,0 +1,39 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const readDeployWorkflow = (): string => + readFileSync(join(process.cwd(), ".github/workflows/deploy.yml"), "utf8"); + +const EXPRESSION_START: string = "$"; + +describe("dashboard deploy workflow", (): void => { + it("runs main migrations with the dedicated direct migration credential", (): void => { + const deployWorkflow: string = readDeployWorkflow(); + + expect(deployWorkflow).toContain( + `DATABASE_URL: ${EXPRESSION_START}{{ secrets.DATABASE_MIGRATION_URL }}` + ); + expect(deployWorkflow).toContain("pnpm db:migrate"); + expect(deployWorkflow).not.toContain( + `knowhere-dashboard-migrate-${EXPRESSION_START}{GITHUB_RUN_ID}` + ); + expect(deployWorkflow).not.toContain("ttlSecondsAfterFinished"); + }); + + it("fails clearly before migration when the dedicated credential is missing", (): void => { + const deployWorkflow: string = readDeployWorkflow(); + + expect(deployWorkflow).toContain(`if [ -z "${EXPRESSION_START}{DATABASE_URL}" ]; then`); + expect(deployWorkflow).toContain("Missing DATABASE_MIGRATION_URL"); + }); + + it("gates both staging pushes and production releases on migration success", (): void => { + const deployWorkflow: string = readDeployWorkflow(); + + expect(deployWorkflow).toContain( + `if: ${EXPRESSION_START}{{ github.event_name == 'release' || github.ref == 'refs/heads/staging' || github.event_name == 'workflow_dispatch' }}` + ); + expect(deployWorkflow).toContain("needs: [build-and-publish, migrate]"); + }); +});