diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 5caaf0e6..bfee0d86 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -16,6 +16,9 @@ jobs: VERSION_TAG: ${{ fromJson(steps.variables.outputs.result).VERSION_TAG }} ZTOR_PR_CONTAINER_APP_NAME: ${{ fromJson(steps.variables.outputs.result).ZTOR_PR_CONTAINER_APP_NAME }} GITHUB_ENVIRONMENT: ${{ fromJson(steps.variables.outputs.result).GITHUB_ENVIRONMENT }} + GITHUB_SWARM_ENVIRONMENT: ${{ fromJson(steps.variables.outputs.result).GITHUB_SWARM_ENVIRONMENT }} + FRONTEND_HOSTNAME: ${{ fromJson(steps.variables.outputs.result).FRONTEND_HOSTNAME }} + ZTOR_HOSTNAME: ${{ fromJson(steps.variables.outputs.result).ZTOR_HOSTNAME }} steps: - uses: actions/checkout@v4 with: @@ -70,23 +73,46 @@ jobs: ## Buildx is needed for caching - name: Set up Buildx uses: docker/setup-buildx-action@v2 - ## TODO: this is under erikvv's GitHub Container Registry (ghcr.io) - ## because Zenmo used to be on a legacy GitHub plan. + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v6 + with: + push: true + file: ./docker/production/ztor/Dockerfile + tags: ghcr.io/zenmo/ztor:${{ needs.variables.outputs.VERSION_TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max + + build-frontend: + needs: variables + environment: ${{ needs.variables.outputs.GITHUB_SWARM_ENVIRONMENT }} + runs-on: ubuntu-latest + steps: + ## Buildx is needed for caching + - name: Set up Buildx + uses: docker/setup-buildx-action@v2 - name: Login to Azure Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io - username: erikvv - password: ${{ secrets.ERIKVV_GHCR_PUSH_PASSWORD }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: push: true - tags: ghcr.io/erikvv/ztor:${{ needs.variables.outputs.VERSION_TAG }} + file: ./docker/production/frontend/Dockerfile + tags: ghcr.io/zenmo/zero-frontend:${{ needs.variables.outputs.VERSION_TAG }} + build-args: VITE_ZTOR_URL=https://${{ vars.ZTOR_HOSTNAME || needs.variables.outputs.ZTOR_HOSTNAME }} cache-from: type=gha cache-to: type=gha,mode=max - migrate: + migrate-azure: needs: - variables environment: ${{ needs.variables.outputs.GITHUB_ENVIRONMENT }} @@ -108,9 +134,31 @@ jobs: - name: migrate run: flyway migrate - deploy-ztor: + migrate-swarm: + needs: + - variables + environment: ${{ needs.variables.outputs.GITHUB_SWARM_ENVIRONMENT }} + runs-on: ubuntu-latest + container: + image: redgate/flyway:10.2.0 + env: + ## TODO: in case of pull request, copy and migrate test database + FLYWAY_URL: jdbc:postgresql://postgres.zenmo.com:5432/${{ vars.DB_NAME }} + FLYWAY_USER: ${{ vars.DB_NAME }} + FLYWAY_PASSWORD: ${{ secrets.DB_PASSWORD }} + FLYWAY_LOCATIONS: filesystem:./migrations + FLYWAY_BASELINE_ON_MIGRATE: true + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: migrations + - name: migrate + run: flyway migrate + + deploy-ztor-azure: needs: - - migrate + - migrate-azure - build-ztor - variables - environment-variables @@ -147,14 +195,48 @@ jobs: BASE_URL=https://${{ needs.environment-variables.outputs.ZTOR_CONTAINER_APP_NAME }}.zero.zenmo.com --ingress external --tags branch=${{ github.head_ref || github.ref_name }} - --image ghcr.io/erikvv/ztor:${{ needs.variables.outputs.VERSION_TAG }} - --registry-username erikvv - --registry-password ${{ secrets.ERIKVV_GHCR_PULL_PASSWORD }} + --image ghcr.io/zenmo/ztor:${{ needs.variables.outputs.VERSION_TAG }} + --registry-username ${{ github.actor }} + --registry-password ${{ secrets.GITHUB_TOKEN }} --registry-server ghcr.io --cpu 1.5 --memory 3 --min-replicas 0 + deploy-swarm: + needs: + - migrate-swarm + - build-ztor + - build-frontend + - variables + - environment-variables + environment: ${{ needs.variables.outputs.GITHUB_SWARM_ENVIRONMENT }} + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + sparse-checkout: docker/production + - name: Deploy to Docker Swarm + uses: sagebind/docker-swarm-deploy-action@v2 + env: + TAG: ${{ needs.variables.outputs.VERSION_TAG }} + FRONTEND_HOSTNAME: ${{ vars.FRONTEND_HOSTNAME || needs.variables.outputs.FRONTEND_HOSTNAME }} + ZTOR_HOSTNAME: ${{ vars.ZTOR_HOSTNAME || needs.variables.outputs.ZTOR_HOSTNAME }} + DB_NAME: ${{ vars.DB_NAME }} + POSTGRES_PASSWORD: ${{ secrets.DB_PASSWORD }} + AZURE_STORAGE_ACCOUNT_NAME: zerostore + AZURE_STORAGE_ACCOUNT_KEY: ${{ secrets.AZURE_STORAGE_ACCOUNT_KEY }} + AZURE_STORAGE_CONTAINER: ${{ vars.AZURE_STORAGE_CONTAINER }} + CORS_ALLOW_ORIGIN_PATTERN: ${{ vars.CORS_ALLOW_ORIGIN_PATTERN }} + OAUTH_CLIENT_ID: ${{ vars.OAUTH_CLIENT_ID }} + OAUTH_CLIENT_SECRET: ${{ secrets.OAUTH_CLIENT_SECRET }} + with: + remote_host: ssh://root@server.zenmo.com + ssh_private_key: ${{ secrets.SWARM_SSH_PRIVATE_KEY }} + ssh_public_key: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ1E4LUG22qgzc8U7oNYGWCn0cyA31+iyX2pck9wcPMS + args: stack deploy --compose-file ./docker/production/compose.yaml zero-${{ needs.variables.outputs.GITHUB_ENVIRONMENT }} + build-deploy-static-site: name: Build and deploy static site runs-on: ubuntu-latest diff --git a/docker-compose.yaml b/compose.yaml similarity index 95% rename from docker-compose.yaml rename to compose.yaml index 471d7e43..5df285d4 100644 --- a/docker-compose.yaml +++ b/compose.yaml @@ -8,7 +8,7 @@ services: - ./migrations:/flyway/sql command: migrate env_file: - - ./docker/flyway.env + - docker/local/flyway.env environment: FLYWAY_URL: jdbc:postgresql://postgres:5432/postgres FLYWAY_USER: postgres @@ -44,7 +44,7 @@ services: - gradle-zorm-test-home:/home/gradle/.gradle command: zorm:test --no-daemon env_file: - - ./docker/ztor.env + - docker/local/ztor.env environment: POSTGRES_URL: jdbc:postgresql://postgres:5432/test POSTGRES_USER: postgres @@ -52,12 +52,13 @@ services: ## Production version ztor-production: build: + dockerfile: ./docker/production/ztor/Dockerfile context: . user: 1000:1000 ports: - 127.0.0.1:8082:8082 env_file: - - ./docker/ztor.env + - docker/local/ztor.env environment: POSTGRES_URL: jdbc:postgresql://postgres:5432/postgres POSTGRES_USER: postgres @@ -82,7 +83,7 @@ services: depends_on: - postgres env_file: - - ./docker/ztor.env + - docker/local/ztor.env environment: POSTGRES_URL: jdbc:postgresql://postgres:5432/postgres POSTGRES_USER: postgres @@ -166,7 +167,7 @@ services: depends_on: - postgres env_file: - - ./docker/ztor.env + - docker/local/ztor.env environment: POSTGRES_URL: jdbc:postgresql://postgres:5432/postgres POSTGRES_USER: postgres @@ -202,7 +203,7 @@ services: ## first run `docker-compose run --rm npm run build` frontend-static: build: - context: ./docker/caddy + context: docker/local/caddy volumes: - ./frontend/dist:/srv ports: @@ -216,7 +217,7 @@ services: - postgres:/var/lib/postgresql/data command: "-c log_statement=all" env_file: - - ./docker/postgres.env + - docker/local/postgres.env volumes: gradle-vallum-test-cache: diff --git a/docker/.gitignore b/docker/local/.gitignore similarity index 100% rename from docker/.gitignore rename to docker/local/.gitignore diff --git a/docker/caddy/Caddyfile b/docker/local/caddy/Caddyfile similarity index 100% rename from docker/caddy/Caddyfile rename to docker/local/caddy/Caddyfile diff --git a/docker/caddy/Dockerfile b/docker/local/caddy/Dockerfile similarity index 100% rename from docker/caddy/Dockerfile rename to docker/local/caddy/Dockerfile diff --git a/docker/flyway.example.env b/docker/local/flyway.example.env similarity index 100% rename from docker/flyway.example.env rename to docker/local/flyway.example.env diff --git a/docker/postgres.example.env b/docker/local/postgres.example.env similarity index 100% rename from docker/postgres.example.env rename to docker/local/postgres.example.env diff --git a/docker/ztor.example.env b/docker/local/ztor.example.env similarity index 100% rename from docker/ztor.example.env rename to docker/local/ztor.example.env diff --git a/docker/production/compose.yaml b/docker/production/compose.yaml new file mode 100644 index 00000000..9eec6e76 --- /dev/null +++ b/docker/production/compose.yaml @@ -0,0 +1,52 @@ +version: "3.8" + +# Compose file for Docker Swarm +services: + frontend: + image: ghcr.io/zenmo/zero-frontend:${TAG} + networks: + - caddy_default + labels: + caddy: ${FRONTEND_HOSTNAME} + caddy.reverse_proxy: "{{upstreams 2015}}" + # This certificate only works for the pullrequest domain. + # It should fall back to Let's Encrypt for the main domain. + # Except it doesn't. + #caddy.tls: /static_certs/wildcard.zero.zenmo.com/fullchain.pem /static_certs/wildcard.zero.zenmo.com/privkey.pem + deploy: + resources: + limits: + cpus: "2" + memory: 2G + + ztor: + image: ghcr.io/zenmo/ztor:${TAG} + environment: + POSTGRES_URL: jdbc:postgresql://postgres.zenmo.com:5432/${DB_NAME} + POSTGRES_USER: ${DB_NAME} + POSTGRES_PASSWORD: ${DB_PASSWORD} + AZURE_STORAGE_ACCOUNT_NAME: zerostore + AZURE_STORAGE_ACCOUNT_KEY: ${AZURE_STORAGE_ACCOUNT_KEY} + AZURE_STORAGE_CONTAINER: ${AZURE_STORAGE_CONTAINER} + CORS_ALLOW_ORIGIN_PATTERN: ${CORS_ALLOW_ORIGIN_PATTERN} + OAUTH_CLIENT_ID: ${OAUTH_CLIENT_ID} + OAUTH_CLIENT_SECRET: ${OAUTH_CLIENT_SECRET} + BASE_URL: https://${ZTOR_HOSTNAME} + networks: + - caddy_default + - postgres_default + labels: + caddy: ${ZTOR_HOSTNAME} + caddy.reverse_proxy: "{{upstreams 8082}}" + caddy.tls: /static_certs/wildcard.zero.zenmo.com/fullchain.pem /static_certs/wildcard.zero.zenmo.com/privkey.pem + deploy: + resources: + limits: + cpus: "4" + memory: 8G + +networks: + caddy_default: + external: true + postgres_default: + external: true diff --git a/docker/production/frontend/Caddyfile b/docker/production/frontend/Caddyfile new file mode 100644 index 00000000..b0200d34 --- /dev/null +++ b/docker/production/frontend/Caddyfile @@ -0,0 +1,5 @@ +:2015 { + root /srv + file_server + try_files {path} /index.html +} diff --git a/docker/production/frontend/Dockerfile b/docker/production/frontend/Dockerfile new file mode 100644 index 00000000..efa3cec1 --- /dev/null +++ b/docker/production/frontend/Dockerfile @@ -0,0 +1,30 @@ +# syntax=docker/dockerfile:1.7-labs +# for COPY --exclude support +FROM gradle:8.10.0-jdk21 AS gradle +# TODO: copies too much, breaks caching +COPY --chown=gradle:gradle . /home/gradle/src +WORKDIR /home/gradle/src +RUN --mount=type=cache,target=/home/gradle/.gradle/caches gradle zummon:jsBrowserProductionLibraryDistribution --no-daemon + +FROM node:22 AS node +ARG VITE_ZTOR_URL + +RUN mkdir -p /app/frontend +RUN mkdir -p /app/build/js/packages +WORKDIR /app/frontend +COPY --from=gradle /home/gradle/src/build/js /app/build/js + +# first do this so it is cached in a docker layer +COPY frontend/package.json frontend/package-lock.json ./ +RUN --mount=type=cache,target=/root/.npm npm install + +COPY --exclude=frontend/dist --exclude=frontend/node_modules frontend* ./ +RUN npm run build + +FROM caddy:2.8.4 + +COPY docker/production/frontend/Caddyfile /etc/caddy/Caddyfile +COPY --from=node /app/frontend/dist /srv +RUN caddy validate --config /etc/caddy/Caddyfile + +CMD caddy run --config /etc/caddy/Caddyfile diff --git a/Dockerfile b/docker/production/ztor/Dockerfile similarity index 86% rename from Dockerfile rename to docker/production/ztor/Dockerfile index 20845401..764f59ba 100644 --- a/Dockerfile +++ b/docker/production/ztor/Dockerfile @@ -1,6 +1,7 @@ ## For production FROM gradle:8.10.0-jdk21 AS build +# TODO: gradle projects should be put in a separate directory COPY --chown=gradle:gradle . /home/gradle/src WORKDIR /home/gradle/src RUN --mount=type=cache,target=/home/gradle/.gradle/caches gradle ztor:buildFatJar --no-daemon diff --git a/frontend/src/excel-import/feedback.tsx b/frontend/src/excel-import/feedback.tsx index d171936b..8d4279e6 100644 --- a/frontend/src/excel-import/feedback.tsx +++ b/frontend/src/excel-import/feedback.tsx @@ -1,5 +1,5 @@ import {FunctionComponent} from "react" -import {SurveyWithErrors, SurveyValidator, ValidationResult, Status, KtList} from "zero-zummon" +import {SurveyWithErrors, surveyValidator, ValidationResult, Status, KtList} from "zero-zummon" import {Button} from "primereact/button" import {MessageDisplay} from "./message-display" import { Panel } from 'primereact/panel'; @@ -14,8 +14,7 @@ export const Feedback: FunctionComponent<{ surveyWithErrors: SurveyWithErrors navigateNext: () => void }> = ({surveyWithErrors, navigateNext}) => { - const surveyValidator = new SurveyValidator() - const results = surveyValidator.validate(surveyWithErrors.survey) + const results = surveyValidator.get().validate(surveyWithErrors.survey) return ( <> diff --git a/github-actions/get-variables.js b/github-actions/get-variables.js index 32b1fc67..8858aafa 100644 --- a/github-actions/get-variables.js +++ b/github-actions/get-variables.js @@ -29,10 +29,16 @@ module.exports = (context) => { .substring(0, maxBranchLength) .replaceAll(/-*$/g, '') // remove trailing dashes because it would lead to an invalid name + const versionIdentifier = `${shortBranch}-${context.runNumber}` + return { - ZTOR_PR_CONTAINER_APP_NAME: `${containerAppBaseName}-${shortBranch}-${context.runNumber}`, - VERSION_TAG: `${shortBranch}-${context.runNumber}-${shortCommit}`, + ZTOR_PR_CONTAINER_APP_NAME: `${containerAppBaseName}-${versionIdentifier}`, + VERSION_TAG: `${versionIdentifier}-${shortCommit}`, GITHUB_ENVIRONMENT: getEnvironment(context), + GITHUB_SWARM_ENVIRONMENT: `swarm-${getEnvironment(context)}`, + // handled by wildcard certificate + FRONTEND_HOSTNAME: `frontend-${versionIdentifier}.zero.zenmo.com`, + ZTOR_HOSTNAME: `ztor-${versionIdentifier}.zero.zenmo.com`, } } diff --git a/zummon/src/commonMain/kotlin/companysurvey/Validation.kt b/zummon/src/commonMain/kotlin/companysurvey/Validation.kt index 6f1aadd0..653708a8 100644 --- a/zummon/src/commonMain/kotlin/companysurvey/Validation.kt +++ b/zummon/src/commonMain/kotlin/companysurvey/Validation.kt @@ -2,6 +2,8 @@ package com.zenmo.zummon.companysurvey import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport +@OptIn(ExperimentalJsExport::class) +@JsExport fun interface Validator { fun validate(item: T): List }