From 4b5f1079f2c05288a27f155cabf1a49f84490b0d Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 13:13:57 +0200 Subject: [PATCH 01/16] feat: allow deploying to hetzner --- .env.example | 40 ++++ .github/workflows/awsdeploy.yml | 80 ------- .github/workflows/deploy-template.yml | 137 ------------ .github/workflows/deploy.yml | 261 ++++++++++++++++++++++ .github/workflows/e2e.yml | 66 +++--- .github/workflows/ecrbuild-all.yml | 89 -------- .github/workflows/ecrbuild-template.yml | 143 ------------ .github/workflows/ghcr-build-all.yml | 51 +++++ .github/workflows/ghcr-build-template.yml | 84 +++++++ .github/workflows/on_main.yml | 48 +--- .github/workflows/on_pr.yml | 67 ++---- .github/workflows/preview.yml | 172 ++++++++++++++ .github/workflows/pull-preview-script.sh | 7 - .github/workflows/pull-preview.yml | 93 -------- .gitignore | 7 + docker-compose.preview.pr.yml | 7 - docker-compose.preview.sandbox.yml | 7 - docker-compose.preview.yml | 82 ------- infra/.env.example | 46 ++++ infra/.env.preview.enc | 51 +++++ infra/.sops.yaml | 16 ++ infra/Caddyfile | 40 ++++ infra/Caddyfile.preview | 53 +++++ infra/import-backup.sh | 29 +++ infra/stack.preview.yml | 158 +++++++++++++ infra/stack.yml | 153 +++++++++++++ package.json | 14 +- 27 files changed, 1218 insertions(+), 783 deletions(-) create mode 100644 .env.example delete mode 100644 .github/workflows/awsdeploy.yml delete mode 100644 .github/workflows/deploy-template.yml create mode 100644 .github/workflows/deploy.yml delete mode 100644 .github/workflows/ecrbuild-all.yml delete mode 100644 .github/workflows/ecrbuild-template.yml create mode 100644 .github/workflows/ghcr-build-all.yml create mode 100644 .github/workflows/ghcr-build-template.yml create mode 100644 .github/workflows/preview.yml delete mode 100644 .github/workflows/pull-preview-script.sh delete mode 100644 .github/workflows/pull-preview.yml delete mode 100644 docker-compose.preview.pr.yml delete mode 100644 docker-compose.preview.sandbox.yml delete mode 100644 docker-compose.preview.yml create mode 100644 infra/.env.example create mode 100644 infra/.env.preview.enc create mode 100644 infra/.sops.yaml create mode 100644 infra/Caddyfile create mode 100644 infra/Caddyfile.preview create mode 100755 infra/import-backup.sh create mode 100644 infra/stack.preview.yml create mode 100644 infra/stack.yml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..12034ca718 --- /dev/null +++ b/.env.example @@ -0,0 +1,40 @@ +# Base environment configuration +# Copy this to .env and customize as needed +# Values here are defaults that work across development, testing, and self-hosting + +# Database configuration +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=postgres +POSTGRES_PORT=54322 + +# Cache configuration +VALKEY_HOST=localhost +VALKEY_PORT=6379 + +# Minio configuration +MINIO_ROOT_USER=pubpub-admin +MINIO_ROOT_PASSWORD=pubpub-admin +ASSETS_BUCKET_NAME=assets.pubpub.local +ASSETS_UPLOAD_KEY=pubpubuser +ASSETS_UPLOAD_SECRET_KEY=pubpubpass +ASSETS_REGION=us-east-1 +ASSETS_STORAGE_ENDPOINT=http://localhost:9000 + +# Email configuration +MAILGUN_SMTP_HOST=localhost +MAILGUN_SMTP_PORT=54325 +MAILGUN_SMTP_USERNAME=xxx +MAILGUN_SMTP_PASSWORD=xxx + +# Application configuration +API_KEY=super_secret_key +PUBPUB_URL=http://localhost:3000 + +# Other configuration +OTEL_SERVICE_NAME=pubpub-v7-dev +HONEYCOMB_API_KEY=xxx + +# Volume types (can be overridden per environment) +DB_VOLUME_TYPE=postgres_data +MINIO_VOLUME_TYPE=minio_data \ No newline at end of file diff --git a/.github/workflows/awsdeploy.yml b/.github/workflows/awsdeploy.yml deleted file mode 100644 index 52459d83f9..0000000000 --- a/.github/workflows/awsdeploy.yml +++ /dev/null @@ -1,80 +0,0 @@ -# Based on https://docs.github.com/en/actions/deployment/deploying-to-your-cloud-provider/deploying-to-amazon-elastic-container-service - -name: aws ecs deploy - -on: - workflow_call: - inputs: - proper-name: - required: true - type: string - environment: - required: true - type: string - image-tag-override: # example: latest, 7037e37a18a379d583164441baff9e594cc479f8 - type: string # use this to force a container version. - secrets: - AWS_ACCESS_KEY_ID: - required: true - AWS_SECRET_ACCESS_KEY: - required: true - - #dispatch event means you can call it from the Github UI and set inputs on a form. - # MUST match the inputs of the workflow_call. - workflow_dispatch: - inputs: - proper-name: - required: true - type: string - environment: - required: true - type: string - image-tag-override: # example: latest, 7037e37a18a379d583164441baff9e594cc479f8 - type: string # use this to force a container version. - -jobs: - deploy-core: - uses: ./.github/workflows/deploy-template.yml - with: - service: core - environment: ${{ inputs.environment }} - proper-name: ${{ inputs.proper-name }} - image-tag-override: ${{ inputs.image-tag-override }} - secrets: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - - deploy-jobs: - uses: ./.github/workflows/deploy-template.yml - needs: deploy-core - with: - service: jobs - environment: ${{ inputs.environment }} - proper-name: ${{ inputs.proper-name }} - image-tag-override: ${{ inputs.image-tag-override }} - secrets: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - - deploy-bastion: - uses: ./.github/workflows/deploy-template.yml - with: - service: bastion - environment: ${{ inputs.environment }} - proper-name: ${{ inputs.proper-name }} - repo-name-override: pubpub-v7 - image-tag-override: ${{ inputs.image-tag-override }} - secrets: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - - deploy-site-builder: - uses: ./.github/workflows/deploy-template.yml - with: - service: site-builder - environment: ${{ inputs.environment }} - proper-name: ${{ inputs.proper-name }} - image-tag-override: ${{ inputs.image-tag-override }} - secrets: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} diff --git a/.github/workflows/deploy-template.yml b/.github/workflows/deploy-template.yml deleted file mode 100644 index d98a0370c8..0000000000 --- a/.github/workflows/deploy-template.yml +++ /dev/null @@ -1,137 +0,0 @@ -# Based on https://docs.github.com/en/actions/deployment/deploying-to-your-cloud-provider/deploying-to-amazon-elastic-container-service - -name: aws ecs deploy template - -on: - workflow_call: - inputs: - service: # example: core - required: true - type: string - proper-name: # example: blake - required: true - type: string - environment: # example: staging - required: true - type: string - repo-name-override: - type: string - image-tag-override: # example: latest, 7037e37a18a379d583164441baff9e594cc479f8 - type: string # use this to force a container version. - secrets: - AWS_ACCESS_KEY_ID: - required: true - AWS_SECRET_ACCESS_KEY: - required: true - workflow_dispatch: - inputs: - service: # example: core - required: true - type: string - proper-name: # example: blake - required: true - type: string - environment: # example: staging - required: true - type: string - repo-name-override: - type: string - image-tag-override: # example: latest, 7037e37a18a379d583164441baff9e594cc479f8 - type: string # use this to force a container version. - -env: - AWS_REGION: us-east-1 - ECR_REPOSITORY_PREFIX: pubpub-v7 - ECR_REPOSITORY_NAME_OVERRIDE: ${{ inputs.repo-name-override }} - ECS_SERVICE: ${{ inputs.proper-name }}-${{inputs.service}} - ECS_CLUSTER: ${{inputs.proper-name}}-ecs-cluster-${{inputs.environment}} - ECS_TASK_DEFINITION_TEMPLATE: ${{ inputs.proper-name }}-${{inputs.service}} - CONTAINER_NAME: ${{inputs.service}} - -jobs: - deploy: - name: Deploy - runs-on: ubuntu-latest - environment: ${{ inputs.proper-name }}-${{ inputs.environment }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ vars.IAM_ROLE_TO_ASSUME }} - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ env.AWS_REGION }} - - - name: Get image tag based on SHA - id: gettag - env: - OVERRIDE: ${{inputs.image-tag-override}} - # use shell substitution - run: echo "tag=${OVERRIDE:-$(git describe --always --abbrev=40 --dirty)}" >> $GITHUB_OUTPUT - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Retrieve Task Definition contents from template - id: get-taskdef - run: | - aws ecs describe-task-definition \ - --task-definition $ECS_TASK_DEFINITION_TEMPLATE \ - --query taskDefinition >> template_task_def.json - - - name: Get image labels - id: label - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - IMAGE_TAG: ${{ steps.gettag.outputs.tag }} - run: | - echo "label=$ECR_REGISTRY/${ECR_REPOSITORY_NAME_OVERRIDE:-$ECR_REPOSITORY_PREFIX-${CONTAINER_NAME}}:$IMAGE_TAG" >> $GITHUB_OUTPUT - echo "base_label=$ECR_REGISTRY/$ECR_REPOSITORY_PREFIX:$IMAGE_TAG" >> $GITHUB_OUTPUT - - - name: Fill in the new image ID in the Amazon ECS task definition - id: task-def-service - uses: aws-actions/amazon-ecs-render-task-definition@c804dfbdd57f713b6c079302a4c01db7017a36fc - with: - task-definition: template_task_def.json - container-name: ${{ env.CONTAINER_NAME }} - image: ${{ steps.label.outputs.label }} - - # Complication when the number of containers in the task are unknown: - # we have to know where to get the inputs for each step, including the upload - # step. - - name: Fill in the new image ID in the Amazon ECS task definition for migrations - id: task-def-migration - if: inputs.service == 'core' - uses: aws-actions/amazon-ecs-render-task-definition@c804dfbdd57f713b6c079302a4c01db7017a36fc - with: - task-definition: ${{ steps.task-def-service.outputs.task-definition }} - container-name: migrations - image: ${{ steps.label.outputs.base_label }} - - - name: Deploy Amazon ECS task definition - id: deploy-service-only - # This one is different. The single-image case is when not deploying core. - if: inputs.service != 'core' - uses: aws-actions/amazon-ecs-deploy-task-definition@16f052ed696e6e5bf88c208a8e5ba1af7ced3310 - with: - # it is because of this line that the two steps need different if conditions - task-definition: ${{ steps.task-def-service.outputs.task-definition }} - service: ${{ env.ECS_SERVICE }} - cluster: ${{ env.ECS_CLUSTER }} - wait-for-service-stability: true - - - name: Deploy Amazon ECS task definition including migrations - id: deploy-service-and-migrations - if: inputs.service == 'core' - uses: aws-actions/amazon-ecs-deploy-task-definition@16f052ed696e6e5bf88c208a8e5ba1af7ced3310 - with: - # it is because of this line that the two steps need different if conditions - task-definition: ${{ steps.task-def-migration.outputs.task-definition }} - service: ${{ env.ECS_SERVICE }} - cluster: ${{ env.ECS_CLUSTER }} - wait-for-service-stability: true diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000000..c131c2a639 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,261 @@ +name: Deploy to Hetzner + +run-name: >- + ${{ + github.event_name == 'release' && format('Deploy prod: {0}', github.event.release.tag_name) || + github.event_name == 'workflow_run' && format('Deploy staging: {0}', github.event.workflow_run.head_commit.message) || + format('Deploy: {0}', github.sha) + }} + +concurrency: + group: >- + deploy-${{ + github.event_name == 'release' && format('release-{0}', github.event.release.tag_name) || + github.event_name == 'workflow_run' && format('ci-{0}', github.event.workflow_run.head_branch) || + github.ref + }} + cancel-in-progress: true + +on: + workflow_run: + workflows: [CI] + types: [completed] + branches: [main] + workflow_dispatch: + inputs: + skip_ci_check: + description: Deploy even if CI failed + required: false + default: false + type: boolean + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + if: | + github.event_name == 'workflow_dispatch' || + github.event_name == 'release' || + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') + + permissions: + contents: read + packages: write + + outputs: + image_tag: ${{ steps.vars.outputs.image_tag }} + host: ${{ steps.vars.outputs.host }} + env_file: ${{ steps.vars.outputs.env_file }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha || github.sha }} + + - name: Set deployment vars + id: vars + run: | + if [[ "${{ github.event_name }}" == "release" ]]; then + echo "image_tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT + echo "host=${{ secrets.SSH_HOST_PROD }}" >> $GITHUB_OUTPUT + echo "env_file=.env.enc" >> $GITHUB_OUTPUT + echo "publish_latest=false" >> $GITHUB_OUTPUT + else + echo "image_tag=${{ github.sha }}" >> $GITHUB_OUTPUT + echo "host=${{ secrets.SSH_HOST_STAGING }}" >> $GITHUB_OUTPUT + echo "env_file=.env.staging.enc" >> $GITHUB_OUTPUT + echo "publish_latest=true" >> $GITHUB_OUTPUT + fi + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push platform + uses: docker/build-push-action@v6 + with: + context: . + push: true + provenance: false + sbom: false + cache-from: type=gha,scope=platform + cache-to: type=gha,mode=max,scope=platform + build-args: | + PACKAGE=core + CI=true + target: next-app-core + tags: | + ghcr.io/pubpub/platform:${{ steps.vars.outputs.image_tag }} + ${{ steps.vars.outputs.publish_latest == 'true' && 'ghcr.io/pubpub/platform:latest' || '' }} + platforms: linux/amd64 + + - name: Build and push platform-migrations + uses: docker/build-push-action@v6 + with: + context: . + push: true + provenance: false + sbom: false + cache-from: type=gha,scope=platform-migrations + cache-to: type=gha,mode=max,scope=platform-migrations + build-args: | + CI=true + target: monorepo + tags: | + ghcr.io/pubpub/platform-migrations:${{ steps.vars.outputs.image_tag }} + ${{ steps.vars.outputs.publish_latest == 'true' && 'ghcr.io/pubpub/platform-migrations:latest' || '' }} + platforms: linux/amd64 + + - name: Build and push platform-jobs + uses: docker/build-push-action@v6 + with: + context: . + push: true + provenance: false + sbom: false + cache-from: type=gha,scope=platform-jobs + cache-to: type=gha,mode=max,scope=platform-jobs + build-args: | + PACKAGE=jobs + CI=true + target: jobs + tags: | + ghcr.io/pubpub/platform-jobs:${{ steps.vars.outputs.image_tag }} + ${{ steps.vars.outputs.publish_latest == 'true' && 'ghcr.io/pubpub/platform-jobs:latest' || '' }} + platforms: linux/amd64 + + - name: Build and push platform-site-builder + uses: docker/build-push-action@v6 + with: + context: . + push: true + provenance: false + sbom: false + cache-from: type=gha,scope=platform-site-builder + cache-to: type=gha,mode=max,scope=platform-site-builder + build-args: | + PACKAGE=site-builder + CI=true + target: jobs + tags: | + ghcr.io/pubpub/platform-site-builder:${{ steps.vars.outputs.image_tag }} + ${{ steps.vars.outputs.publish_latest == 'true' && 'ghcr.io/pubpub/platform-site-builder:latest' || '' }} + platforms: linux/amd64 + + deploy: + needs: build + runs-on: ubuntu-latest + + steps: + - name: Start SSH agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Add known hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan -H "${{ needs.build.outputs.host }}" >> ~/.ssh/known_hosts + + - name: Deploy over SSH + env: + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST: ${{ needs.build.outputs.host }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.ref_name }} + GHCR_USER: ${{ secrets.GHCR_USER }} + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} + IMAGE_TAG: ${{ needs.build.outputs.image_tag }} + ENV_FILE: ${{ needs.build.outputs.env_file }} + run: | + ssh "${SSH_USER}@${SSH_HOST}" \ + "env GHCR_USER='${GHCR_USER}' GHCR_TOKEN='${GHCR_TOKEN}' IMAGE_TAG='${IMAGE_TAG}' ENV_FILE='${ENV_FILE}' bash -s -- '${REPO}' '${BRANCH}'" <<'EOS' + set -euo pipefail + + REPO="${1:?missing repo}" + BRANCH="${2:-main}" + + : "${IMAGE_TAG:?missing IMAGE_TAG}" + : "${GHCR_USER:?missing GHCR_USER}" + : "${GHCR_TOKEN:?missing GHCR_TOKEN}" + + REPO_NAME="${REPO##*/}" + APP_DIR="/srv/${REPO_NAME}" + REPO_SSH="git@github.com:${REPO}.git" + + if [[ -z "$REPO_NAME" || -z "$APP_DIR" ]]; then + echo "bad derived paths: REPO='$REPO' REPO_NAME='$REPO_NAME' APP_DIR='$APP_DIR'" + exit 1 + fi + + ssh-keyscan -H github.com >> ~/.ssh/known_hosts 2>/dev/null + chmod 600 ~/.ssh/known_hosts + + if [[ ! -d "${APP_DIR}/.git" ]]; then + sudo mkdir -p "${APP_DIR}" + sudo chown -R "$USER:$USER" "${APP_DIR}" + git clone --branch "${BRANCH}" "${REPO_SSH}" "${APP_DIR}" + fi + + cd "${APP_DIR}" + git fetch --prune --tags origin + git checkout --detach "${IMAGE_TAG}" 2>/dev/null || git checkout --detach "origin/${BRANCH}" + + cd infra + umask 077 + + : "${ENV_FILE:?missing ENV_FILE}" + sops -d --input-type dotenv --output-type dotenv "$ENV_FILE" > .env + + if ! sudo docker info --format '{{.Swarm.LocalNodeState}}' | grep -qx active; then + sudo docker swarm init --advertise-addr "$(hostname -I | awk '{print $1}')" + fi + + echo "$GHCR_TOKEN" | sudo docker login ghcr.io -u "$GHCR_USER" --password-stdin + + echo "deploying with IMAGE_TAG=$IMAGE_TAG" + + sudo env IMAGE_TAG="$IMAGE_TAG" \ + docker stack deploy -c stack.yml \ + --with-registry-auth --resolve-image always --prune \ + pubpub + + sudo docker stack services pubpub + sudo docker image prune -f + + # wait for platform rollout + wait_rollout() { + echo "waiting for rollout of $1..." + svc="$1" + timeout="${2:-600}" + end=$((SECONDS+timeout)) + + while (( SECONDS < end )); do + desired="$(sudo docker service inspect "$svc" --format '{{.Spec.Mode.Replicated.Replicas}}' 2>/dev/null || echo "")" + running="$(sudo docker service ps "$svc" --filter desired-state=running --format '{{.CurrentState}}' 2>/dev/null | grep -c '^Running' || true)" + state="$(sudo docker service inspect "$svc" --format '{{if .UpdateStatus}}{{.UpdateStatus.State}}{{end}}' 2>/dev/null || echo "")" + echo " $svc: desired=$desired running=$running state=$state" + + if [[ -n "$desired" && "$running" == "$desired" ]] && { [[ -z "$state" ]] || [[ "$state" == "completed" ]]; }; then + echo " $svc rollout complete" + return 0 + fi + + sleep 5 + done + + echo "rollout timeout for $svc" + return 1 + } + + wait_rollout pubpub_platform 600 + + EOS diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a49b764d75..9861c4219f 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,26 +1,21 @@ on: workflow_call: inputs: - image-tag-override: # example: latest, 7037e37a18a379d583164441baff9e594cc479f8 - type: string # use this to force a container version. - secrets: - AWS_ACCESS_KEY_ID: - required: true - AWS_SECRET_ACCESS_KEY: - required: true + image-tag-override: + type: string env: CI: true - AWS_REGION: us-east-1 - - ECR_REPOSITORY_PREFIX: pubpub-v7 - CONTAINER_NAME: core jobs: integration-tests: name: Integration tests runs-on: ubuntu-latest + permissions: + contents: read + packages: read + strategy: matrix: package: @@ -52,7 +47,6 @@ jobs: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - name: Setup pnpm cache - # since this always runs after CI, there's no need to save the cache afterwards, since it's guaranteed to be the same uses: actions/cache/restore@v4 with: path: ${{ steps.get-store-path.outputs.STORE_PATH }} @@ -60,7 +54,6 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm-store- - # mostly to skip preconstruct build - name: Setup turbo cache uses: actions/cache/restore@v4 with: @@ -69,15 +62,7 @@ jobs: restore-keys: | ${{ runner.os }}-turbo- - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ vars.IAM_ROLE_TO_ASSUME }} - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ env.AWS_REGION }} - - - name: Get image tag based on SHA + - name: Get image tag id: gettag run: | if [ -n "${{ inputs.image-tag-override }}" ]; then @@ -88,20 +73,22 @@ jobs: echo "Using current SHA as image tag" fi - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Get image labels + - name: Compute image refs id: label env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} IMAGE_TAG: ${{ steps.gettag.outputs.tag }} run: | - echo "core_label=$ECR_REGISTRY/${ECR_REPOSITORY_NAME_OVERRIDE:-$ECR_REPOSITORY_PREFIX-core}:$IMAGE_TAG" >> $GITHUB_OUTPUT - echo "jobs_label=$ECR_REGISTRY/${ECR_REPOSITORY_NAME_OVERRIDE:-$ECR_REPOSITORY_PREFIX-jobs}:$IMAGE_TAG" >> $GITHUB_OUTPUT - echo "base_label=$ECR_REGISTRY/$ECR_REPOSITORY_PREFIX:$IMAGE_TAG" >> $GITHUB_OUTPUT - echo "site_builder_label=$ECR_REGISTRY/${ECR_REPOSITORY_NAME_OVERRIDE:-$ECR_REPOSITORY_PREFIX-site-builder}:$IMAGE_TAG" >> $GITHUB_OUTPUT + echo "core_label=ghcr.io/pubpub/platform:$IMAGE_TAG" >> $GITHUB_OUTPUT + echo "jobs_label=ghcr.io/pubpub/platform-jobs:$IMAGE_TAG" >> $GITHUB_OUTPUT + echo "base_label=ghcr.io/pubpub/platform-migrations:$IMAGE_TAG" >> $GITHUB_OUTPUT + echo "site_builder_label=ghcr.io/pubpub/platform-site-builder:$IMAGE_TAG" >> $GITHUB_OUTPUT - name: Install dependencies run: pnpm install --frozen-lockfile --prefer-offline @@ -115,7 +102,6 @@ jobs: - name: Run migrations and seed run: pnpm --filter core reset-base env: - # 20241126: this prevents the arcadia seed from running, which contains a ton of pubs which potentially might slow down the tests MINIMAL_SEED: true SKIP_VALIDATION: true @@ -124,16 +110,16 @@ jobs: - name: Start up core etc run: pnpm integration:setup env: - INTEGRATION_TESTS_IMAGE: ${{steps.label.outputs.core_label}} - SITE_BUILDER_IMAGE: ${{steps.label.outputs.site_builder_label}} - JOBS_IMAGE: ${{steps.label.outputs.jobs_label}} + INTEGRATION_TESTS_IMAGE: ${{ steps.label.outputs.core_label }} + SITE_BUILDER_IMAGE: ${{ steps.label.outputs.site_builder_label }} + JOBS_IMAGE: ${{ steps.label.outputs.jobs_label }} - name: Log out Container ID for health check id: log-container-id run: echo "CONTAINER_ID=$(docker compose -f docker-compose.test.yml ps integration-tests -q)" >> $GITHUB_OUTPUT - name: Wait until container is healthy - run: while [ "`docker inspect -f {{.State.Health.Status}} ${{steps.log-container-id.outputs.CONTAINER_ID}}`" != "healthy" ]; do sleep .2; done + run: while [ "`docker inspect -f {{.State.Health.Status}} ${{ steps.log-container-id.outputs.CONTAINER_ID }}`" != "healthy" ]; do sleep .2; done - name: Run integration tests run: pnpm playwright:test --filter ${{ matrix.package }} --env-mode=loose @@ -143,12 +129,12 @@ jobs: DATABASE_URL: postgresql://postgres:postgres@localhost:54322/postgres - name: Print container logs - if: ${{failure() || cancelled()}} + if: ${{ failure() || cancelled() }} run: docker compose -f docker-compose.test.yml --profile integration logs -t env: - INTEGRATION_TESTS_IMAGE: ${{steps.label.outputs.core_label}} - SITE_BUILDER_IMAGE: ${{steps.label.outputs.site_builder_label}} - JOBS_IMAGE: ${{steps.label.outputs.jobs_label}} + INTEGRATION_TESTS_IMAGE: ${{ steps.label.outputs.core_label }} + SITE_BUILDER_IMAGE: ${{ steps.label.outputs.site_builder_label }} + JOBS_IMAGE: ${{ steps.label.outputs.jobs_label }} - name: Upload core playwright snapshots artifact if: failure() && matrix.package == 'core' diff --git a/.github/workflows/ecrbuild-all.yml b/.github/workflows/ecrbuild-all.yml deleted file mode 100644 index 11d9af56da..0000000000 --- a/.github/workflows/ecrbuild-all.yml +++ /dev/null @@ -1,89 +0,0 @@ -# Based on https://docs.github.com/en/actions/deployment/deploying-to-your-cloud-provider/deploying-to-amazon-elastic-container-service - -name: docker build to ECR - -on: - workflow_call: - secrets: - AWS_ACCESS_KEY_ID: - required: true - AWS_SECRET_ACCESS_KEY: - required: true - inputs: - publish_to_ghcr: - type: boolean - default: false - outputs: - core-image: - description: "Core image SHA" - value: ${{ jobs.build-core.outputs.image-sha }} - base-image: - description: "Base image SHA" - value: ${{ jobs.build-base.outputs.image-sha }} - jobs-image: - description: "Jobs image SHA" - value: ${{ jobs.build-jobs.outputs.image-sha }} - site-builder-image: - description: "Site builder image SHA" - value: ${{ jobs.build-site-builder.outputs.image-sha }} - -jobs: - emit-sha-tag: - name: Emit container tag sha - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - - name: Get image tag - id: label - run: | - sha_short=$(git describe --always --abbrev=40 --dirty) - echo "Building containers with tag:" - echo "$sha_short" - - build-base: - uses: ./.github/workflows/ecrbuild-template.yml - with: - publish_to_ghcr: ${{ inputs.publish_to_ghcr }} - ghcr_image_name: platform-migrations - secrets: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - - build-core: - uses: ./.github/workflows/ecrbuild-template.yml - # needs: - # - build-base - with: - package: core - publish_to_ghcr: ${{ inputs.publish_to_ghcr }} - ghcr_image_name: platform - secrets: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - - build-jobs: - uses: ./.github/workflows/ecrbuild-template.yml - # needs: - # - build-base - with: - package: jobs - target: jobs - publish_to_ghcr: ${{ inputs.publish_to_ghcr }} - ghcr_image_name: platform-jobs - secrets: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - - build-site-builder: - uses: ./.github/workflows/ecrbuild-template.yml - with: - package: site-builder - target: jobs - publish_to_ghcr: ${{ inputs.publish_to_ghcr }} - ghcr_image_name: platform-site-builder - secrets: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} diff --git a/.github/workflows/ecrbuild-template.yml b/.github/workflows/ecrbuild-template.yml deleted file mode 100644 index 4fc87f6027..0000000000 --- a/.github/workflows/ecrbuild-template.yml +++ /dev/null @@ -1,143 +0,0 @@ -# Based on https://docs.github.com/en/actions/deployment/deploying-to-your-cloud-provider/deploying-to-amazon-elastic-container-service - -name: aws ecr build template - -on: - workflow_call: - inputs: - package: - type: string - runner: - type: string - default: ubuntu-latest - target: - type: string - publish_to_ghcr: - type: boolean - default: false - ghcr_image_name: - type: string - required: false - outputs: - image-sha: - description: "Image SHA" - value: ${{ jobs.build.outputs.image-sha }} - secrets: - AWS_ACCESS_KEY_ID: - required: true - AWS_SECRET_ACCESS_KEY: - required: true - -env: - PACKAGE: ${{ inputs.package }} - AWS_REGION: us-east-1 # set this to your preferred AWS region, e.g. us-west-1 - ECR_REPOSITORY_PREFIX: pubpub-v7 # set this to your Amazon ECR repository name - TARGET: ${{ inputs.target }} - -jobs: - build: - name: Build - runs-on: ${{ inputs.runner }} - outputs: - image-sha: ${{ steps.label.outputs.label }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ vars.IAM_ROLE_TO_ASSUME }} - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ env.AWS_REGION }} - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # necessary in order to upload build source maps to sentry - - name: Get sentry token - id: sentry-token - uses: aws-actions/aws-secretsmanager-get-secrets@v2 - with: - secret-ids: | - SENTRY_AUTH_TOKEN, ${{ vars.SENTRY_AUTH_TOKEN_ARN }} - - - name: setup docker buildx - uses: docker/setup-buildx-action@v3 - - - name: Create and use a new builder instance - run: | - docker buildx create --name cached-builder --use - - - name: Get image label - id: label - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - run: | - sha_short=$(git describe --always --abbrev=40 --dirty) - if [[ -z $PACKAGE ]] - then - package_suffix="" - echo "target=monorepo" >> $GITHUB_OUTPUT - else - package_suffix="-${PACKAGE}" - echo "target=${TARGET:-next-app-${PACKAGE}}" >> $GITHUB_OUTPUT - fi - echo "label=$ECR_REGISTRY/$ECR_REPOSITORY_PREFIX$package_suffix:$sha_short" >> $GITHUB_OUTPUT - if [[ ${{ inputs.publish_to_ghcr }} == "true" && -n ${{ inputs.ghcr_image_name }} ]] - then - TIMESTAMP=$(date +%Y%m%d-%H%M%S) - - echo "ghcr_latest_label=ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:latest" >> $GITHUB_OUTPUT - - echo "ghcr_sha_label=ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:$sha_short" >> $GITHUB_OUTPUT - - echo "ghcr_timestamp_label=ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:$TIMESTAMP" >> $GITHUB_OUTPUT - fi - - - name: Check if SENTRY_AUTH_TOKEN is set - run: | - if [[ -z ${{ env.SENTRY_AUTH_TOKEN }} ]] - then - echo "SENTRY_AUTH_TOKEN is not set" - exit 1 - fi - - - name: Build, tag, and push image to Amazon ECR - uses: docker/build-push-action@v6 - id: build-image - env: - REGISTRY_REF: ${{steps.login-ecr.outputs.registry}}/${{env.ECR_REPOSITORY_PREFIX}}-${{env.PACKAGE}}:cache - LABEL: ${{ steps.label.outputs.label }} - TARGET: ${{ steps.label.outputs.target }} - SENTRY_AUTH_TOKEN: ${{ env.SENTRY_AUTH_TOKEN }} - with: - context: . - # cache-from: type=registry,ref=${{env.REGISTRY_REF}} - # cache-to: type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=${{env.REGISTRY_REF}} - builder: cached-builder - build-args: | - PACKAGE=${{ inputs.package }} - CI=true - secrets: | - SENTRY_AUTH_TOKEN=${{ env.SENTRY_AUTH_TOKEN }} - target: ${{ steps.label.outputs.target }} - tags: | - ${{ steps.label.outputs.label }} - ${{ steps.label.outputs.ghcr_latest_label }} - ${{ steps.label.outputs.ghcr_sha_label }} - ${{ steps.label.outputs.ghcr_timestamp_label }} - platforms: linux/amd64 - push: true diff --git a/.github/workflows/ghcr-build-all.yml b/.github/workflows/ghcr-build-all.yml new file mode 100644 index 0000000000..3a0e6c955d --- /dev/null +++ b/.github/workflows/ghcr-build-all.yml @@ -0,0 +1,51 @@ +name: docker build to GHCR + +on: + workflow_call: + inputs: + publish_latest: + type: boolean + default: false + outputs: + core-image: + description: "Core image ref" + value: ${{ jobs.build-core.outputs.image-sha }} + base-image: + description: "Base/migrations image ref" + value: ${{ jobs.build-base.outputs.image-sha }} + jobs-image: + description: "Jobs image ref" + value: ${{ jobs.build-jobs.outputs.image-sha }} + site-builder-image: + description: "Site builder image ref" + value: ${{ jobs.build-site-builder.outputs.image-sha }} + +jobs: + build-base: + uses: ./.github/workflows/ghcr-build-template.yml + with: + ghcr_image_name: platform-migrations + publish_latest: ${{ inputs.publish_latest }} + + build-core: + uses: ./.github/workflows/ghcr-build-template.yml + with: + package: core + ghcr_image_name: platform + publish_latest: ${{ inputs.publish_latest }} + + build-jobs: + uses: ./.github/workflows/ghcr-build-template.yml + with: + package: jobs + target: jobs + ghcr_image_name: platform-jobs + publish_latest: ${{ inputs.publish_latest }} + + build-site-builder: + uses: ./.github/workflows/ghcr-build-template.yml + with: + package: site-builder + target: jobs + ghcr_image_name: platform-site-builder + publish_latest: ${{ inputs.publish_latest }} diff --git a/.github/workflows/ghcr-build-template.yml b/.github/workflows/ghcr-build-template.yml new file mode 100644 index 0000000000..5fadfb33b6 --- /dev/null +++ b/.github/workflows/ghcr-build-template.yml @@ -0,0 +1,84 @@ +name: ghcr build template + +on: + workflow_call: + inputs: + package: + type: string + runner: + type: string + default: ubuntu-latest + target: + type: string + ghcr_image_name: + type: string + required: true + publish_latest: + type: boolean + default: false + outputs: + image-sha: + description: "Full GHCR image ref with SHA tag" + value: ${{ jobs.build.outputs.image-sha }} + +jobs: + build: + name: Build + runs-on: ${{ inputs.runner }} + permissions: + contents: read + packages: write + + outputs: + image-sha: ${{ steps.label.outputs.label }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Compute image tags + id: label + run: | + sha_short=$(git describe --always --abbrev=40 --dirty) + + if [[ -z "${{ inputs.package }}" ]]; then + echo "target=monorepo" >> $GITHUB_OUTPUT + else + echo "target=${{ inputs.target || format('next-app-{0}', inputs.package) }}" >> $GITHUB_OUTPUT + fi + + echo "label=ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:$sha_short" >> $GITHUB_OUTPUT + + TAGS="ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:$sha_short" + if [[ "${{ inputs.publish_latest }}" == "true" ]]; then + TAGS="$TAGS,ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:latest" + fi + echo "tags=$TAGS" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + cache-from: type=gha,scope=${{ inputs.ghcr_image_name }} + cache-to: type=gha,mode=max,scope=${{ inputs.ghcr_image_name }} + build-args: | + PACKAGE=${{ inputs.package }} + CI=true + target: ${{ steps.label.outputs.target }} + tags: ${{ steps.label.outputs.tags }} + platforms: linux/amd64 + push: true + provenance: false + sbom: false diff --git a/.github/workflows/on_main.yml b/.github/workflows/on_main.yml index 265f1ae985..6ae645b4f9 100644 --- a/.github/workflows/on_main.yml +++ b/.github/workflows/on_main.yml @@ -1,5 +1,3 @@ -# Based on https://docs.github.com/en/actions/deployment/deploying-to-your-cloud-provider/deploying-to-amazon-elastic-container-service - name: Promote from main on: @@ -13,33 +11,15 @@ jobs: build-all: needs: ci - uses: ./.github/workflows/ecrbuild-all.yml + uses: ./.github/workflows/ghcr-build-all.yml with: - publish_to_ghcr: true - secrets: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + publish_latest: true run-e2e: needs: - ci - build-all uses: ./.github/workflows/e2e.yml - secrets: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - - deploy-all: - uses: ./.github/workflows/awsdeploy.yml - needs: - - build-all - - run-e2e - with: - proper-name: stevie - environment: production - secrets: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} deploy-docs: permissions: @@ -49,27 +29,3 @@ jobs: uses: ./.github/workflows/build-docs.yml with: preview: false - - deploy-preview: - uses: ./.github/workflows/pull-preview.yml - needs: - - build-all - permissions: - contents: read - deployments: write - pull-requests: write - statuses: write - with: - PLATFORM_IMAGE: ${{ needs.build-all.outputs.core-image }} - JOBS_IMAGE: ${{ needs.build-all.outputs.jobs-image }} - MIGRATIONS_IMAGE: ${{ needs.build-all.outputs.base-image }} - SITE_BUILDER_IMAGE: ${{ needs.build-all.outputs.site-builder-image }} - AWS_REGION: "us-east-1" - ALWAYS_ON: "main" - COMPOSE_FILES: docker-compose.preview.sandbox.yml - secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - GH_PAT_PR_PREVIEW_CLEANUP: ${{ secrets.GH_PAT_PR_PREVIEW_CLEANUP }} - PREVIEW_DATACITE_REPOSITORY_ID: ${{ secrets.PREVIEW_DATACITE_REPOSITORY_ID }} - PREVIEW_DATACITE_PASSWORD: ${{ secrets.PREVIEW_DATACITE_PASSWORD }} diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 12a513f65e..d49311dfbb 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -1,14 +1,9 @@ -# Based on https://docs.github.com/en/actions/deployment/deploying-to-your-cloud-provider/deploying-to-amazon-elastic-container-service - name: PR Updated triggers on: pull_request: types: [labeled, unlabeled, synchronize, closed, reopened, opened] -env: - AWS_REGION: us-east-1 - permissions: id-token: write contents: read @@ -30,7 +25,6 @@ jobs: docs: - 'docs/**' - # you can skip the build by adding 'skip-build' to the commit message, useful when testing tests skip_build_sha: outputs: @@ -62,20 +56,17 @@ jobs: run: | pr_number="${{ github.event.pull_request.number }}" - # get all workflow runs for this PR gh api "/repos/${{ github.repository }}/actions/workflows/on_pr.yml/runs?event=pull_request&per_page=100" \ --jq ".workflow_runs[] | select(.pull_requests[]?.number == ${pr_number}) | select(.id < ${{ github.run_id }}) | {id: .id, sha: .head_sha, created: .created_at}" \ | jq -s 'sort_by(.created) | reverse | .[].id' -r \ | while read run_id; do echo "Checking run: $run_id" - # check if build-all job succeeded in this run run=$(gh api "/repos/${{ github.repository }}/actions/runs/${run_id}/jobs") echo "Run: $run" all_success=$(echo "$run" | jq '[.jobs[] | select(.name | contains("build-all")) | .conclusion] | all(. == "success")') echo "All success for $run_id: $all_success" if [ "$all_success" == "true" ]; then - # get the SHA for this run successful_sha=$(gh api "/repos/${{ github.repository }}/actions/runs/${run_id}" --jq '.head_sha') echo "last-successful-build-sha=${successful_sha}" >> $GITHUB_OUTPUT echo "Found last successful build at SHA: $successful_sha (run: $run_id)" @@ -97,10 +88,7 @@ jobs: needs: - path-filter - skip_build_sha - uses: ./.github/workflows/ecrbuild-all.yml - secrets: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + uses: ./.github/workflows/ghcr-build-all.yml e2e: if: (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize') && always() && (needs.build-all.result == 'success' || needs.build-all.result == 'skipped') @@ -111,58 +99,39 @@ jobs: uses: ./.github/workflows/e2e.yml with: image-tag-override: ${{ needs.skip_build_sha.outputs.last-successful-build-sha || '' }} - secrets: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} deploy-preview: - if: needs.build-all.result == 'success' - uses: ./.github/workflows/pull-preview.yml + if: needs.build-all.result == 'success' && contains(github.event.pull_request.labels.*.name, 'preview') + uses: ./.github/workflows/preview.yml needs: - build-all permissions: contents: read - deployments: write pull-requests: write - statuses: write with: - # PLATFORM_IMAGE: 246372085946.dkr.ecr.us-east-1.amazonaws.com/pubpub-v7-core:2b9a81a279c4e405bbedcdbb697c897ded52fbc0 - # JOBS_IMAGE: 246372085946.dkr.ecr.us-east-1.amazonaws.com/pubpub-v7-jobs:c786662f4899de16a621e366a485eca5adda4d6a - # MIGRATIONS_IMAGE: 246372085946.dkr.ecr.us-east-1.amazonaws.com/pubpub-v7:c786662f4899de16a621e366a485eca5adda4d6a - # SITE_BUILDER_IMAGE: 246372085946.dkr.ecr.us-east-1.amazonaws.com/pubpub-v7-site-builder:c786662f4899de16a621e366a485eca5adda4d6a - PLATFORM_IMAGE: ${{ needs.build-all.outputs.core-image }} - JOBS_IMAGE: ${{ needs.build-all.outputs.jobs-image }} - MIGRATIONS_IMAGE: ${{ needs.build-all.outputs.base-image }} - SITE_BUILDER_IMAGE: ${{ needs.build-all.outputs.site-builder-image }} - AWS_REGION: "us-east-1" - COMPOSE_FILES: docker-compose.preview.pr.yml + action: deploy + image_tag: ${{ github.event.pull_request.head.sha }} secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - GH_PAT_PR_PREVIEW_CLEANUP: ${{ secrets.GH_PAT_PR_PREVIEW_CLEANUP }} - PREVIEW_DATACITE_REPOSITORY_ID: ${{ secrets.PREVIEW_DATACITE_REPOSITORY_ID }} - PREVIEW_DATACITE_PASSWORD: ${{ secrets.PREVIEW_DATACITE_PASSWORD }} + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST_PREVIEW: ${{ secrets.SSH_HOST_PREVIEW }} + GHCR_USER: ${{ secrets.GHCR_USER }} + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} close-preview: - uses: ./.github/workflows/pull-preview.yml - if: ${{(github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview')) || (github.event.action == 'unlabeled' && github.event.label.name == 'preview')}} + if: (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview')) || (github.event.action == 'unlabeled' && github.event.label.name == 'preview') + uses: ./.github/workflows/preview.yml permissions: contents: read - deployments: write pull-requests: write - statuses: write with: - PLATFORM_IMAGE: "x" # not used - JOBS_IMAGE: "x" # not used - MIGRATIONS_IMAGE: "x" # not used - SITE_BUILDER_IMAGE: "x" # not used - AWS_REGION: "us-east-1" + action: teardown secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - GH_PAT_PR_PREVIEW_CLEANUP: ${{ secrets.GH_PAT_PR_PREVIEW_CLEANUP }} - PREVIEW_DATACITE_REPOSITORY_ID: ${{ secrets.PREVIEW_DATACITE_REPOSITORY_ID }} - PREVIEW_DATACITE_PASSWORD: ${{ secrets.PREVIEW_DATACITE_PASSWORD }} + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST_PREVIEW: ${{ secrets.SSH_HOST_PREVIEW }} + GHCR_USER: ${{ secrets.GHCR_USER }} + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} deploy-docs-preview: permissions: diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 0000000000..1b7d3cf45e --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,172 @@ +name: PR Preview + +on: + workflow_call: + inputs: + action: + required: true + type: string + description: "'deploy' or 'teardown'" + image_tag: + required: false + type: string + description: "image tag to deploy (only needed for deploy)" + secrets: + SSH_PRIVATE_KEY: + required: true + SSH_USER: + required: true + SSH_HOST_PREVIEW: + required: true + GHCR_USER: + required: true + GHCR_TOKEN: + required: true + +permissions: + contents: read + pull-requests: write + +jobs: + preview: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - name: Get PR number + id: pr + run: | + echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT + echo "stack_name=preview-pr-${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT + echo "host=pr-${{ github.event.pull_request.number }}.preview.pubpub.org" >> $GITHUB_OUTPUT + + - name: Start SSH agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Add known hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan -H "${{ secrets.SSH_HOST_PREVIEW }}" >> ~/.ssh/known_hosts + + - name: Deploy preview stack + if: inputs.action == 'deploy' + env: + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST: ${{ secrets.SSH_HOST_PREVIEW }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + GHCR_USER: ${{ secrets.GHCR_USER }} + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} + IMAGE_TAG: ${{ inputs.image_tag }} + STACK_NAME: ${{ steps.pr.outputs.stack_name }} + PREVIEW_HOST: ${{ steps.pr.outputs.host }} + run: | + ssh "${SSH_USER}@${SSH_HOST}" \ + "env GHCR_USER='${GHCR_USER}' GHCR_TOKEN='${GHCR_TOKEN}' IMAGE_TAG='${IMAGE_TAG}' STACK_NAME='${STACK_NAME}' PREVIEW_HOST='${PREVIEW_HOST}' bash -s -- '${REPO}' '${BRANCH}'" <<'EOS' + set -euo pipefail + + REPO="${1:?missing repo}" + BRANCH="${2:-main}" + + : "${IMAGE_TAG:?missing IMAGE_TAG}" + : "${GHCR_USER:?missing GHCR_USER}" + : "${GHCR_TOKEN:?missing GHCR_TOKEN}" + : "${STACK_NAME:?missing STACK_NAME}" + : "${PREVIEW_HOST:?missing PREVIEW_HOST}" + + REPO_NAME="${REPO##*/}" + APP_DIR="/srv/${REPO_NAME}" + REPO_SSH="git@github.com:${REPO}.git" + + ssh-keyscan -H github.com >> ~/.ssh/known_hosts 2>/dev/null + chmod 600 ~/.ssh/known_hosts + + if [[ ! -d "${APP_DIR}/.git" ]]; then + sudo mkdir -p "${APP_DIR}" + sudo chown -R "$USER:$USER" "${APP_DIR}" + git clone --branch "${BRANCH}" "${REPO_SSH}" "${APP_DIR}" + fi + + cd "${APP_DIR}" + git fetch --prune origin + git checkout "origin/${BRANCH}" --detach + + cd infra + + if ! sudo docker info --format '{{.Swarm.LocalNodeState}}' | grep -qx active; then + sudo docker swarm init --advertise-addr "$(hostname -I | awk '{print $1}')" + fi + + echo "$GHCR_TOKEN" | sudo docker login ghcr.io -u "$GHCR_USER" --password-stdin + + echo "deploying preview stack $STACK_NAME with IMAGE_TAG=$IMAGE_TAG at $PREVIEW_HOST" + + sudo env IMAGE_TAG="$IMAGE_TAG" PREVIEW_HOST="$PREVIEW_HOST" \ + docker stack deploy -c stack.preview.yml \ + --with-registry-auth --resolve-image always \ + "$STACK_NAME" + + sudo docker stack services "$STACK_NAME" + sudo docker image prune -f + + EOS + + - name: Teardown preview stack + if: inputs.action == 'teardown' + env: + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST: ${{ secrets.SSH_HOST_PREVIEW }} + STACK_NAME: ${{ steps.pr.outputs.stack_name }} + run: | + ssh "${SSH_USER}@${SSH_HOST}" \ + "env STACK_NAME='${STACK_NAME}' bash -s" <<'EOS' + set -euo pipefail + : "${STACK_NAME:?missing STACK_NAME}" + + echo "tearing down preview stack $STACK_NAME" + + if sudo docker stack ls --format '{{.Name}}' | grep -qx "$STACK_NAME"; then + sudo docker stack rm "$STACK_NAME" + sleep 10 + # prune volumes for this stack + sudo docker volume ls --filter "label=com.docker.stack.namespace=$STACK_NAME" -q \ + | xargs -r sudo docker volume rm || true + echo "stack $STACK_NAME removed" + else + echo "stack $STACK_NAME not found, nothing to tear down" + fi + + sudo docker image prune -f + + EOS + + - name: Comment on PR + if: inputs.action == 'deploy' + uses: actions/github-script@v7 + with: + script: | + const body = `Preview deployed at https://${{ steps.pr.outputs.host }}`; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: ${{ steps.pr.outputs.number }}, + }); + const existing = comments.find(c => c.body.includes('Preview deployed at')); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: ${{ steps.pr.outputs.number }}, + body, + }); + } diff --git a/.github/workflows/pull-preview-script.sh b/.github/workflows/pull-preview-script.sh deleted file mode 100644 index 6af5557d58..0000000000 --- a/.github/workflows/pull-preview-script.sh +++ /dev/null @@ -1,7 +0,0 @@ -# install latest version of docker compose, by default it's using an ancient version -sudo curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-"$(uname -m)" \ - -o "$(which docker-compose)" && sudo chmod +x "$(which docker-compose)" - -docker image prune -a -f - -df -h diff --git a/.github/workflows/pull-preview.yml b/.github/workflows/pull-preview.yml deleted file mode 100644 index 27a3dba4c0..0000000000 --- a/.github/workflows/pull-preview.yml +++ /dev/null @@ -1,93 +0,0 @@ -on: - workflow_call: - inputs: - PLATFORM_IMAGE: - required: true - type: string - JOBS_IMAGE: - required: true - type: string - MIGRATIONS_IMAGE: - required: true - type: string - SITE_BUILDER_IMAGE: - required: true - type: string - AWS_REGION: - required: true - type: string - ALWAYS_ON: - required: false - type: string - COMPOSE_FILES: - required: false - type: string - secrets: - AWS_ACCESS_KEY_ID: - required: true - AWS_SECRET_ACCESS_KEY: - required: true - GH_PAT_PR_PREVIEW_CLEANUP: - required: true - PREVIEW_DATACITE_REPOSITORY_ID: - required: true - PREVIEW_DATACITE_PASSWORD: - required: true - -permissions: - contents: read - deployments: write - pull-requests: write - statuses: write - -jobs: - preview: - timeout-minutes: 30 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Copy .env file - run: cp ./self-host/.env.example ./self-host/.env - - - name: Configure pullpreview - env: - PLATFORM_IMAGE: ${{ inputs.PLATFORM_IMAGE }} - JOBS_IMAGE: ${{ inputs.JOBS_IMAGE }} - MIGRATIONS_IMAGE: ${{ inputs.MIGRATIONS_IMAGE }} - SITE_BUILDER_IMAGE: ${{ inputs.SITE_BUILDER_IMAGE }} - run: | - sed -i "s|image: PLATFORM_IMAGE|image: $PLATFORM_IMAGE|" docker-compose.preview.yml - sed -i "s|image: JOBS_IMAGE|image: $JOBS_IMAGE|" docker-compose.preview.yml - sed -i "s|image: MIGRATIONS_IMAGE|image: $MIGRATIONS_IMAGE|" docker-compose.preview.yml - sed -i "s|image: SITE_BUILDER_IMAGE|image: $SITE_BUILDER_IMAGE|" docker-compose.preview.yml - sed -i "s|DATACITE_REPOSITORY_ID: DATACITE_REPOSITORY_ID|DATACITE_REPOSITORY_ID: ${{ secrets.PREVIEW_DATACITE_REPOSITORY_ID }}|" docker-compose.preview.yml - sed -i "s|DATACITE_PASSWORD: DATACITE_PASSWORD|DATACITE_PASSWORD: ${{ secrets.PREVIEW_DATACITE_PASSWORD }}|" docker-compose.preview.yml - sed -i "s|email someone@example.com|email dev@pubpub.org|" self-host/caddy/Caddyfile - sed -i "s|example.com|{\$PUBLIC_URL}|" self-host/caddy/Caddyfile - - - name: Get ECR token - id: ecrtoken - run: echo "value=$(aws ecr get-login-password --region us-east-1)" >> $GITHUB_OUTPUT - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: "us-east-1" - - - uses: pullpreview/action@v5 - with: - label: preview - admins: 3mcd - compose_files: ./self-host/docker-compose.yml,docker-compose.preview.yml,${{ inputs.COMPOSE_FILES }} - default_port: 443 - instance_type: small - always_on: ${{ inputs.ALWAYS_ON }} - ports: 80,443 - registries: docker://AWS:${{steps.ecrtoken.outputs.value}}@246372085946.dkr.ecr.us-east-1.amazonaws.com - github_token: ${{ secrets.GH_PAT_PR_PREVIEW_CLEANUP }} - pre_script: "./.github/workflows/pull-preview-script.sh" - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ inputs.AWS_REGION }} - PULLPREVIEW_LOGGER_LEVEL: DEBUG diff --git a/.gitignore b/.gitignore index ccc4f294ca..72421cce93 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,10 @@ storybook-static ./playwright .local_data + +# infra decrypted env files (encrypted versions are tracked) +infra/.env +infra/.env.staging + +!.env*.enc +!.env.example \ No newline at end of file diff --git a/docker-compose.preview.pr.yml b/docker-compose.preview.pr.yml deleted file mode 100644 index c1e1c23609..0000000000 --- a/docker-compose.preview.pr.yml +++ /dev/null @@ -1,7 +0,0 @@ -services: - platform: - environment: - PUBPUB_URL: ${PULLPREVIEW_URL} - caddy: - environment: - PUBLIC_URL: ${PULLPREVIEW_PUBLIC_DNS} diff --git a/docker-compose.preview.sandbox.yml b/docker-compose.preview.sandbox.yml deleted file mode 100644 index 9c9123799c..0000000000 --- a/docker-compose.preview.sandbox.yml +++ /dev/null @@ -1,7 +0,0 @@ -services: - platform: - environment: - PUBPUB_URL: https://sandbox.pubpub.org - caddy: - environment: - PUBLIC_URL: sandbox.pubpub.org diff --git a/docker-compose.preview.yml b/docker-compose.preview.yml deleted file mode 100644 index 6a4f431ca9..0000000000 --- a/docker-compose.preview.yml +++ /dev/null @@ -1,82 +0,0 @@ -services: - platform: - image: PLATFORM_IMAGE - environment: - POSTGRES_USER: preview - POSTGRES_PASSWORD: preview - POSTGRES_DB: preview - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin - ASSETS_UPLOAD_KEY: preview-different - ASSETS_UPLOAD_SECRET_KEY: preview-different123 - ASSETS_STORAGE_ENDPOINT: https://${PULLPREVIEW_PUBLIC_DNS}/a - FLAGS: uploads:off,invites:off,disabled-actions:http+email - ENV_NAME: sandbox - DATACITE_API_URL: https://api.test.datacite.org - DATACITE_REPOSITORY_ID: DATACITE_REPOSITORY_ID - DATACITE_PASSWORD: DATACITE_PASSWORD - - minio-init: - restart: on-failure - environment: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin - ASSETS_UPLOAD_KEY: preview-different - ASSETS_UPLOAD_SECRET_KEY: preview-different123 - - minio: - environment: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin - MINIO_BROWSER_REDIRECT_URL: https://${PULLPREVIEW_PUBLIC_DNS}/assets-ui - # volumes: - # - ./minio:/data - platform-jobs: - image: JOBS_IMAGE - platform-migrations: - image: MIGRATIONS_IMAGE - restart: on-failure - command: pnpm --filter core reset - site-builder: - image: SITE_BUILDER_IMAGE - depends_on: - - platform - - platform-jobs - - minio - ports: - - "4000:4000" - restart: on-failure - networks: - - app-network - environment: - - PUBPUB_URL=http://platform:3000 - - S3_ENDPOINT=http://minio:9000 - - S3_REGION=us-east-1 - - S3_ACCESS_KEY=preview-different - - S3_SECRET_KEY=preview-different123 - - S3_BUCKET_NAME=assets - - PORT=4000 - - SITES_BASE_URL=https://${PULLPREVIEW_PUBLIC_DNS}/sites - - caddy: - image: CADDY_SITES_IMAGE - restart: on-failure - depends_on: - - platform - - platform-jobs - - minio - env_file: .env - environment: - - S3_ENDPOINT=http://minio:9000 - - S3_REGION=us-east-1 - - ASSETS_BUCKET_NAME=assets - - ASSETS_UPLOAD_KEY=preview-different - - ASSETS_UPLOAD_SECRET_KEY=preview-different123 - ports: - - "443:443" - volumes: - - ./caddy:/etc/caddy - - caddy-data:/data - - caddy-config:/config - networks: - - app-network diff --git a/infra/.env.example b/infra/.env.example new file mode 100644 index 0000000000..f8e6ec7ab3 --- /dev/null +++ b/infra/.env.example @@ -0,0 +1,46 @@ +# production environment variables +# copy this to .env, fill in real values, then encrypt with: +# sops -e --input-type dotenv --output-type dotenv .env > .env.enc + +PUBPUB_HOSTNAME=app.pubpub.org +PUBPUB_URL=https://app.pubpub.org + +POSTGRES_USER= +POSTGRES_PASSWORD= +POSTGRES_DB=pubpub +DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} + +PGHOST=db +PGPORT=5432 +PGUSER=${POSTGRES_USER} +PGPASSWORD=${POSTGRES_PASSWORD} +PGDATABASE=${POSTGRES_DB} + +VALKEY_HOST=cache + +MINIO_ROOT_USER= +MINIO_ROOT_PASSWORD= +ASSETS_BUCKET_NAME=assets +ASSETS_UPLOAD_KEY= +ASSETS_UPLOAD_SECRET_KEY= +ASSETS_REGION=us-east-1 +ASSETS_STORAGE_ENDPOINT=http://minio:9000 +ASSETS_PUBLIC_ENDPOINT=https://app.pubpub.org/a + +S3_ENDPOINT=http://minio:9000 +S3_REGION=us-east-1 + +SITE_BUILDER_ENDPOINT=http://site-builder:4000 +SITE_BUILDER_API_KEY= + +JWT_SECRET= +MAILGUN_SMTP_HOST= +MAILGUN_SMTP_PORT=587 +MAILGUN_SMTP_PASSWORD= +MAILGUN_SMTP_USERNAME= +GCLOUD_KEY_FILE=xxx + +OTEL_SERVICE_NAME=pubpub-v7-prod +HONEYCOMB_API_KEY= + +API_KEY= diff --git a/infra/.env.preview.enc b/infra/.env.preview.enc new file mode 100644 index 0000000000..f467e193fb --- /dev/null +++ b/infra/.env.preview.enc @@ -0,0 +1,51 @@ +#ENC[AES256_GCM,data:of7NlDnxPpiEB5vsG/V1x3ZLOs/MKjJDuadN+vv9Kb4R,iv:TMmk9ukk2hkoB1kcG9Q8xHB1cO5aQLZ5r8g+26F01kc=,tag:eOFfFj9IrLuONwACqKIpXw==,type:comment] +#ENC[AES256_GCM,data:PtQw+iKvi39aNyMJ0vN0P6djoAzotLKsbQQ/Pl18TwteSNWRI9aq5FLiMPj6LAq+jmNpyBM5KiSHdTg=,iv:QySN5PpC8CROiICNnFTHMY24RMaZbkIrLbCEQWW/gnQ=,tag:EMltaYcGdFo9F+lsfXi0iw==,type:comment] +#ENC[AES256_GCM,data:GAOzJxMiPKOg9UEEwkZU4uY5OvtCyJr57M77gYFq8g8OqKOu1md+ZanhUZ9VzUujs3CE2sr64Z2Ommnj9N9wnNvSFA==,iv:3f5ncmBejz4v/zayzEJH1nkVS6D0nLM6SeAHdkGN6jk=,tag:ga6J1rGmpezcirtl+z1S3Q==,type:comment] +PUBPUB_HOSTNAME=ENC[AES256_GCM,data:bLWlWiwmE4P+79EBTPMB9yGZUbNV/oe1muxjOUcI9g==,iv:u3gYXAUajl40QSOHANO+E9/fzqEQuLSva7zVC+27prM=,tag:tdagMgYZ/AnOvf1DZImQdg==,type:str] +PUBPUB_URL=ENC[AES256_GCM,data:XMpckdKMM7um9cgLm3ejKv1496WrEd8bnFMMeoSzsu7VVlcNEu0p,iv:8l+hwEEVkKzM/rWh3vU4mBXP9Zad19nPdfIfctylKgM=,tag:6n6lwjGGAQ1iZFb2Gk0ZOA==,type:str] +POSTGRES_USER=ENC[AES256_GCM,data:vP3XRKaW7Es=,iv:N1+m250CXrBJ6M89420Kw6EOk36ubYm5V1cBKYbivuQ=,tag:HauutfyydiwpP8p6ED7xow==,type:str] +POSTGRES_PASSWORD=ENC[AES256_GCM,data:CNdOwTzEIJw=,iv:vysY4S7GXiJarW+nn3IhtqpbqGt66uxF8rESBH5rndE=,tag:4OiNss65oGUYwT6DQ90Uiw==,type:str] +POSTGRES_DB=ENC[AES256_GCM,data:/rEWpBI1,iv:ofQM5AREZEyarZqtXUw8J59RaxUXFxJTSSl+QY1fRL8=,tag:vtZ5xaSpoJueZjyW2qPR1Q==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:fFrfySct2Bnf4uqW7nh1jomFXA07uIzVj87evDBIIiaJbeGVKAVN34335Nx5EiZiVMH/sP+ZbpCjRY0Y+Oe3ua4Q9PruRSZZ+g==,iv:NF+60xk7pHOnU5UGGBXvhARKmGQn9D2siYPnzs90F/E=,tag:znrYhkuW9y63kj4sv0qadA==,type:str] +PGHOST=ENC[AES256_GCM,data:oSQ=,iv:ARIr/ltvf45sch9FDtmkvoB1ZtO9NpuY3kCL80rC2Nc=,tag:1av8sSCFfJn4mwCVCDE/sw==,type:str] +PGPORT=ENC[AES256_GCM,data:gQqRmg==,iv:rIE824GodwwYDV6RrlU6GGzeMsHl1OslYEcPbpM4FG8=,tag:v1GEpBHvh57goirCznNQvw==,type:str] +PGUSER=ENC[AES256_GCM,data:vTCVB+18i5eCDcNAhXPFMw==,iv:fXVQXWyRBxKYXVxyBd50Vlj9LYP4WTlSu7XJHZ+/chE=,tag:vJjvVSLkD54164ouNKiiMw==,type:str] +PGPASSWORD=ENC[AES256_GCM,data:5yyUjiSoNNoBKkkc5G6aPSxDqVU=,iv:cLBzonquatuOvAPggVcdvqtLpAvv03K4f3vr6BHm4W8=,tag:UNLRQreMuJDShjYfyl4n9A==,type:str] +PGDATABASE=ENC[AES256_GCM,data:eq2+1G9H9g91NMlzvXQ=,iv:Qv+cAvUy14/cCnExGip5wDYOz1DfFVm4PDCNitFXBXk=,tag:qhF3oR/8djZjaywEZlT0Pw==,type:str] +VALKEY_HOST=ENC[AES256_GCM,data:GRcBoFc=,iv:InuwdL0xoUpAJ3qFey5VNKNAH8FnbahDrJb4+uZMtdc=,tag:zXsAGvP5USeT8q0TpLLuFA==,type:str] +MINIO_ROOT_USER=ENC[AES256_GCM,data:nyYt8RF/1nSo6RGe,iv:qVrZPQoM/jPXLduovT7diIuYWf5qOSig+2vyIK3Eyp0=,tag:/WtmapHuczbyPatIUfn0wA==,type:str] +MINIO_ROOT_PASSWORD=ENC[AES256_GCM,data:BzRA0o1ExPc5RRyI,iv:vvY32RbF7PAJ04DnEDJEhtth+69ERIqgSd3WnAhVo2k=,tag:qWeKz/4jqf4omQO05h/gow==,type:str] +ASSETS_BUCKET_NAME=ENC[AES256_GCM,data:zhZbMz1l,iv:V5sDOJS91ja2rNWkoRKZcGrfCoXLMOaQGfQEEOuAYwY=,tag:v/ctTnjabZONK7tHskIGhw==,type:str] +ASSETS_UPLOAD_KEY=ENC[AES256_GCM,data:2Jp0Ag0E9tO49w==,iv:834sjanGTdosiSEp9hU4A2Bids/AD+hF+gtay/ShQG0=,tag:ELJSrXCxYLJ3RY5URtoMpQ==,type:str] +ASSETS_UPLOAD_SECRET_KEY=ENC[AES256_GCM,data:4KCOFy9D6lWdug==,iv:rfGMxDxDwwR2ge/stnr3j7XSUEqEvnkZ4tqEO7kipao=,tag:RkNrw0bFcullZDFDnVBPsg==,type:str] +ASSETS_REGION=ENC[AES256_GCM,data:9CuBN+8Sc9Gh,iv:+LQ+exp0GK3BBBjZiFvs9ojdVEU8qwjkMMFfUjRf+i0=,tag:sBxanmVZshLdG4AjwJixbw==,type:str] +ASSETS_STORAGE_ENDPOINT=ENC[AES256_GCM,data:DYulcQXvnZZnAxHEYo+hvKI=,iv:eTmV3sT3t4NJMPXztvJ/Q8xr94BLx/HvOI2uzEvTV1A=,tag:ivb5ozzFm84oo75V2L0h0g==,type:str] +ASSETS_PUBLIC_ENDPOINT=ENC[AES256_GCM,data:YKLdtwJfpv5FAGaF9eLyoKBTvaINVI7skg+crNrpMpgythjM9UtEkQUkYLvY8Q==,iv:6OgMI0kF2ZSmyr3vwIgJSDmJd/SZIyNnhPpW0gRuCpM=,tag:f84QuBy/RvTzuy2kGRwuiA==,type:str] +S3_ENDPOINT=ENC[AES256_GCM,data:OV9s2rWftVgT1qTyIKfvK9k=,iv:nfoRG45f/rMmtc6w64KIpzyRMHzUQKSZEpd/54dRKnY=,tag:sRMy3UPbtuS9Ew1WoAsBLw==,type:str] +S3_REGION=ENC[AES256_GCM,data:+o9P6dq/OTyv,iv:w/taA929bdDnWTfV41ztTW/A2aTNtOPBWGG9GrO4m50=,tag:GlrRSsryimlwV/4cOMLYCQ==,type:str] +SITE_BUILDER_ENDPOINT=ENC[AES256_GCM,data:M8noZ9SuEb3UinuqXKg3mptsXLv+CBDR,iv:iI4HMrTGmjA1KlBQailX7nHKaMkgFziAyaE0rEIOXTs=,tag:/9V9YLNriXXhZ18iKFa27Q==,type:str] +SITE_BUILDER_API_KEY=ENC[AES256_GCM,data:sp9o,iv:9ekQscr6uw67f/q/gTBbdHOqllB4wpL0tVb19D94mDk=,tag:a8PFEsrzmN6ft2mTU9fVSA==,type:str] +MAILGUN_SMTP_HOST=ENC[AES256_GCM,data:kVfz,iv:1vsZ6xyd3UmqKiacWYCj+aFoOZwrRpQupri71OaMjxo=,tag:jibeX711XVd4r9NEqrF/3g==,type:str] +MAILGUN_SMTP_PORT=ENC[AES256_GCM,data:X3w5,iv:TCEpq0sGFFRX3t0bSi0N9TrExaEuJtCfhc2zbtuTmCM=,tag:7Lf10+o30Cskx4T5bOw2mA==,type:str] +MAILGUN_SMTP_PASSWORD=ENC[AES256_GCM,data:dDXf,iv:XGlanU02ma39SAYQXAoE1UzPTfRS/KmHK97kVys+nZs=,tag:/O+QEwi5LR3ZzR1lPzhJbg==,type:str] +MAILGUN_SMTP_USERNAME=ENC[AES256_GCM,data:DEbB,iv:p1gdSMBMdMGXdYEb4XoYdMNnNYRu3JQfm0yNETOJAAI=,tag:AyILasgDTviZhJX51e+BnQ==,type:str] +GCLOUD_KEY_FILE=ENC[AES256_GCM,data:DgDE,iv:dGCtYZIAUz+IRbI/8Xh2O5fl1jllPr9W75QxdO8N8Xk=,tag:VSfR/8Pd/odDU9+8C00eMw==,type:str] +OTEL_SERVICE_NAME=ENC[AES256_GCM,data:xhtoAe/QWc7DtnYjoRKUZkQ=,iv:ZF+CKi0f1Sq3c5UiKpd6NFfurUc4dFKuz246D2frvas=,tag:PYwdy58De+T+l1q1ZFIJoQ==,type:str] +HONEYCOMB_API_KEY=ENC[AES256_GCM,data:dLTW,iv:JmuG6I7LR6jjNKTpX46r86ZDRyT87Em1YE7ZZZkZ4OE=,tag:hclmkfgwyLR6oVpHGP7clg==,type:str] +API_KEY=ENC[AES256_GCM,data:H9M9,iv:NXDRy323PFP7c5+LtgCal2OkgY5K66CJwJp3Bjj5cJg=,tag:0L1AFfkRSgIyVDqIgrBjXQ==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByRWZudVlCeDV6Ni91RDNz\nMCt1Ujg5SUxnL0Nxenp5Vk1BYkdGUjZHcEFBCkNyRmY2YndaMG41NC9JL2tCYXFr\nWWllUW1TMHMyMS9nZ3B2V0dqb1JzRnMKLS0tIDluRHdZM0lZWFNDU296ZHNMOGlK\nRm53V3ZTd21YRHI5Q3kyT1p3Nml2OGMK0dYKzCiSggxIgIjLabDZe8tfdg78BS0U\nZQnnigGcpawgKxPKm1DvXUJKWZzPtTTehUL3TAQadYe+ZzVOsAnVuA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEbnE3eGlLWWxiMnlyUGxD\nMklvYk11SDdxb014aGtDaVBXc08xMCtzWlhZClZNYmhFeHQvd0NrdDE5Z2x6SERM\najFmOU0zM2ZOaERYT2dkYkF4czAxY1EKLS0tIE1MUHdVa3FMd0hnZldYWGxaTnlo\nMWlOaE53N0lJTDRYL0xZWTZlRElGZTQKhQUYdy7l3PKCW42Ifch1T2qvZw2xeifF\nLScPBD1sTQBd6zWG+QVfeSHjUbrpHYgvWDZkRtM1g8EJRQI9Ry2CwQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_recipient=age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWdmZRUVRrOWpVZWcxc3VU\nSW15Nm5zc1BPRDNqdkQxOGFsbmNNcFY4eDNJCkpnR2t3aFpnOWFPVmVsMFlPc2FY\nREgzeUVnajliWDVyTEZSR1pNK1dnVFkKLS0tIHc3akVLdTFnWHNPWGMzOU85UlFT\nNVdPZ09iWDNZdkZQUzBwbXNkVlNKQWMKHoKV5ruHR8omGWRPv9rAl40wunWoFGrP\nuy+Jei87mB0pCc4oghSWI4W1pHKGyaFXqx1cpge/X9ib6DRcK7UVXA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_recipient=age1slx6e48k7fre0ddyu7dtm2wwcqaywn5ke2mkngym3rpazxwvvuyq9qjknk +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwNDBiNWU2R3VUN0RuR1FG\nY0M3RlN2UFFNTmZ1TE40VkN1WkhHRkhBRnpvClhUM0hvdElsZkl1a0FlTkkvaTI0\nM1BUWnVHbTdBNjRKazBWZ3hiKzhROG8KLS0tIEZLYXFxUVRlWGVESlJtRGhWY1oz\nNGIzbzIyYmcvdWdCVTFtREdjelh5YVUKa5yUPircj6hoy/a1D07vRxv6S8syDUf/\nHc3AIltVEkMtzfekyOXUANJKqCFhUYPSVLD3f1XnIB/bDvZyw7fx6A==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBocmwvU3dsTGZzMXMxUUpr\nMXIwVGRWWjZlYVZqZndLeVQ4UkZoNDRxMUIwCnhRV1VMZjZzckVXRjN2UklMb2Ni\nRU9jUFNURG5JM1JGL3liSGVPUC9uNFEKLS0tIHZkMFhUMFZickhwK0NONEtGNXZn\nZmU0Yjl3aTRiWk02a3dlQlFNK2xqVmcK+4giw+0KrTYDrx1hHLJzmNZD4mKjVnkN\nqEJIuu3ZPBfkm7yWJXFlcgSWOKahE3Epii8j80Cmc5oePSjt1z0spA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_recipient=age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2SXlGMlVINFV3K0pFSjI0\nVHRwdjRmQmU1a0o5bVNPMUZab3BTMU9TT1ZRCnJEZWNhZmVUUzZhOG9wRlcrbGxN\nS3VMdWNMaS9zQlE2bEI4bkxMMHhNSW8KLS0tIGxRWU1rQ1MyYk00ZzhZa0pBdFFw\nMHNxbklXM1R5MkZGSEJ0MkpzVmpxWlEKHDBJVh6m5H+MbUwO52SnfXHbQfVNlwRT\ndgM7Wiv8ply4s9f1jPl3dkge+W6CHw75v1gvEzQ0tAOssQ+qf6wg4Q==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy +sops_lastmodified=2026-04-02T11:05:34Z +sops_mac=ENC[AES256_GCM,data:lg4QHKFbldLKvhrhW2eEK3CuPjkk2wjwfcxX4KteRi3Jk4CXhWQGLPqEI++TwmOCjngYl0CfkRncDCzkQ1fWqweXl0o4zXo6AzkLmDoNYrTKhxAnb44t4q+LNCmi8aAlNvUXYhEyMBveL2MUgBYRNiJZAsnQbBgLubQjbydcGMI=,iv:BwdF4hd2bJ2pwNC5VieNkIMw6850r42ATh8CFbJRZ2M=,tag:3KOGg69IBt/9iNFcrP0ZeQ==,type:str] +sops_unencrypted_suffix=_unencrypted +sops_version=3.12.1 diff --git a/infra/.sops.yaml b/infra/.sops.yaml new file mode 100644 index 0000000000..51f479ecc3 --- /dev/null +++ b/infra/.sops.yaml @@ -0,0 +1,16 @@ +# creation_rules: +# - path_regex: \.env(\.staging)?(\.enc)?$ +# age: +# # add your age public keys here, one per team member / CI system +# # generate with: age-keygen +# # example: +# # - age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +creation_rules: + - path_regex: \.env(\.preview|\.sandbox)?(\.enc)?$ + age: + - age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr + - age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj + - age1slx6e48k7fre0ddyu7dtm2wwcqaywn5ke2mkngym3rpazxwvvuyq9qjknk + - age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h + - age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx + - age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy diff --git a/infra/Caddyfile b/infra/Caddyfile new file mode 100644 index 0000000000..d1af2b1081 --- /dev/null +++ b/infra/Caddyfile @@ -0,0 +1,40 @@ +{ + admin :2019 +} + +{$PUBPUB_HOSTNAME} { + encode gzip + + handle_path /assets* { + reverse_proxy minio:9000 + } + + handle_path /assets-ui* { + reverse_proxy minio:9001 + } + + handle_path /site-builder* { + reverse_proxy site-builder:4000 + } + + handle_path /sites/* { + root * /sites + file_server { + fs s3 { + bucket {$ASSETS_BUCKET_NAME:assets} + region {$ASSETS_REGION:us-east-1} + endpoint {$S3_ENDPOINT:http://minio:9000} + access_key {$ASSETS_UPLOAD_KEY} + secret_key {$ASSETS_UPLOAD_SECRET_KEY} + } + } + } + + handle { + reverse_proxy platform:3000 + } +} + +:80 { + respond "OK" 200 +} diff --git a/infra/Caddyfile.preview b/infra/Caddyfile.preview new file mode 100644 index 0000000000..2d7f8975be --- /dev/null +++ b/infra/Caddyfile.preview @@ -0,0 +1,53 @@ +{ + admin :2019 + + on_demand_tls { + interval 2m + burst 5 + } +} + +:443 { + tls { + on_demand + } + + encode gzip + + handle_path /assets* { + reverse_proxy minio:9000 + } + + handle_path /assets-ui* { + reverse_proxy minio:9001 + } + + handle_path /site-builder* { + reverse_proxy site-builder:4000 + } + + handle_path /sites/* { + root * /sites + file_server { + fs s3 { + bucket {$ASSETS_BUCKET_NAME:assets} + region {$ASSETS_REGION:us-east-1} + endpoint {$S3_ENDPOINT:http://minio:9000} + access_key {$ASSETS_UPLOAD_KEY} + secret_key {$ASSETS_UPLOAD_SECRET_KEY} + } + } + } + + handle_path /emails/* { + reverse_proxy inbucket:9000 + } + + handle { + reverse_proxy platform:3000 + } +} + +:80 { + respond "OK" 200 +} diff --git a/infra/import-backup.sh b/infra/import-backup.sh new file mode 100755 index 0000000000..293453ad43 --- /dev/null +++ b/infra/import-backup.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +DUMP_FILE="${1:?usage: $0 }" +STACK_NAME="${2:-pubpub}" + +if [[ ! -f "$DUMP_FILE" ]]; then + echo "file not found: $DUMP_FILE" + exit 1 +fi + +DB_CONTAINER=$(sudo docker ps --filter "label=com.docker.swarm.service.name=${STACK_NAME}_db" --format '{{.ID}}' | head -1) + +if [[ -z "$DB_CONTAINER" ]]; then + echo "no running db container found for stack: $STACK_NAME" + exit 1 +fi + +echo "importing $DUMP_FILE into container $DB_CONTAINER ..." + +if [[ "$DUMP_FILE" == *.sql ]]; then + sudo docker exec -i "$DB_CONTAINER" \ + psql -U "$PGUSER" -d "$PGDATABASE" < "$DUMP_FILE" +else + sudo docker exec -i "$DB_CONTAINER" \ + pg_restore --clean --if-exists --no-owner -U "$PGUSER" -d "$PGDATABASE" < "$DUMP_FILE" +fi + +echo "import complete" diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml new file mode 100644 index 0000000000..7e924a8ff5 --- /dev/null +++ b/infra/stack.preview.yml @@ -0,0 +1,158 @@ +services: + proxy: + image: ghcr.io/pubpub/caddy-sites:latest + env_file: [.env] + volumes: + - ./Caddyfile.preview:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + networks: [appnet] + ports: + - target: 80 + published: 80 + protocol: tcp + mode: host + - target: 443 + published: 443 + protocol: tcp + mode: host + deploy: + replicas: 1 + restart_policy: + condition: any + + platform: + image: ghcr.io/pubpub/platform:${IMAGE_TAG} + env_file: [.env] + environment: + HOSTNAME: "0.0.0.0" + NODE_ENV: production + PORT: "3000" + PUBPUB_URL: https://${PREVIEW_HOST} + SITE_BUILDER_ENDPOINT: http://site-builder:4000 + FLAGS: "uploads:off,invites:off,disabled-actions:http+email" + networks: [appnet] + healthcheck: + test: + - CMD-SHELL + - > + node -e "require('http') + .get('http://127.0.0.1:3000/api/health', r => process.exit(r.statusCode < 400 ? 0 : 1)) + .on('error', () => process.exit(1));" + interval: 10s + timeout: 3s + retries: 6 + start_period: 60s + deploy: + replicas: 1 + restart_policy: + condition: on-failure + + platform-jobs: + image: ghcr.io/pubpub/platform-jobs:${IMAGE_TAG} + env_file: [.env] + environment: + NODE_ENV: production + PUBPUB_URL: http://platform:3000 + networks: [appnet] + deploy: + replicas: 1 + restart_policy: + condition: on-failure + + platform-migrations: + image: ghcr.io/pubpub/platform-migrations:${IMAGE_TAG} + env_file: [.env] + command: ["pnpm", "--filter", "core", "reset"] + networks: [appnet] + deploy: + replicas: 1 + restart_policy: + condition: on-failure + max_attempts: 3 + + site-builder: + image: ghcr.io/pubpub/platform-site-builder:${IMAGE_TAG} + env_file: [.env] + environment: + NODE_ENV: production + PUBPUB_URL: http://platform:3000 + PORT: "4000" + networks: [appnet] + deploy: + replicas: 1 + restart_policy: + condition: on-failure + + db: + image: postgres:15 + environment: + POSTGRES_USER: preview + POSTGRES_PASSWORD: preview + POSTGRES_DB: preview + volumes: + - pgdata:/var/lib/postgresql/data + networks: [appnet] + deploy: + replicas: 1 + restart_policy: + condition: any + + cache: + image: valkey/valkey:8-alpine + networks: [appnet] + deploy: + replicas: 1 + restart_policy: + condition: any + + minio: + image: minio/minio:latest + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + command: server --console-address ":9001" /data + networks: [appnet] + deploy: + replicas: 1 + restart_policy: + condition: any + + minio-init: + image: minio/mc:latest + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + ASSETS_BUCKET_NAME: assets + ASSETS_UPLOAD_KEY: preview-key + ASSETS_UPLOAD_SECRET_KEY: preview-secret + entrypoint: > + /bin/sh -c ' + /usr/bin/mc alias set myminio http://minio:9000 "$${MINIO_ROOT_USER}" "$${MINIO_ROOT_PASSWORD}"; + /usr/bin/mc mb --ignore-existing myminio/"$${ASSETS_BUCKET_NAME}"; + /usr/bin/mc anonymous set download myminio/"$${ASSETS_BUCKET_NAME}"; + /usr/bin/mc admin user add myminio "$${ASSETS_UPLOAD_KEY}" "$${ASSETS_UPLOAD_SECRET_KEY}"; + /usr/bin/mc admin policy attach myminio readwrite --user "$${ASSETS_UPLOAD_KEY}";' + networks: [appnet] + deploy: + replicas: 1 + restart_policy: + condition: on-failure + max_attempts: 5 + + inbucket: + image: inbucket/inbucket:latest + networks: [appnet] + deploy: + replicas: 1 + restart_policy: + condition: any + +networks: + appnet: + driver: overlay + +volumes: + pgdata: + caddy_data: + caddy_config: diff --git a/infra/stack.yml b/infra/stack.yml new file mode 100644 index 0000000000..b74df9d710 --- /dev/null +++ b/infra/stack.yml @@ -0,0 +1,153 @@ +services: + proxy: + image: ghcr.io/pubpub/caddy-sites:latest + env_file: [.env] + ports: + - target: 80 + published: 80 + protocol: tcp + mode: host + - target: 443 + published: 443 + protocol: tcp + mode: host + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + networks: [appnet] + deploy: + replicas: 1 + restart_policy: + condition: any + + platform: + image: ghcr.io/pubpub/platform:${IMAGE_TAG} + env_file: [.env] + environment: + HOSTNAME: '0.0.0.0' + NODE_ENV: production + PORT: '3000' + PUBPUB_URL: ${PUBPUB_URL} + SITE_BUILDER_ENDPOINT: http://site-builder:4000 + networks: [appnet] + healthcheck: + test: + - CMD-SHELL + - > + node -e "require('http') + .get('http://127.0.0.1:3000/api/health', r => process.exit(r.statusCode < 400 ? 0 : 1)) + .on('error', () => process.exit(1));" + interval: 10s + timeout: 3s + retries: 6 + start_period: 60s + deploy: + replicas: 2 + update_config: + order: start-first + parallelism: 1 + delay: 5s + failure_action: rollback + rollback_config: + order: stop-first + parallelism: 1 + restart_policy: + condition: on-failure + + platform-jobs: + image: ghcr.io/pubpub/platform-jobs:${IMAGE_TAG} + env_file: [.env] + environment: + NODE_ENV: production + PUBPUB_URL: http://platform:3000 + networks: [appnet] + deploy: + replicas: 1 + update_config: + order: start-first + parallelism: 1 + delay: 5s + failure_action: rollback + restart_policy: + condition: on-failure + + platform-migrations: + image: ghcr.io/pubpub/platform-migrations:${IMAGE_TAG} + env_file: [.env] + command: ['pnpm', '--filter', 'core', 'migrate-docker'] + networks: [appnet] + deploy: + replicas: 1 + restart_policy: + condition: on-failure + max_attempts: 3 + + site-builder: + image: ghcr.io/pubpub/platform-site-builder:${IMAGE_TAG} + env_file: [.env] + environment: + NODE_ENV: production + PUBPUB_URL: http://platform:3000 + PORT: '4000' + networks: [appnet] + deploy: + replicas: 1 + update_config: + order: start-first + parallelism: 1 + delay: 5s + failure_action: rollback + restart_policy: + condition: on-failure + + db: + image: postgres:15 + tmpfs: + - /dev/shm:size=2147483648 + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + command: > + -c shared_buffers=2GB + -c effective_cache_size=6GB + -c work_mem=16MB + -c maintenance_work_mem=512MB + -c max_connections=500 + volumes: + - pgdata:/var/lib/postgresql/data + networks: [appnet, dbaccess] + deploy: + replicas: 1 + restart_policy: + condition: any + + cache: + image: valkey/valkey:8-alpine + networks: [appnet] + deploy: + replicas: 1 + restart_policy: + condition: any + + inbucket: + image: inbucket/inbucket:latest + networks: [appnet] + deploy: + replicas: 1 + restart_policy: + condition: any + +networks: + appnet: + driver: overlay + dbaccess: + driver: overlay + attachable: true + +volumes: + pgdata: + minio_data: + caddy_data: + caddy_config: diff --git a/package.json b/package.json index 58a7c2e858..653d45e0f2 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,13 @@ "dev:teardown": "docker compose -f docker-compose.dev.yml down -v", "integration:setup": "docker compose -f docker-compose.test.yml --profile integration up -d", "integration:teardown": "docker compose -f docker-compose.test.yml --profile integration down -v", - "context-editor:playwright": "pnpm --filter context-editor run playwright:test" + "context-editor:playwright": "pnpm --filter context-editor run playwright:test", + "secrets:encrypt": "cd infra && sops -e --input-type dotenv --output-type dotenv --output .env.enc .env", + "secrets:encrypt:preview": "cd infra && sops -e --input-type dotenv --output-type dotenv --output .env.preview.enc .env.preview", + "secrets:encrypt:sandbox": "cd infra && sops -e --input-type dotenv --output-type dotenv --output .env.sandbox.enc .env.sandbox", + "secrets:decrypt": "cd infra && sops -d --input-type dotenv --output-type dotenv --output .env .env.enc", + "secrets:decrypt:preview": "cd infra && sops -d --input-type dotenv --output-type dotenv --output .env.preview .env.preview.enc", + "secrets:decrypt:sandbox": "cd infra && sops -d --input-type dotenv --output-type dotenv --output .env.sandbox .env.sandbox.enc" }, "devDependencies": { "@babel/core": "7.28.3", @@ -54,7 +60,9 @@ "turbo": "^2.5.6" }, "preconstruct": { - "packages": ["packages/*"], + "packages": [ + "packages/*" + ], "exports": true, "___experimentalFlags_WILL_CHANGE_IN_PATCH": { "typeModule": true, @@ -86,4 +94,4 @@ "imports": { "#register-loader": "./core/prisma/seed/stubs/register-loader.js" } -} +} \ No newline at end of file From 5769e32af521dc18a0876c5b03dc86999dc930c1 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 13:26:25 +0200 Subject: [PATCH 02/16] chore: format --- .github/workflows/ghcr-build-all.yml | 86 ++++++++++++++-------------- package.json | 6 +- 2 files changed, 45 insertions(+), 47 deletions(-) diff --git a/.github/workflows/ghcr-build-all.yml b/.github/workflows/ghcr-build-all.yml index 3a0e6c955d..ebbe006625 100644 --- a/.github/workflows/ghcr-build-all.yml +++ b/.github/workflows/ghcr-build-all.yml @@ -1,51 +1,51 @@ name: docker build to GHCR on: - workflow_call: - inputs: - publish_latest: - type: boolean - default: false - outputs: - core-image: - description: "Core image ref" - value: ${{ jobs.build-core.outputs.image-sha }} - base-image: - description: "Base/migrations image ref" - value: ${{ jobs.build-base.outputs.image-sha }} - jobs-image: - description: "Jobs image ref" - value: ${{ jobs.build-jobs.outputs.image-sha }} - site-builder-image: - description: "Site builder image ref" - value: ${{ jobs.build-site-builder.outputs.image-sha }} + workflow_call: + inputs: + publish_latest: + type: boolean + default: false + outputs: + core-image: + description: 'Core image ref' + value: ${{ jobs.build-core.outputs.image-sha }} + base-image: + description: 'Base/migrations image ref' + value: ${{ jobs.build-base.outputs.image-sha }} + jobs-image: + description: 'Jobs image ref' + value: ${{ jobs.build-jobs.outputs.image-sha }} + site-builder-image: + description: 'Site builder image ref' + value: ${{ jobs.build-site-builder.outputs.image-sha }} jobs: - build-base: - uses: ./.github/workflows/ghcr-build-template.yml - with: - ghcr_image_name: platform-migrations - publish_latest: ${{ inputs.publish_latest }} + build-base: + uses: ./.github/workflows/ghcr-build-template.yml + with: + ghcr_image_name: platform-migrations + publish_latest: ${{ inputs.publish_latest }} - build-core: - uses: ./.github/workflows/ghcr-build-template.yml - with: - package: core - ghcr_image_name: platform - publish_latest: ${{ inputs.publish_latest }} + build-core: + uses: ./.github/workflows/ghcr-build-template.yml + with: + package: core + ghcr_image_name: platform + publish_latest: ${{ inputs.publish_latest }} - build-jobs: - uses: ./.github/workflows/ghcr-build-template.yml - with: - package: jobs - target: jobs - ghcr_image_name: platform-jobs - publish_latest: ${{ inputs.publish_latest }} + build-jobs: + uses: ./.github/workflows/ghcr-build-template.yml + with: + package: jobs + target: jobs + ghcr_image_name: platform-jobs + publish_latest: ${{ inputs.publish_latest }} - build-site-builder: - uses: ./.github/workflows/ghcr-build-template.yml - with: - package: site-builder - target: jobs - ghcr_image_name: platform-site-builder - publish_latest: ${{ inputs.publish_latest }} + build-site-builder: + uses: ./.github/workflows/ghcr-build-template.yml + with: + package: site-builder + target: jobs + ghcr_image_name: platform-site-builder + publish_latest: ${{ inputs.publish_latest }} diff --git a/package.json b/package.json index 653d45e0f2..84f24cfdaa 100644 --- a/package.json +++ b/package.json @@ -60,9 +60,7 @@ "turbo": "^2.5.6" }, "preconstruct": { - "packages": [ - "packages/*" - ], + "packages": ["packages/*"], "exports": true, "___experimentalFlags_WILL_CHANGE_IN_PATCH": { "typeModule": true, @@ -94,4 +92,4 @@ "imports": { "#register-loader": "./core/prisma/seed/stubs/register-loader.js" } -} \ No newline at end of file +} From 070aa8d93c35c687355dd6b8a114391209a95efd Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 13:31:02 +0200 Subject: [PATCH 03/16] fix: ci issue --- .github/workflows/on_main.yml | 7 +++++++ .github/workflows/on_pr.yml | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/.github/workflows/on_main.yml b/.github/workflows/on_main.yml index 6ae645b4f9..190b2142c0 100644 --- a/.github/workflows/on_main.yml +++ b/.github/workflows/on_main.yml @@ -5,12 +5,19 @@ on: branches: - main +permissions: + contents: read + packages: write + jobs: ci: uses: ./.github/workflows/ci.yml build-all: needs: ci + permissions: + contents: read + packages: write uses: ./.github/workflows/ghcr-build-all.yml with: publish_latest: true diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index d49311dfbb..6feb995c1d 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -7,6 +7,7 @@ on: permissions: id-token: write contents: read + packages: write jobs: path-filter: @@ -88,6 +89,9 @@ jobs: needs: - path-filter - skip_build_sha + permissions: + contents: read + packages: write uses: ./.github/workflows/ghcr-build-all.yml e2e: From d83286b92e0fe484d38218ff09a31d946ec1b346 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 14:19:42 +0200 Subject: [PATCH 04/16] fix: use sentry auth token --- .github/workflows/ghcr-build-template.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ghcr-build-template.yml b/.github/workflows/ghcr-build-template.yml index 5fadfb33b6..cf2c77a260 100644 --- a/.github/workflows/ghcr-build-template.yml +++ b/.github/workflows/ghcr-build-template.yml @@ -76,6 +76,8 @@ jobs: build-args: | PACKAGE=${{ inputs.package }} CI=true + secrets: | + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} target: ${{ steps.label.outputs.target }} tags: ${{ steps.label.outputs.tags }} platforms: linux/amd64 From 69ff995bc04094d63e7d8b76883709251962004f Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 14:30:22 +0200 Subject: [PATCH 05/16] fix: use sentry auth token --- .github/workflows/ghcr-build-template.yml | 149 +++++++++++----------- 1 file changed, 77 insertions(+), 72 deletions(-) diff --git a/.github/workflows/ghcr-build-template.yml b/.github/workflows/ghcr-build-template.yml index cf2c77a260..388925a53c 100644 --- a/.github/workflows/ghcr-build-template.yml +++ b/.github/workflows/ghcr-build-template.yml @@ -1,86 +1,91 @@ name: ghcr build template on: - workflow_call: - inputs: - package: - type: string - runner: - type: string - default: ubuntu-latest - target: - type: string - ghcr_image_name: - type: string - required: true - publish_latest: - type: boolean - default: false - outputs: - image-sha: - description: "Full GHCR image ref with SHA tag" - value: ${{ jobs.build.outputs.image-sha }} + workflow_call: + inputs: + package: + type: string + runner: + type: string + default: ubuntu-latest + target: + type: string + ghcr_image_name: + type: string + required: true + publish_latest: + type: boolean + default: false + outputs: + image-sha: + description: 'Full GHCR image ref with SHA tag' + value: ${{ jobs.build.outputs.image-sha }} + secrets: + SENTRY_AUTH_TOKEN: + required: true jobs: - build: - name: Build - runs-on: ${{ inputs.runner }} - permissions: - contents: read - packages: write + build: + name: Build + runs-on: ${{ inputs.runner }} + permissions: + contents: read + packages: write - outputs: - image-sha: ${{ steps.label.outputs.label }} + outputs: + image-sha: ${{ steps.label.outputs.label }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - - name: Compute image tags - id: label - run: | - sha_short=$(git describe --always --abbrev=40 --dirty) + - name: Compute image tags + id: label + run: | + sha_short=$(git describe --always --abbrev=40 --dirty) - if [[ -z "${{ inputs.package }}" ]]; then - echo "target=monorepo" >> $GITHUB_OUTPUT - else - echo "target=${{ inputs.target || format('next-app-{0}', inputs.package) }}" >> $GITHUB_OUTPUT - fi + if [[ -z "${{ inputs.package }}" ]]; then + echo "target=monorepo" >> $GITHUB_OUTPUT + else + echo "target=${{ inputs.target || format('next-app-{0}', inputs.package) }}" >> $GITHUB_OUTPUT + fi - echo "label=ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:$sha_short" >> $GITHUB_OUTPUT + echo "label=ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:$sha_short" >> $GITHUB_OUTPUT - TAGS="ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:$sha_short" - if [[ "${{ inputs.publish_latest }}" == "true" ]]; then - TAGS="$TAGS,ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:latest" - fi - echo "tags=$TAGS" >> $GITHUB_OUTPUT + TAGS="ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:$sha_short" + if [[ "${{ inputs.publish_latest }}" == "true" ]]; then + TAGS="$TAGS,ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:latest" + fi + echo "tags=$TAGS" >> $GITHUB_OUTPUT - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: . - cache-from: type=gha,scope=${{ inputs.ghcr_image_name }} - cache-to: type=gha,mode=max,scope=${{ inputs.ghcr_image_name }} - build-args: | - PACKAGE=${{ inputs.package }} - CI=true - secrets: | - SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - target: ${{ steps.label.outputs.target }} - tags: ${{ steps.label.outputs.tags }} - platforms: linux/amd64 - push: true - provenance: false - sbom: false + - name: Build and push + uses: docker/build-push-action@v6 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + with: + context: . + cache-from: type=gha,scope=${{ inputs.ghcr_image_name }} + cache-to: type=gha,mode=max,scope=${{ inputs.ghcr_image_name }} + build-args: | + PACKAGE=${{ inputs.package }} + CI=true + secrets: | + SENTRY_AUTH_TOKEN=${{ env.SENTRY_AUTH_TOKEN }} + target: ${{ steps.label.outputs.target }} + tags: ${{ steps.label.outputs.tags }} + platforms: linux/amd64 + push: true + provenance: false + sbom: false From d18fd3f0dbebc1616f70faf134c36d31cf2813c1 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 15:09:03 +0200 Subject: [PATCH 06/16] fix: don't secret it like that --- .github/workflows/ghcr-build-template.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ghcr-build-template.yml b/.github/workflows/ghcr-build-template.yml index 388925a53c..e518cd9b9a 100644 --- a/.github/workflows/ghcr-build-template.yml +++ b/.github/workflows/ghcr-build-template.yml @@ -20,9 +20,6 @@ on: image-sha: description: 'Full GHCR image ref with SHA tag' value: ${{ jobs.build.outputs.image-sha }} - secrets: - SENTRY_AUTH_TOKEN: - required: true jobs: build: From 9b073e2bac3e75789fca7c284f99d3c12c8eb044 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 15:11:27 +0200 Subject: [PATCH 07/16] fix: try and pass through sentry_Auth --- .github/workflows/ghcr-build-all.yml | 11 +++++++++++ .github/workflows/ghcr-build-template.yml | 3 +++ .github/workflows/on_main.yml | 2 ++ .github/workflows/on_pr.yml | 2 ++ 4 files changed, 18 insertions(+) diff --git a/.github/workflows/ghcr-build-all.yml b/.github/workflows/ghcr-build-all.yml index ebbe006625..734ef9703b 100644 --- a/.github/workflows/ghcr-build-all.yml +++ b/.github/workflows/ghcr-build-all.yml @@ -19,6 +19,9 @@ on: site-builder-image: description: 'Site builder image ref' value: ${{ jobs.build-site-builder.outputs.image-sha }} + secrets: + SENTRY_AUTH_TOKEN: + required: true jobs: build-base: @@ -26,6 +29,8 @@ jobs: with: ghcr_image_name: platform-migrations publish_latest: ${{ inputs.publish_latest }} + secrets: + SENTRY_AUTH_TOKEN: ${{ env.SENTRY_AUTH_TOKEN }} build-core: uses: ./.github/workflows/ghcr-build-template.yml @@ -33,6 +38,8 @@ jobs: package: core ghcr_image_name: platform publish_latest: ${{ inputs.publish_latest }} + secrets: + SENTRY_AUTH_TOKEN: ${{ env.SENTRY_AUTH_TOKEN }} build-jobs: uses: ./.github/workflows/ghcr-build-template.yml @@ -41,6 +48,8 @@ jobs: target: jobs ghcr_image_name: platform-jobs publish_latest: ${{ inputs.publish_latest }} + secrets: + SENTRY_AUTH_TOKEN: ${{ env.SENTRY_AUTH_TOKEN }} build-site-builder: uses: ./.github/workflows/ghcr-build-template.yml @@ -49,3 +58,5 @@ jobs: target: jobs ghcr_image_name: platform-site-builder publish_latest: ${{ inputs.publish_latest }} + secrets: + SENTRY_AUTH_TOKEN: ${{ env.SENTRY_AUTH_TOKEN }} diff --git a/.github/workflows/ghcr-build-template.yml b/.github/workflows/ghcr-build-template.yml index e518cd9b9a..388925a53c 100644 --- a/.github/workflows/ghcr-build-template.yml +++ b/.github/workflows/ghcr-build-template.yml @@ -20,6 +20,9 @@ on: image-sha: description: 'Full GHCR image ref with SHA tag' value: ${{ jobs.build.outputs.image-sha }} + secrets: + SENTRY_AUTH_TOKEN: + required: true jobs: build: diff --git a/.github/workflows/on_main.yml b/.github/workflows/on_main.yml index 190b2142c0..3b287557ec 100644 --- a/.github/workflows/on_main.yml +++ b/.github/workflows/on_main.yml @@ -19,6 +19,8 @@ jobs: contents: read packages: write uses: ./.github/workflows/ghcr-build-all.yml + secrets: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} with: publish_latest: true diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 6feb995c1d..4444745766 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -93,6 +93,8 @@ jobs: contents: read packages: write uses: ./.github/workflows/ghcr-build-all.yml + secrets: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} e2e: if: (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize') && always() && (needs.build-all.result == 'success' || needs.build-all.result == 'skipped') From e894fd21b437a5dd16d12bcfc3e6d87aa3ab593e Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 16:09:30 +0200 Subject: [PATCH 08/16] fix: try again --- .github/workflows/deploy.yml | 8 ++++++++ .github/workflows/ghcr-build-all.yml | 16 ++++++++-------- .github/workflows/ghcr-build-template.yml | 4 +--- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c131c2a639..b42d478b40 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -91,6 +91,8 @@ jobs: build-args: | PACKAGE=core CI=true + secrets: | + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} target: next-app-core tags: | ghcr.io/pubpub/platform:${{ steps.vars.outputs.image_tag }} @@ -108,6 +110,8 @@ jobs: cache-to: type=gha,mode=max,scope=platform-migrations build-args: | CI=true + secrets: | + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} target: monorepo tags: | ghcr.io/pubpub/platform-migrations:${{ steps.vars.outputs.image_tag }} @@ -126,6 +130,8 @@ jobs: build-args: | PACKAGE=jobs CI=true + secrets: | + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} target: jobs tags: | ghcr.io/pubpub/platform-jobs:${{ steps.vars.outputs.image_tag }} @@ -144,6 +150,8 @@ jobs: build-args: | PACKAGE=site-builder CI=true + secrets: | + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} target: jobs tags: | ghcr.io/pubpub/platform-site-builder:${{ steps.vars.outputs.image_tag }} diff --git a/.github/workflows/ghcr-build-all.yml b/.github/workflows/ghcr-build-all.yml index 734ef9703b..70c46bdf6f 100644 --- a/.github/workflows/ghcr-build-all.yml +++ b/.github/workflows/ghcr-build-all.yml @@ -29,8 +29,8 @@ jobs: with: ghcr_image_name: platform-migrations publish_latest: ${{ inputs.publish_latest }} - secrets: - SENTRY_AUTH_TOKEN: ${{ env.SENTRY_AUTH_TOKEN }} + secrets: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} build-core: uses: ./.github/workflows/ghcr-build-template.yml @@ -38,8 +38,8 @@ jobs: package: core ghcr_image_name: platform publish_latest: ${{ inputs.publish_latest }} - secrets: - SENTRY_AUTH_TOKEN: ${{ env.SENTRY_AUTH_TOKEN }} + secrets: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} build-jobs: uses: ./.github/workflows/ghcr-build-template.yml @@ -48,8 +48,8 @@ jobs: target: jobs ghcr_image_name: platform-jobs publish_latest: ${{ inputs.publish_latest }} - secrets: - SENTRY_AUTH_TOKEN: ${{ env.SENTRY_AUTH_TOKEN }} + secrets: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} build-site-builder: uses: ./.github/workflows/ghcr-build-template.yml @@ -58,5 +58,5 @@ jobs: target: jobs ghcr_image_name: platform-site-builder publish_latest: ${{ inputs.publish_latest }} - secrets: - SENTRY_AUTH_TOKEN: ${{ env.SENTRY_AUTH_TOKEN }} + secrets: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} diff --git a/.github/workflows/ghcr-build-template.yml b/.github/workflows/ghcr-build-template.yml index 388925a53c..f0fb902ac0 100644 --- a/.github/workflows/ghcr-build-template.yml +++ b/.github/workflows/ghcr-build-template.yml @@ -72,8 +72,6 @@ jobs: - name: Build and push uses: docker/build-push-action@v6 - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} with: context: . cache-from: type=gha,scope=${{ inputs.ghcr_image_name }} @@ -82,7 +80,7 @@ jobs: PACKAGE=${{ inputs.package }} CI=true secrets: | - SENTRY_AUTH_TOKEN=${{ env.SENTRY_AUTH_TOKEN }} + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} target: ${{ steps.label.outputs.target }} tags: ${{ steps.label.outputs.tags }} platforms: linux/amd64 From facea38931ed8d1f8b5bab101d57eff544bb43ec Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 20:08:13 +0200 Subject: [PATCH 09/16] fix: also filter on label --- .github/workflows/on_pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 4444745766..3c8c44fc0a 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -12,7 +12,7 @@ permissions: jobs: path-filter: runs-on: ubuntu-latest - if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' || github.event.action == 'closed' + if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' || github.event.action == 'closed' || github.event.action == 'labeled' || github.event.action == 'unlabeled' outputs: docs: ${{ steps.changes.outputs.docs }} steps: From 52b38e0d2f996875955432960ca2db76e701d962 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 20:13:21 +0200 Subject: [PATCH 10/16] fix: push to kf --- .github/workflows/ghcr-build-template.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ghcr-build-template.yml b/.github/workflows/ghcr-build-template.yml index f0fb902ac0..32da2fa6f3 100644 --- a/.github/workflows/ghcr-build-template.yml +++ b/.github/workflows/ghcr-build-template.yml @@ -62,11 +62,11 @@ jobs: echo "target=${{ inputs.target || format('next-app-{0}', inputs.package) }}" >> $GITHUB_OUTPUT fi - echo "label=ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:$sha_short" >> $GITHUB_OUTPUT + echo "label=ghcr.io/knowledgefutures/${{ inputs.ghcr_image_name }}:$sha_short" >> $GITHUB_OUTPUT - TAGS="ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:$sha_short" + TAGS="ghcr.io/knowledgefutures/${{ inputs.ghcr_image_name }}:$sha_short" if [[ "${{ inputs.publish_latest }}" == "true" ]]; then - TAGS="$TAGS,ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:latest" + TAGS="$TAGS,ghcr.io/knowledgefutures/${{ inputs.ghcr_image_name }}:latest" fi echo "tags=$TAGS" >> $GITHUB_OUTPUT From fe19959f388bfa0134e729d1f1d53fc79da6d69b Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 20:17:58 +0200 Subject: [PATCH 11/16] fix: fix even more --- .github/workflows/deploy.yml | 24 ++++++++++++------------ .github/workflows/preview.yml | 8 +++++--- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b42d478b40..9649598583 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -79,7 +79,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Build and push platform + - name: Build and push pubstar uses: docker/build-push-action@v6 with: context: . @@ -95,8 +95,8 @@ jobs: SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} target: next-app-core tags: | - ghcr.io/pubpub/platform:${{ steps.vars.outputs.image_tag }} - ${{ steps.vars.outputs.publish_latest == 'true' && 'ghcr.io/pubpub/platform:latest' || '' }} + ghcr.io/knowledgefutures/platform:${{ steps.vars.outputs.image_tag }} + ${{ steps.vars.outputs.publish_latest == 'true' && 'ghcr.io/knowledgefutures/platform:latest' || '' }} platforms: linux/amd64 - name: Build and push platform-migrations @@ -114,8 +114,8 @@ jobs: SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} target: monorepo tags: | - ghcr.io/pubpub/platform-migrations:${{ steps.vars.outputs.image_tag }} - ${{ steps.vars.outputs.publish_latest == 'true' && 'ghcr.io/pubpub/platform-migrations:latest' || '' }} + ghcr.io/knowledgefutures/platform-migrations:${{ steps.vars.outputs.image_tag }} + ${{ steps.vars.outputs.publish_latest == 'true' && 'ghcr.io/knowledgefutures/platform-migrations:latest' || '' }} platforms: linux/amd64 - name: Build and push platform-jobs @@ -134,8 +134,8 @@ jobs: SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} target: jobs tags: | - ghcr.io/pubpub/platform-jobs:${{ steps.vars.outputs.image_tag }} - ${{ steps.vars.outputs.publish_latest == 'true' && 'ghcr.io/pubpub/platform-jobs:latest' || '' }} + ghcr.io/knowledgefutures/platform-jobs:${{ steps.vars.outputs.image_tag }} + ${{ steps.vars.outputs.publish_latest == 'true' && 'ghcr.io/knowledgefutures/platform-jobs:latest' || '' }} platforms: linux/amd64 - name: Build and push platform-site-builder @@ -154,8 +154,8 @@ jobs: SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} target: jobs tags: | - ghcr.io/pubpub/platform-site-builder:${{ steps.vars.outputs.image_tag }} - ${{ steps.vars.outputs.publish_latest == 'true' && 'ghcr.io/pubpub/platform-site-builder:latest' || '' }} + ghcr.io/knowledgefutures/platform-site-builder:${{ steps.vars.outputs.image_tag }} + ${{ steps.vars.outputs.publish_latest == 'true' && 'ghcr.io/knowledgefutures/platform-site-builder:latest' || '' }} platforms: linux/amd64 deploy: @@ -234,9 +234,9 @@ jobs: sudo env IMAGE_TAG="$IMAGE_TAG" \ docker stack deploy -c stack.yml \ --with-registry-auth --resolve-image always --prune \ - pubpub + pubstar - sudo docker stack services pubpub + sudo docker stack services pubstar sudo docker image prune -f # wait for platform rollout @@ -264,6 +264,6 @@ jobs: return 1 } - wait_rollout pubpub_platform 600 + wait_rollout pubstar 600 EOS diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 1b7d3cf45e..68b624d635 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -39,7 +39,7 @@ jobs: run: | echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT echo "stack_name=preview-pr-${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT - echo "host=pr-${{ github.event.pull_request.number }}.preview.pubpub.org" >> $GITHUB_OUTPUT + echo "host=pr-${{ github.event.pull_request.number }}.preview.pubstar.org" >> $GITHUB_OUTPUT - name: Start SSH agent uses: webfactory/ssh-agent@v0.9.0 @@ -62,10 +62,11 @@ jobs: GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} IMAGE_TAG: ${{ inputs.image_tag }} STACK_NAME: ${{ steps.pr.outputs.stack_name }} - PREVIEW_HOST: ${{ steps.pr.outputs.host }} + PREVIEW_HOST: ${{steps.pr.outputs.host }} + PR_NUMBER: ${{ steps.pr.outputs.number }} run: | ssh "${SSH_USER}@${SSH_HOST}" \ - "env GHCR_USER='${GHCR_USER}' GHCR_TOKEN='${GHCR_TOKEN}' IMAGE_TAG='${IMAGE_TAG}' STACK_NAME='${STACK_NAME}' PREVIEW_HOST='${PREVIEW_HOST}' bash -s -- '${REPO}' '${BRANCH}'" <<'EOS' + "env GHCR_USER='${GHCR_USER}' GHCR_TOKEN='${GHCR_TOKEN}' IMAGE_TAG='${IMAGE_TAG}' STACK_NAME='${STACK_NAME}' PREVIEW_HOST='${PREVIEW_HOST}' PR_NUMBER='${PR_NUMBER}' bash -s -- '${REPO}' '${BRANCH}'" <<'EOS' set -euo pipefail REPO="${1:?missing repo}" @@ -76,6 +77,7 @@ jobs: : "${GHCR_TOKEN:?missing GHCR_TOKEN}" : "${STACK_NAME:?missing STACK_NAME}" : "${PREVIEW_HOST:?missing PREVIEW_HOST}" + : "${PR_NUMBER:?missing PR_NUMBER}" REPO_NAME="${REPO##*/}" APP_DIR="/srv/${REPO_NAME}" From 307af6bfa9b083598febb0d4af841a9c66b892f7 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 20:38:04 +0200 Subject: [PATCH 12/16] fix: fix even more --- .github/workflows/e2e.yml | 8 ++++---- .github/workflows/preview.yml | 14 ++++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 9861c4219f..b60a889c71 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -85,10 +85,10 @@ jobs: env: IMAGE_TAG: ${{ steps.gettag.outputs.tag }} run: | - echo "core_label=ghcr.io/pubpub/platform:$IMAGE_TAG" >> $GITHUB_OUTPUT - echo "jobs_label=ghcr.io/pubpub/platform-jobs:$IMAGE_TAG" >> $GITHUB_OUTPUT - echo "base_label=ghcr.io/pubpub/platform-migrations:$IMAGE_TAG" >> $GITHUB_OUTPUT - echo "site_builder_label=ghcr.io/pubpub/platform-site-builder:$IMAGE_TAG" >> $GITHUB_OUTPUT + echo "core_label=ghcr.io/knowledgefutures/platform:$IMAGE_TAG" >> $GITHUB_OUTPUT + echo "jobs_label=ghcr.io/knowledgefutures/platform-jobs:$IMAGE_TAG" >> $GITHUB_OUTPUT + echo "base_label=ghcr.io/knowledgefutures/platform-migrations:$IMAGE_TAG" >> $GITHUB_OUTPUT + echo "site_builder_label=ghcr.io/knowledgefutures/platform-site-builder:$IMAGE_TAG" >> $GITHUB_OUTPUT - name: Install dependencies run: pnpm install --frozen-lockfile --prefer-offline diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 68b624d635..c5f15871cb 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -66,7 +66,7 @@ jobs: PR_NUMBER: ${{ steps.pr.outputs.number }} run: | ssh "${SSH_USER}@${SSH_HOST}" \ - "env GHCR_USER='${GHCR_USER}' GHCR_TOKEN='${GHCR_TOKEN}' IMAGE_TAG='${IMAGE_TAG}' STACK_NAME='${STACK_NAME}' PREVIEW_HOST='${PREVIEW_HOST}' PR_NUMBER='${PR_NUMBER}' bash -s -- '${REPO}' '${BRANCH}'" <<'EOS' + "env GHCR_USER='${GHCR_USER}' GHCR_TOKEN='${GHCR_TOKEN}' IMAGE_TAG='${IMAGE_TAG}' STACK_NAME='${STACK_NAME}' PREVIEW_HOST='${PREVIEW_HOST}' PR_NUMBER='${PR_NUMBER}' bash -s -- '${REPO}' '${BRANCH}'" <<'EOS' set -euo pipefail REPO="${1:?missing repo}" @@ -78,6 +78,7 @@ jobs: : "${STACK_NAME:?missing STACK_NAME}" : "${PREVIEW_HOST:?missing PREVIEW_HOST}" : "${PR_NUMBER:?missing PR_NUMBER}" + : "${ENV_FILE:?missing ENV_FILE}" REPO_NAME="${REPO##*/}" APP_DIR="/srv/${REPO_NAME}" @@ -98,18 +99,23 @@ jobs: cd infra + sops -d --input-type dotenv --output-type dotenv ".env.preview.enc" > .env + if ! sudo docker info --format '{{.Swarm.LocalNodeState}}' | grep -qx active; then sudo docker swarm init --advertise-addr "$(hostname -I | awk '{print $1}')" fi echo "$GHCR_TOKEN" | sudo docker login ghcr.io -u "$GHCR_USER" --password-stdin - echo "deploying preview stack $STACK_NAME with IMAGE_TAG=$IMAGE_TAG at $PREVIEW_HOST" + echo "IMAGE_TAG in shell: [$IMAGE_TAG]" + + # For some reason, not pulling explicitly makes the docker stack deploy throw an error that it can't find the package. + sudo docker pull ghcr.io/knowledgefutures/platform:"$IMAGE_TAG" + # deploy/update stack sudo env IMAGE_TAG="$IMAGE_TAG" PREVIEW_HOST="$PREVIEW_HOST" \ docker stack deploy -c stack.preview.yml \ - --with-registry-auth --resolve-image always \ - "$STACK_NAME" + --with-registry-auth --resolve-image always --prune "$STACK_NAME" sudo docker stack services "$STACK_NAME" sudo docker image prune -f From a35f1732db309a19b25340894d7c0e92a9482c16 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 20:51:24 +0200 Subject: [PATCH 13/16] ugh --- .github/workflows/preview.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index c5f15871cb..7aab542734 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -78,7 +78,6 @@ jobs: : "${STACK_NAME:?missing STACK_NAME}" : "${PREVIEW_HOST:?missing PREVIEW_HOST}" : "${PR_NUMBER:?missing PR_NUMBER}" - : "${ENV_FILE:?missing ENV_FILE}" REPO_NAME="${REPO##*/}" APP_DIR="/srv/${REPO_NAME}" From e5a56cd6adfc4fa906b04d80d303c7f0ef1020f9 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 20:53:28 +0200 Subject: [PATCH 14/16] fix: expedite testing --- .github/workflows/on_pr.yml | 257 ++++++++++++++++++------------------ 1 file changed, 129 insertions(+), 128 deletions(-) diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 3c8c44fc0a..8f473338ab 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -10,113 +10,114 @@ permissions: packages: write jobs: - path-filter: - runs-on: ubuntu-latest - if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' || github.event.action == 'closed' || github.event.action == 'labeled' || github.event.action == 'unlabeled' - outputs: - docs: ${{ steps.changes.outputs.docs }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: dorny/paths-filter@v3 - id: changes - with: - filters: | - docs: - - 'docs/**' + # path-filter: + # runs-on: ubuntu-latest + # if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' || github.event.action == 'closed' || github.event.action == 'labeled' || github.event.action == 'unlabeled' + # outputs: + # docs: ${{ steps.changes.outputs.docs }} + # steps: + # - uses: actions/checkout@v4 + # with: + # fetch-depth: 0 + # - uses: dorny/paths-filter@v3 + # id: changes + # with: + # filters: | + # docs: + # - 'docs/**' - # you can skip the build by adding 'skip-build' to the commit message, useful when testing tests - skip_build_sha: - outputs: - last-successful-build-sha: ${{ steps.last-build.outputs.last-successful-build-sha }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} - - name: Check if skip-build is in the commit message - id: check - run: | - echo "commit message: $(git log -1 --pretty=%B)" - if [[ "$(git log -1 --pretty=%B)" == *"skip-build"* ]]; then - echo "skip-build is in the commit message" - echo "skip-build=true" >> $GITHUB_OUTPUT - else - echo "skip-build is not in the commit message" - echo "skip-build=false" >> $GITHUB_OUTPUT - echo "skip-build-sha=" >> $GITHUB_OUTPUT - fi - - name: Find last successful build SHA - id: last-build - if: ${{ steps.check.outputs.skip-build == 'true' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - pr_number="${{ github.event.pull_request.number }}" + # # you can skip the build by adding 'skip-build' to the commit message, useful when testing tests + # skip_build_sha: + # outputs: + # last-successful-build-sha: ${{ steps.last-build.outputs.last-successful-build-sha }} + # runs-on: ubuntu-latest + # steps: + # - name: Checkout + # uses: actions/checkout@v4 + # with: + # fetch-depth: 0 + # ref: ${{ github.event.pull_request.head.sha }} + # - name: Check if skip-build is in the commit message + # id: check + # run: | + # echo "commit message: $(git log -1 --pretty=%B)" + # if [[ "$(git log -1 --pretty=%B)" == *"skip-build"* ]]; then + # echo "skip-build is in the commit message" + # echo "skip-build=true" >> $GITHUB_OUTPUT + # else + # echo "skip-build is not in the commit message" + # echo "skip-build=false" >> $GITHUB_OUTPUT + # echo "skip-build-sha=" >> $GITHUB_OUTPUT + # fi + # - name: Find last successful build SHA + # id: last-build + # if: ${{ steps.check.outputs.skip-build == 'true' }} + # env: + # GH_TOKEN: ${{ github.token }} + # run: | + # pr_number="${{ github.event.pull_request.number }}" - gh api "/repos/${{ github.repository }}/actions/workflows/on_pr.yml/runs?event=pull_request&per_page=100" \ - --jq ".workflow_runs[] | select(.pull_requests[]?.number == ${pr_number}) | select(.id < ${{ github.run_id }}) | {id: .id, sha: .head_sha, created: .created_at}" \ - | jq -s 'sort_by(.created) | reverse | .[].id' -r \ - | while read run_id; do - echo "Checking run: $run_id" - run=$(gh api "/repos/${{ github.repository }}/actions/runs/${run_id}/jobs") - echo "Run: $run" - all_success=$(echo "$run" | jq '[.jobs[] | select(.name | contains("build-all")) | .conclusion] | all(. == "success")') - echo "All success for $run_id: $all_success" + # gh api "/repos/${{ github.repository }}/actions/workflows/on_pr.yml/runs?event=pull_request&per_page=100" \ + # --jq ".workflow_runs[] | select(.pull_requests[]?.number == ${pr_number}) | select(.id < ${{ github.run_id }}) | {id: .id, sha: .head_sha, created: .created_at}" \ + # | jq -s 'sort_by(.created) | reverse | .[].id' -r \ + # | while read run_id; do + # echo "Checking run: $run_id" + # run=$(gh api "/repos/${{ github.repository }}/actions/runs/${run_id}/jobs") + # echo "Run: $run" + # all_success=$(echo "$run" | jq '[.jobs[] | select(.name | contains("build-all")) | .conclusion] | all(. == "success")') + # echo "All success for $run_id: $all_success" - if [ "$all_success" == "true" ]; then - successful_sha=$(gh api "/repos/${{ github.repository }}/actions/runs/${run_id}" --jq '.head_sha') - echo "last-successful-build-sha=${successful_sha}" >> $GITHUB_OUTPUT - echo "Found last successful build at SHA: $successful_sha (run: $run_id)" - exit 0 - fi - done + # if [ "$all_success" == "true" ]; then + # successful_sha=$(gh api "/repos/${{ github.repository }}/actions/runs/${run_id}" --jq '.head_sha') + # echo "last-successful-build-sha=${successful_sha}" >> $GITHUB_OUTPUT + # echo "Found last successful build at SHA: $successful_sha (run: $run_id)" + # exit 0 + # fi + # done - if [ "$all_success" == "false" ]; then - echo "last-successful-build-sha=" >> $GITHUB_OUTPUT - echo "No previous successful build found in this PR" - fi + # if [ "$all_success" == "false" ]; then + # echo "last-successful-build-sha=" >> $GITHUB_OUTPUT + # echo "No previous successful build found in this PR" + # fi - ci: - if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' || (github.event.action == 'labeled' && github.event.label.name == 'preview') - uses: ./.github/workflows/ci.yml + # ci: + # if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' || (github.event.action == 'labeled' && github.event.label.name == 'preview') + # uses: ./.github/workflows/ci.yml - build-all: - if: (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' || (github.event.action == 'labeled' && github.event.label.name == 'preview')) && (needs.skip_build_sha.outputs.last-successful-build-sha == '') - needs: - - path-filter - - skip_build_sha - permissions: - contents: read - packages: write - uses: ./.github/workflows/ghcr-build-all.yml - secrets: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + # build-all: + # if: (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' || (github.event.action == 'labeled' && github.event.label.name == 'preview')) && (needs.skip_build_sha.outputs.last-successful-build-sha == '') + # needs: + # - path-filter + # - skip_build_sha + # permissions: + # contents: read + # packages: write + # uses: ./.github/workflows/ghcr-build-all.yml + # secrets: + # SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - e2e: - if: (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize') && always() && (needs.build-all.result == 'success' || needs.build-all.result == 'skipped') - needs: - - path-filter - - build-all - - skip_build_sha - uses: ./.github/workflows/e2e.yml - with: - image-tag-override: ${{ needs.skip_build_sha.outputs.last-successful-build-sha || '' }} + # e2e: + # if: (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize') && always() && (needs.build-all.result == 'success' || needs.build-all.result == 'skipped') + # needs: + # - path-filter + # - build-all + # - skip_build_sha + # uses: ./.github/workflows/e2e.yml + # with: + # image-tag-override: ${{ needs.skip_build_sha.outputs.last-successful-build-sha || '' }} deploy-preview: - if: needs.build-all.result == 'success' && contains(github.event.pull_request.labels.*.name, 'preview') + # if: needs.build-all.result == 'success' && contains(github.event.pull_request.labels.*.name, 'preview') uses: ./.github/workflows/preview.yml - needs: - - build-all + # needs: + # - build-all permissions: contents: read pull-requests: write with: action: deploy - image_tag: ${{ github.event.pull_request.head.sha }} + # image_tag: ${{ github.event.pull_request.head.sha }} + image_tag: 307af6bfa9b083598febb0d4af841a9c66b892f7 secrets: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_USER: ${{ secrets.SSH_USER }} @@ -139,42 +140,42 @@ jobs: GHCR_USER: ${{ secrets.GHCR_USER }} GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} - deploy-docs-preview: - permissions: - contents: write - pages: write - pull-requests: write - needs: - - path-filter - if: (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize') && needs.path-filter.outputs.docs == 'true' - uses: ./.github/workflows/build-docs.yml - with: - preview: true + # deploy-docs-preview: + # permissions: + # contents: write + # pages: write + # pull-requests: write + # needs: + # - path-filter + # if: (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize') && needs.path-filter.outputs.docs == 'true' + # uses: ./.github/workflows/build-docs.yml + # with: + # preview: true - close-docs-preview: - needs: - - path-filter - permissions: - contents: write - pages: write - pull-requests: write - if: github.event.action == 'closed' && needs.path-filter.outputs.docs == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 + # close-docs-preview: + # needs: + # - path-filter + # permissions: + # contents: write + # pages: write + # pull-requests: write + # if: github.event.action == 'closed' && needs.path-filter.outputs.docs == 'true' + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 - - name: Close docs preview - uses: rossjrw/pr-preview-action@v1 - with: - source-dir: docs/out - action: remove + # - name: Close docs preview + # uses: rossjrw/pr-preview-action@v1 + # with: + # source-dir: docs/out + # action: remove - status-check: - needs: - - ci - - e2e - runs-on: ubuntu-latest - steps: - - name: ok - run: | - echo ok + # status-check: + # needs: + # - ci + # - e2e + # runs-on: ubuntu-latest + # steps: + # - name: ok + # run: | + # echo ok From fd2d9ba0ccc7da70387469760bc6be32673e7651 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 20:55:41 +0200 Subject: [PATCH 15/16] fix: reencrypt --- infra/.env.preview.enc | 88 +++++++++++++++++++++--------------------- infra/.env.sandbox.enc | 53 +++++++++++++++++++++++++ infra/.sops.yaml | 1 + 3 files changed, 99 insertions(+), 43 deletions(-) create mode 100644 infra/.env.sandbox.enc diff --git a/infra/.env.preview.enc b/infra/.env.preview.enc index f467e193fb..9d0815896f 100644 --- a/infra/.env.preview.enc +++ b/infra/.env.preview.enc @@ -1,51 +1,53 @@ -#ENC[AES256_GCM,data:of7NlDnxPpiEB5vsG/V1x3ZLOs/MKjJDuadN+vv9Kb4R,iv:TMmk9ukk2hkoB1kcG9Q8xHB1cO5aQLZ5r8g+26F01kc=,tag:eOFfFj9IrLuONwACqKIpXw==,type:comment] -#ENC[AES256_GCM,data:PtQw+iKvi39aNyMJ0vN0P6djoAzotLKsbQQ/Pl18TwteSNWRI9aq5FLiMPj6LAq+jmNpyBM5KiSHdTg=,iv:QySN5PpC8CROiICNnFTHMY24RMaZbkIrLbCEQWW/gnQ=,tag:EMltaYcGdFo9F+lsfXi0iw==,type:comment] -#ENC[AES256_GCM,data:GAOzJxMiPKOg9UEEwkZU4uY5OvtCyJr57M77gYFq8g8OqKOu1md+ZanhUZ9VzUujs3CE2sr64Z2Ommnj9N9wnNvSFA==,iv:3f5ncmBejz4v/zayzEJH1nkVS6D0nLM6SeAHdkGN6jk=,tag:ga6J1rGmpezcirtl+z1S3Q==,type:comment] -PUBPUB_HOSTNAME=ENC[AES256_GCM,data:bLWlWiwmE4P+79EBTPMB9yGZUbNV/oe1muxjOUcI9g==,iv:u3gYXAUajl40QSOHANO+E9/fzqEQuLSva7zVC+27prM=,tag:tdagMgYZ/AnOvf1DZImQdg==,type:str] -PUBPUB_URL=ENC[AES256_GCM,data:XMpckdKMM7um9cgLm3ejKv1496WrEd8bnFMMeoSzsu7VVlcNEu0p,iv:8l+hwEEVkKzM/rWh3vU4mBXP9Zad19nPdfIfctylKgM=,tag:6n6lwjGGAQ1iZFb2Gk0ZOA==,type:str] -POSTGRES_USER=ENC[AES256_GCM,data:vP3XRKaW7Es=,iv:N1+m250CXrBJ6M89420Kw6EOk36ubYm5V1cBKYbivuQ=,tag:HauutfyydiwpP8p6ED7xow==,type:str] -POSTGRES_PASSWORD=ENC[AES256_GCM,data:CNdOwTzEIJw=,iv:vysY4S7GXiJarW+nn3IhtqpbqGt66uxF8rESBH5rndE=,tag:4OiNss65oGUYwT6DQ90Uiw==,type:str] -POSTGRES_DB=ENC[AES256_GCM,data:/rEWpBI1,iv:ofQM5AREZEyarZqtXUw8J59RaxUXFxJTSSl+QY1fRL8=,tag:vtZ5xaSpoJueZjyW2qPR1Q==,type:str] -DATABASE_URL=ENC[AES256_GCM,data:fFrfySct2Bnf4uqW7nh1jomFXA07uIzVj87evDBIIiaJbeGVKAVN34335Nx5EiZiVMH/sP+ZbpCjRY0Y+Oe3ua4Q9PruRSZZ+g==,iv:NF+60xk7pHOnU5UGGBXvhARKmGQn9D2siYPnzs90F/E=,tag:znrYhkuW9y63kj4sv0qadA==,type:str] -PGHOST=ENC[AES256_GCM,data:oSQ=,iv:ARIr/ltvf45sch9FDtmkvoB1ZtO9NpuY3kCL80rC2Nc=,tag:1av8sSCFfJn4mwCVCDE/sw==,type:str] -PGPORT=ENC[AES256_GCM,data:gQqRmg==,iv:rIE824GodwwYDV6RrlU6GGzeMsHl1OslYEcPbpM4FG8=,tag:v1GEpBHvh57goirCznNQvw==,type:str] -PGUSER=ENC[AES256_GCM,data:vTCVB+18i5eCDcNAhXPFMw==,iv:fXVQXWyRBxKYXVxyBd50Vlj9LYP4WTlSu7XJHZ+/chE=,tag:vJjvVSLkD54164ouNKiiMw==,type:str] -PGPASSWORD=ENC[AES256_GCM,data:5yyUjiSoNNoBKkkc5G6aPSxDqVU=,iv:cLBzonquatuOvAPggVcdvqtLpAvv03K4f3vr6BHm4W8=,tag:UNLRQreMuJDShjYfyl4n9A==,type:str] -PGDATABASE=ENC[AES256_GCM,data:eq2+1G9H9g91NMlzvXQ=,iv:Qv+cAvUy14/cCnExGip5wDYOz1DfFVm4PDCNitFXBXk=,tag:qhF3oR/8djZjaywEZlT0Pw==,type:str] -VALKEY_HOST=ENC[AES256_GCM,data:GRcBoFc=,iv:InuwdL0xoUpAJ3qFey5VNKNAH8FnbahDrJb4+uZMtdc=,tag:zXsAGvP5USeT8q0TpLLuFA==,type:str] -MINIO_ROOT_USER=ENC[AES256_GCM,data:nyYt8RF/1nSo6RGe,iv:qVrZPQoM/jPXLduovT7diIuYWf5qOSig+2vyIK3Eyp0=,tag:/WtmapHuczbyPatIUfn0wA==,type:str] -MINIO_ROOT_PASSWORD=ENC[AES256_GCM,data:BzRA0o1ExPc5RRyI,iv:vvY32RbF7PAJ04DnEDJEhtth+69ERIqgSd3WnAhVo2k=,tag:qWeKz/4jqf4omQO05h/gow==,type:str] -ASSETS_BUCKET_NAME=ENC[AES256_GCM,data:zhZbMz1l,iv:V5sDOJS91ja2rNWkoRKZcGrfCoXLMOaQGfQEEOuAYwY=,tag:v/ctTnjabZONK7tHskIGhw==,type:str] -ASSETS_UPLOAD_KEY=ENC[AES256_GCM,data:2Jp0Ag0E9tO49w==,iv:834sjanGTdosiSEp9hU4A2Bids/AD+hF+gtay/ShQG0=,tag:ELJSrXCxYLJ3RY5URtoMpQ==,type:str] -ASSETS_UPLOAD_SECRET_KEY=ENC[AES256_GCM,data:4KCOFy9D6lWdug==,iv:rfGMxDxDwwR2ge/stnr3j7XSUEqEvnkZ4tqEO7kipao=,tag:RkNrw0bFcullZDFDnVBPsg==,type:str] -ASSETS_REGION=ENC[AES256_GCM,data:9CuBN+8Sc9Gh,iv:+LQ+exp0GK3BBBjZiFvs9ojdVEU8qwjkMMFfUjRf+i0=,tag:sBxanmVZshLdG4AjwJixbw==,type:str] -ASSETS_STORAGE_ENDPOINT=ENC[AES256_GCM,data:DYulcQXvnZZnAxHEYo+hvKI=,iv:eTmV3sT3t4NJMPXztvJ/Q8xr94BLx/HvOI2uzEvTV1A=,tag:ivb5ozzFm84oo75V2L0h0g==,type:str] -ASSETS_PUBLIC_ENDPOINT=ENC[AES256_GCM,data:YKLdtwJfpv5FAGaF9eLyoKBTvaINVI7skg+crNrpMpgythjM9UtEkQUkYLvY8Q==,iv:6OgMI0kF2ZSmyr3vwIgJSDmJd/SZIyNnhPpW0gRuCpM=,tag:f84QuBy/RvTzuy2kGRwuiA==,type:str] -S3_ENDPOINT=ENC[AES256_GCM,data:OV9s2rWftVgT1qTyIKfvK9k=,iv:nfoRG45f/rMmtc6w64KIpzyRMHzUQKSZEpd/54dRKnY=,tag:sRMy3UPbtuS9Ew1WoAsBLw==,type:str] -S3_REGION=ENC[AES256_GCM,data:+o9P6dq/OTyv,iv:w/taA929bdDnWTfV41ztTW/A2aTNtOPBWGG9GrO4m50=,tag:GlrRSsryimlwV/4cOMLYCQ==,type:str] -SITE_BUILDER_ENDPOINT=ENC[AES256_GCM,data:M8noZ9SuEb3UinuqXKg3mptsXLv+CBDR,iv:iI4HMrTGmjA1KlBQailX7nHKaMkgFziAyaE0rEIOXTs=,tag:/9V9YLNriXXhZ18iKFa27Q==,type:str] -SITE_BUILDER_API_KEY=ENC[AES256_GCM,data:sp9o,iv:9ekQscr6uw67f/q/gTBbdHOqllB4wpL0tVb19D94mDk=,tag:a8PFEsrzmN6ft2mTU9fVSA==,type:str] -MAILGUN_SMTP_HOST=ENC[AES256_GCM,data:kVfz,iv:1vsZ6xyd3UmqKiacWYCj+aFoOZwrRpQupri71OaMjxo=,tag:jibeX711XVd4r9NEqrF/3g==,type:str] -MAILGUN_SMTP_PORT=ENC[AES256_GCM,data:X3w5,iv:TCEpq0sGFFRX3t0bSi0N9TrExaEuJtCfhc2zbtuTmCM=,tag:7Lf10+o30Cskx4T5bOw2mA==,type:str] -MAILGUN_SMTP_PASSWORD=ENC[AES256_GCM,data:dDXf,iv:XGlanU02ma39SAYQXAoE1UzPTfRS/KmHK97kVys+nZs=,tag:/O+QEwi5LR3ZzR1lPzhJbg==,type:str] -MAILGUN_SMTP_USERNAME=ENC[AES256_GCM,data:DEbB,iv:p1gdSMBMdMGXdYEb4XoYdMNnNYRu3JQfm0yNETOJAAI=,tag:AyILasgDTviZhJX51e+BnQ==,type:str] -GCLOUD_KEY_FILE=ENC[AES256_GCM,data:DgDE,iv:dGCtYZIAUz+IRbI/8Xh2O5fl1jllPr9W75QxdO8N8Xk=,tag:VSfR/8Pd/odDU9+8C00eMw==,type:str] -OTEL_SERVICE_NAME=ENC[AES256_GCM,data:xhtoAe/QWc7DtnYjoRKUZkQ=,iv:ZF+CKi0f1Sq3c5UiKpd6NFfurUc4dFKuz246D2frvas=,tag:PYwdy58De+T+l1q1ZFIJoQ==,type:str] -HONEYCOMB_API_KEY=ENC[AES256_GCM,data:dLTW,iv:JmuG6I7LR6jjNKTpX46r86ZDRyT87Em1YE7ZZZkZ4OE=,tag:hclmkfgwyLR6oVpHGP7clg==,type:str] -API_KEY=ENC[AES256_GCM,data:H9M9,iv:NXDRy323PFP7c5+LtgCal2OkgY5K66CJwJp3Bjj5cJg=,tag:0L1AFfkRSgIyVDqIgrBjXQ==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByRWZudVlCeDV6Ni91RDNz\nMCt1Ujg5SUxnL0Nxenp5Vk1BYkdGUjZHcEFBCkNyRmY2YndaMG41NC9JL2tCYXFr\nWWllUW1TMHMyMS9nZ3B2V0dqb1JzRnMKLS0tIDluRHdZM0lZWFNDU296ZHNMOGlK\nRm53V3ZTd21YRHI5Q3kyT1p3Nml2OGMK0dYKzCiSggxIgIjLabDZe8tfdg78BS0U\nZQnnigGcpawgKxPKm1DvXUJKWZzPtTTehUL3TAQadYe+ZzVOsAnVuA==\n-----END AGE ENCRYPTED FILE-----\n +#ENC[AES256_GCM,data:ixzpn8JBsHx4VCKGoG/ukzt+Hw5xfQWgxgm+37CXzzgz,iv:iPyyMaa5t3ZudyI2Ur+6snXjxIj9/PJtxobZCrKi9uU=,tag:WORvsoa83IYbqNw63qp3wA==,type:comment] +#ENC[AES256_GCM,data:nQYzZC0dQM1k1Vigumi/LqtF+QWNq1VNGXlBRMtlwkiGYm0p6A6RGLhNSn3V1/niAkIfQxIv/DDKpTo=,iv:HwPO7nIo715rKHIv3XIhOM43nmQVMWyZlqtewji9INc=,tag:sZfa6jwlf0Bad6DjuT3qHA==,type:comment] +#ENC[AES256_GCM,data:q5JSDf5A1Z8wo2V9GAWbeyunBkYp+fy9a2vY6YHKR6Tu1lj7VZajOFa6ZOOkzUrM2/GvFF+b0/WhxZZ/m46xPAM6bg==,iv:b+0jiG9VHfVU8i2743psbuCJSR8oRHa2Tr56arepDvE=,tag:Iy290T1CJf8VdF9tr+nZ5g==,type:comment] +PUBPUB_HOSTNAME=ENC[AES256_GCM,data:x1bRTjXLP84FSvgvRa9vkPPH6nW/U31HOTurQxUPL4g=,iv:E1AToe2dW9uwUVQ7Pu+3OrsAzQzjcMx7/s8TVadbfGY=,tag:Ukrrd4UeaDoV4NF5VVcwjw==,type:str] +PUBPUB_URL=ENC[AES256_GCM,data:/0B1/xKMBsAb9X7MSQYMmnSPAhjZ9MDzop2CA2zc8lj96T7muyaI5w==,iv:xO3gTN+6l+rVKB4zwUZcJj+x5V6wXVO7HGoXbKn2BFU=,tag:T25RAIEzgs6qf4o6S8eJTw==,type:str] +POSTGRES_USER=ENC[AES256_GCM,data:T5i9yg94+xU=,iv:nzYa2sLEfmN1P5AmdOAsI364NyfNwAz9L7MjjSczsHk=,tag:6is9latZeE0J7WONuQ13dg==,type:str] +POSTGRES_PASSWORD=ENC[AES256_GCM,data:yRvHPXg5e9c=,iv:FfINJ/IJGMtaxazN/QX2tdR+e18GJpCCPqENd/IPE5c=,tag:YoYl6ytxw+OIGiSC0om/qg==,type:str] +POSTGRES_DB=ENC[AES256_GCM,data:HawR8ZwLaQ==,iv:vfqumT5fR7fhAN7ykCSAwihE2cCubzB/ht12JFCe9To=,tag:jxT8BcqrRjVk3yHetHdQ2g==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:uATftDIHp9gZbY7A4wqrgl2evZrTV9EhscnVlYu8Oj+fpEsRDEPG6pEu5tR9+ZX3pSDDGo/JM9leOq4baFGoaGopgew3DkxGBg==,iv:WCs3OUU4QFFst0OWu8MqcrODqBCoPLtNGJpxNxn/WII=,tag:GMu/XEpMNzkisdxaSdDjVw==,type:str] +PGHOST=ENC[AES256_GCM,data:2GY=,iv:ip1EFGbS8Ffg775N6osnQex/2u5QK1YxRPepUHSrMvk=,tag:WjPC9OB0Fo32jYGTiHathw==,type:str] +PGPORT=ENC[AES256_GCM,data:m+NGuw==,iv:Ux+J+FkEZXrS+ti2v/P7/dRlzEGsRoQSKd+nLKOD2L4=,tag:nx+XmVbxPxcKA3zdDR6KHA==,type:str] +PGUSER=ENC[AES256_GCM,data:erlvtoWGDR4l8dZIBorIsg==,iv:m+0BeGUe4ZqsEFG5AlWhgLt36Wz6oY/7se24Qme3Bt8=,tag:lYjlXKlCZUho5Zp8Lu/6zA==,type:str] +PGPASSWORD=ENC[AES256_GCM,data:7O3i0lQG0mFTDDPrl2GVxHO/flg=,iv:pFY9BS91KjKRb9VAzLmFxqQyogG4BGQuRyBR/Ce4cWs=,tag:V8smg5nFRhB9szHaYOmmQQ==,type:str] +PGDATABASE=ENC[AES256_GCM,data:8pjgs5BVUjfuDGnI54s=,iv:4i3GRXd/P13PISZb29yYhMT/2yuPy5rObkozDGAIJ2A=,tag:wKWznQ2OR7U6uiZgTDuRow==,type:str] +VALKEY_HOST=ENC[AES256_GCM,data:BcwXYIE=,iv:tSsrSPyuXb6+tAkBxvEm3udWE0V92lLA6u3o0+Dciyk=,tag:YOLWOc76QAR4Y+d1YYpm+w==,type:str] +MINIO_ROOT_USER=ENC[AES256_GCM,data:5tjr3+awwjmzJSLq0g==,iv:apBFAR2byKFZ8sjy/lkWwUoXC1rmZv9KrZKzeKrlklw=,tag:k1pWu9WcExsgzCPZbuFN+w==,type:str] +MINIO_ROOT_PASSWORD=ENC[AES256_GCM,data:s+n9950So0B8kOG5HA==,iv:SASe+A7pDEJn6eJg0v2UnL+bcshuelW+PSixKddjpak=,tag:BrxG8+C7NxTI7RPja9wajw==,type:str] +ASSETS_BUCKET_NAME=ENC[AES256_GCM,data:rHdZAhW0EaqlOSEvJYM=,iv:YFFvQic+D1w3tIMLn3PHuLlB9/MJqTh2P4LHNlXGgVg=,tag:4cp/3EwXvY7HRxz3Lo1Odw==,type:str] +ASSETS_UPLOAD_KEY=ENC[AES256_GCM,data:ieCNIBMR61B0rbc=,iv:pd6mHtTMPgFBZ6io5qg3WM9Ffon9GO0lYzKU1Y+8rrg=,tag:V0F+egT/mSWrKmbZEsGlDQ==,type:str] +ASSETS_UPLOAD_SECRET_KEY=ENC[AES256_GCM,data:GLaPa1XdXytkN9Q=,iv:EhzgHGzR/2ni1bG6l5Ium1w7PfiCM45e5mMufwHJ3eQ=,tag:VU5S0i/xalLFIgy0bMk9kw==,type:str] +ASSETS_REGION=ENC[AES256_GCM,data:XUTDDjoeY1N2,iv:2DXZLpVFo5OqkazWsYGSnrGa2WiufFLOPhTsUV1GTGo=,tag:GSPfWoZdh7//8ducoKGdgg==,type:str] +ASSETS_STORAGE_ENDPOINT=ENC[AES256_GCM,data:pO4NHx5m6hKEfzyhJlVo8X4=,iv:2JkwOMGaZuk/8RqZRh3cZDgGQFTk5w2UHj8uFWdvKpY=,tag:SvJGMYN1WuqKpGiTh13z7g==,type:str] +ASSETS_PUBLIC_ENDPOINT=ENC[AES256_GCM,data:m4T89W+moHu1VNMPgiho668KozrLU4SkVt1qu2H2SnZ9GuY3oQFoS3+2WgLsfCw=,iv:+0MRqXhd7cu3ZAaKjJx37COMnOoRIrP4fjaZUoHksG0=,tag:EBdEyncf/oWGlx+Hn4+usw==,type:str] +S3_ENDPOINT=ENC[AES256_GCM,data:GUjQ3FWK1KdnBav4kmuRd9E=,iv:wCRB+Lwx+QBnJytax7OrOD6hDBwfGu6ZwoqRVZQVpsE=,tag:i+P28kG74swa9jDvf5DIqQ==,type:str] +S3_REGION=ENC[AES256_GCM,data:1inzcz2Rm3L7,iv:ZRNBoNrumXyi/MTCHxaG3rEv8PpVF5+lT+87W5VT2J4=,tag:J2HaayclKy/IgZUccpMe4Q==,type:str] +SITE_BUILDER_ENDPOINT=ENC[AES256_GCM,data:8gDPn0h5nz4gnXb9tP3yDD2VUGNlJaNN,iv:DsHRT/Q6Ud/0kzLxLELBM7C2MNxbrmeSg22UwB3eMPI=,tag:JgtTJgIpaNKm1iVcuCEigA==,type:str] +SITE_BUILDER_API_KEY=ENC[AES256_GCM,data:qvts,iv:iFd5uIjDfKQYmQFEeIJM7WvnycKdDDOW9CskR/flApk=,tag:MTQBZqyTAq6Ww3pILR+fGg==,type:str] +MAILGUN_SMTP_HOST=ENC[AES256_GCM,data:4iK2,iv:TPX3ioWPf86USPpgYd+elxHcPlAXDeA2tj9xE2xH1Z0=,tag:K42hjwvbmBfVYMTXjfaQrw==,type:str] +MAILGUN_SMTP_PORT=ENC[AES256_GCM,data:5De1,iv:o6HECjWyCKIlsS/FN3yHHw+0a9LTkTF/WG1yWbJLtVQ=,tag:bVsqjuaxG/gE2JxPKT8tgQ==,type:str] +MAILGUN_SMTP_PASSWORD=ENC[AES256_GCM,data:wNNc,iv:1NiQiHLMEoPlRpy+qlicjQmqSIjs+bDDUOxojr0q/38=,tag:gKldNjaSstVC82BzWdKcDw==,type:str] +MAILGUN_SMTP_USERNAME=ENC[AES256_GCM,data:Fcpm,iv:636evj3kc+IJhV4u6g969UlI9bFjateayohZIulnzJo=,tag:OyMjeEd4z65SwdlJrqUifg==,type:str] +GCLOUD_KEY_FILE=ENC[AES256_GCM,data:YVJm,iv:SGN7zmLxgGJ981KKHJxDG6QPr04rpLcymzMae95yIFA=,tag:Z8gZ4lJItuOm8ozghZxq3g==,type:str] +OTEL_SERVICE_NAME=ENC[AES256_GCM,data:VYSYOkhFpWZc/fAnTDIFvrfF,iv:+FM6fir/Rsy1QrwV7KmxL1/S4qyoFQNLJ4nUH5moCBo=,tag:S2OK5VNjcDNh8XxrAXXCKw==,type:str] +HONEYCOMB_API_KEY=ENC[AES256_GCM,data:83Ol,iv:HhT+b/6XLiLeskgDuOWpueXBCJb5cCbdn/oy5jjA0Ho=,tag:xX8TDYQbB2hPWlQ5+YdLGA==,type:str] +API_KEY=ENC[AES256_GCM,data:pO6p,iv:liz9TPYSA3KcYuUkw23g3OJsD0MIkT1kdC7zsN5GLFU=,tag:tO3N9AfhV79xFKWnHzUGyA==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3N0FndHRjZmQrNWZUMUdF\nd0s1aE82eWI3QngydnNhSkZ2RGtIOVdNTjNRClV6UHp1MDYyZWlEcGEydGM3cm5u\nOVlnV29TdnEzMWVkV0tsS2Z2bDlTdE0KLS0tIFNVUEpCd3ZnR3NucWVVMDBXU1l4\nK1BYNzdxQ1pZR2FFMk0reTdCalRFaVkKJTul7h36wCloMur46INKkCqjLp3MdF45\nduT823QpYgvrcsO9vzyek3V1/5Y4iZ5/vsY/tCKhBXKloElXmuMzdg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEbnE3eGlLWWxiMnlyUGxD\nMklvYk11SDdxb014aGtDaVBXc08xMCtzWlhZClZNYmhFeHQvd0NrdDE5Z2x6SERM\najFmOU0zM2ZOaERYT2dkYkF4czAxY1EKLS0tIE1MUHdVa3FMd0hnZldYWGxaTnlo\nMWlOaE53N0lJTDRYL0xZWTZlRElGZTQKhQUYdy7l3PKCW42Ifch1T2qvZw2xeifF\nLScPBD1sTQBd6zWG+QVfeSHjUbrpHYgvWDZkRtM1g8EJRQI9Ry2CwQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByUVVXWmpvL3B6enZ3SFRi\naFd5UWpJSUdTZFJTOE9JYy8yM1cyZWduRkRRCnlYdlNidEVEbWFNUlBRK0F1c2Qz\nblJXUlpDTWJYUFNxNGtDSWl2SzJpek0KLS0tIFdVUGd2SFMyTmVIanhtTVNGb0Jn\ndUFydXdSb3hHa1JydEtpaFplZXRJbVEKAEwjY12xKIdHvuxJGC4S7dAzkH/JcJsh\ndx3Te//BLDN+8lIv5SsYSf6L/pQXLCV0z1ztvARvYf3LfJyg5xsx1Q==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj -sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWdmZRUVRrOWpVZWcxc3VU\nSW15Nm5zc1BPRDNqdkQxOGFsbmNNcFY4eDNJCkpnR2t3aFpnOWFPVmVsMFlPc2FY\nREgzeUVnajliWDVyTEZSR1pNK1dnVFkKLS0tIHc3akVLdTFnWHNPWGMzOU85UlFT\nNVdPZ09iWDNZdkZQUzBwbXNkVlNKQWMKHoKV5ruHR8omGWRPv9rAl40wunWoFGrP\nuy+Jei87mB0pCc4oghSWI4W1pHKGyaFXqx1cpge/X9ib6DRcK7UVXA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqT1hzS0U2KzVTMldsVkFB\neXM1WFJQTUZaTStWdkt6UWtCbVlKVjhHS1NzCmtCVTZNRkF2ci9icjRsdzVoSldH\nZ0tWMlQraHByVm96RXh0TjE5MUFVQU0KLS0tIEhDaE9EaVdlTFVUR3pwWE9lZEs3\nSWxHQXVYMCtZRzVnbklEbEJiMkcrSjQKTBDADGXiDaOPsehaOKf3X7tXC0N+owJ+\nh5aT/IEkvd7hb6QocKxNpeOCWsdigbcCAFNTdckzwrdOkZsl3ZXsQA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1slx6e48k7fre0ddyu7dtm2wwcqaywn5ke2mkngym3rpazxwvvuyq9qjknk -sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwNDBiNWU2R3VUN0RuR1FG\nY0M3RlN2UFFNTmZ1TE40VkN1WkhHRkhBRnpvClhUM0hvdElsZkl1a0FlTkkvaTI0\nM1BUWnVHbTdBNjRKazBWZ3hiKzhROG8KLS0tIEZLYXFxUVRlWGVESlJtRGhWY1oz\nNGIzbzIyYmcvdWdCVTFtREdjelh5YVUKa5yUPircj6hoy/a1D07vRxv6S8syDUf/\nHc3AIltVEkMtzfekyOXUANJKqCFhUYPSVLD3f1XnIB/bDvZyw7fx6A==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5WTJsYXZlcXhRaFBxZlRy\nN1J0cENQa09EWDgrQi9ucGdDV1NmbnFIMVJRCmpGSzdHRHpkVmd4SWYrMFB2dTJx\na3NyZmRucDZraTRnV1NvM01TNWdMNncKLS0tIDltNzluSEMzKzN4YTlQbzhoejAw\nS1RCN29aVGZsaDhjTjhwS2JidFlvSUkKlNSRT/TRo4KKlgBjFF9/HWJRaktV/UVd\nmdXiqVtOhho/Yy4kWEZLQRxqDt7YrDH5ECUgNGbKcyDZQQvfaY4uWQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h -sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBocmwvU3dsTGZzMXMxUUpr\nMXIwVGRWWjZlYVZqZndLeVQ4UkZoNDRxMUIwCnhRV1VMZjZzckVXRjN2UklMb2Ni\nRU9jUFNURG5JM1JGL3liSGVPUC9uNFEKLS0tIHZkMFhUMFZickhwK0NONEtGNXZn\nZmU0Yjl3aTRiWk02a3dlQlFNK2xqVmcK+4giw+0KrTYDrx1hHLJzmNZD4mKjVnkN\nqEJIuu3ZPBfkm7yWJXFlcgSWOKahE3Epii8j80Cmc5oePSjt1z0spA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLZ2NTaTB1OVc1UWtBSzNO\nb2oxcFF2SFN1YyswRzYweDJFWDdKUUMzeWhFCkdyUE9HZE1xQWtDRlBCTXlrLzBJ\nMzIyNzhnWU4xcCtDWkJnMlpVV0RJYWMKLS0tIGFPYlZoZnMyS1FreVRjQldhZnVq\nU3piYUQ3MEtBQWczdnBCVnh0cWxzRUkKx0saDEVKWI+IVgU04YQSryVo4MR6xHMp\n+VR0/PxDGYqQYHpFwc1AktdAiPEvZOwTgzrvTE9wySgtSCPJjT1LjA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx -sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2SXlGMlVINFV3K0pFSjI0\nVHRwdjRmQmU1a0o5bVNPMUZab3BTMU9TT1ZRCnJEZWNhZmVUUzZhOG9wRlcrbGxN\nS3VMdWNMaS9zQlE2bEI4bkxMMHhNSW8KLS0tIGxRWU1rQ1MyYk00ZzhZa0pBdFFw\nMHNxbklXM1R5MkZGSEJ0MkpzVmpxWlEKHDBJVh6m5H+MbUwO52SnfXHbQfVNlwRT\ndgM7Wiv8ply4s9f1jPl3dkge+W6CHw75v1gvEzQ0tAOssQ+qf6wg4Q==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5aW1BVUU0Y1U1by9Ddnpz\ncFBYaWNIQ3ZJbVZ4QzVkMTlyOERzdGlGU1NVCkt5T3dDUUlnRlhlUmRlVGd6aSs4\naGRFRE5NN3FML0tXQmdYZ0p4NWpOMDQKLS0tIHNjN3VRL1E5Y05RbzhGUmZ3d2t3\najJ4Q2VMN1BVN0VacGtuM1ExRFExRDAKQrWnGKtXXamSfCASE1zSvwvC+gPdcYxB\nrMhOcW5HdCXVZkvO59ltiCLJd7b5H7o6AUMK1uMOoT419Mx28UZ3LA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_5__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -sops_lastmodified=2026-04-02T11:05:34Z -sops_mac=ENC[AES256_GCM,data:lg4QHKFbldLKvhrhW2eEK3CuPjkk2wjwfcxX4KteRi3Jk4CXhWQGLPqEI++TwmOCjngYl0CfkRncDCzkQ1fWqweXl0o4zXo6AzkLmDoNYrTKhxAnb44t4q+LNCmi8aAlNvUXYhEyMBveL2MUgBYRNiJZAsnQbBgLubQjbydcGMI=,iv:BwdF4hd2bJ2pwNC5VieNkIMw6850r42ATh8CFbJRZ2M=,tag:3KOGg69IBt/9iNFcrP0ZeQ==,type:str] +sops_age__list_6__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZT21tTFYwVENDdG5vMG1B\nTHlyTFBsV2RvWXVQaFpsU1lQeFROMXVmSEdrCmI5bHg5VURScytsQTd2R281d3lR\nVUt1YTViandFbGZwZFZyTWhob1JFdHMKLS0tIDUxaGJsNjVWUkR0bmZaUGsxSnRw\nS2E2WlFHc1NBT012MURzTW5zYXFxNFEKVQ8SVvRlpoJuTKdp5Q0cmAo8ftquPrib\nBeCg3X0X5i2rKS+nRy90BidwYpPTZ2plB8NIK/nfJxHWOzYi8C+Qhw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_6__map_recipient=age15fsv503cl9ajfg9fphw45sfq0ytlptnr8wtlt48asytz4ev6rfks2pwt5s +sops_lastmodified=2026-04-02T18:55:19Z +sops_mac=ENC[AES256_GCM,data:RgUAZSuk30t5UIps6cb+xF5xWBhcBt+pimxkye3J8hovDQBAkALX86hB+ZjfZoNS3rbIrwQnQdhuMhxeAYiyYpObr9V6b8slFJLGrpRWLgmbnrZHaiM8PDRxueWgYmPujbBqXNeUyqAJH2Zem3xF5sd0wPcel9VnozOLYKqHTbo=,iv:n71D4PCSG9lbGHHgEGWFFqalBMSRcrQhL6l+mUVD4bI=,tag:7FNgCawYiTuZLz7HWbwvIg==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.12.1 diff --git a/infra/.env.sandbox.enc b/infra/.env.sandbox.enc new file mode 100644 index 0000000000..e02b30bb84 --- /dev/null +++ b/infra/.env.sandbox.enc @@ -0,0 +1,53 @@ +#ENC[AES256_GCM,data:wQkMlWpXCYYMEaRDmPdg3RA/OvC0cDBFaJedPxrpNwM6,iv:IsHL5Sjbj401sFavTanuJX7h+H+OEoa2GLSNTzsNUDw=,tag:EDeMEYS00A4qAyJXh77Kug==,type:comment] +#ENC[AES256_GCM,data:UqliiAya0xIAQedGpGEhynI4sIJeXALcdFveqzu4KiZFwwqi/gESKXNr5x92Xerr7OEWYU2KtLJMWCQ=,iv:T0YcqZQ+qvTSUqje944+5KpyCOBoE7dMj1aT9jK2DPc=,tag:gVaM4+n8jZKd6v2cqtXNsA==,type:comment] +#ENC[AES256_GCM,data:kABkuoRP4sgcmFwQPfq+xezHOXdUpfX7YLQYgfV0332LH5+rlqDt4+f8N5NoDZAe+2yw8ISE+Out4Ox6EcDci/AyuQ==,iv:X8J7TqVOwi5lgpvx3YeTBMZDJmJYS9GwXlrbLR/OoAo=,tag:hDqXJ63chspbuyQ7WVMiFg==,type:comment] +PUBPUB_HOSTNAME=ENC[AES256_GCM,data:iDZwrHQu3wGADdT1kSwCWk1mZQ==,iv:NFA9KSxHFFMZuvosjNpIVyvXIZtcGX5xhJgHMn67TV4=,tag:fo/Jssd/o/1cVede1lFnOQ==,type:str] +PUBPUB_URL=ENC[AES256_GCM,data:y1Gs3NTeUagfhsqwkWI6h3cjZm5Z95YnZlDA,iv:wI3OyMWXXZSvBfsKHxR+hqce9D2adWDSH6VBtrv2MMM=,tag:MTVvRMELsg5jwRcKU0s7pQ==,type:str] +POSTGRES_USER=ENC[AES256_GCM,data:/vX4U7evT1E=,iv:TZZvK7a+f21T387KKXDxqm+rPSQbKnTJPN5tYxnWqu8=,tag:lAlNCQ5NkBX2ftiu/jY3ew==,type:str] +POSTGRES_PASSWORD=ENC[AES256_GCM,data:H9icw27sMmQ=,iv:9GYuSe1L2EnTzFoZul4tQlnEHHq593f9DtExBLrDBtQ=,tag:ZiFtjaSkvGJuM08dfBm8yA==,type:str] +POSTGRES_DB=ENC[AES256_GCM,data:Thed3EFH,iv:PWCQLzbweeeEpVEjNtTKpJcaJBemR2iNLadVFq3A7SY=,tag:JEuwpxYWtIUq6xAExOsM1g==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:1yo00zv51841+lIUhZdWSLBnS47JUkUiwrmDs8SmpXFfwAqNPpEQ7rsC/dv0f8ABwBx4PwOHjddXfq3SwgYZloJdxV/SLBuipw==,iv:Zj6E4p2/v9BUErCyR1GQsYE0LjX4UEx0/jhrtO5/B8I=,tag:PFXkGdKChLevrJfSj4pK8g==,type:str] +PGHOST=ENC[AES256_GCM,data:Mt8=,iv:UzF0ZoKorb+ySIm1mXzPB3M8w/4+dFoz3Kdlo7jakO4=,tag:RG5HHkov+5dRVbxVoy2kIQ==,type:str] +PGPORT=ENC[AES256_GCM,data:xB3yFQ==,iv:0WVtRIRih1H8s7L4x4Io8v+9VaR633r9ut8x5UnCUHc=,tag:G1H1Us7puXaFFxgLLXehog==,type:str] +PGUSER=ENC[AES256_GCM,data:rHHuQdyqN3YBnY7iJDd0zQ==,iv:vxOVRAIdKBgVW0rG7qzREWPs5MizElfN0/AqMJjg5lk=,tag:2WnmBBmDN94EFHyEuoJwFA==,type:str] +PGPASSWORD=ENC[AES256_GCM,data:8I8DapRyDRmeAeXwY1clLndxUns=,iv:FVGsbYwGTOamMFAR0gY2nAqKUBFCEQMWnyPO++EAxZc=,tag:LDQv2CYObp1wtykR6qrrmA==,type:str] +PGDATABASE=ENC[AES256_GCM,data:bH0Qb18zEf2LeUdVo3w=,iv:7Upnx66ettzFGySADOuXGcQ1EeRosv9/URIc3RHEesk=,tag:bkD/aj8MSBHLJEDU/yzcIg==,type:str] +VALKEY_HOST=ENC[AES256_GCM,data:ePZxJrU=,iv:ClkDfCQnHlPoXHqmxF6UIvhiG8X4519HjniBAAxRNps=,tag:Bscl6TVztPo+Tu/gTQRsWw==,type:str] +MINIO_ROOT_USER=ENC[AES256_GCM,data:8lyQwx2Kc3dkvCJaNw==,iv:7y858vcAI3xYBrE1vBzIKLa5lhcor2Q14srqgjcgOFY=,tag:0gVKe8Xcy+A1vcxkEaS8Jw==,type:str] +MINIO_ROOT_PASSWORD=ENC[AES256_GCM,data:ytqhDC/Dymr4WA4tew==,iv:/CrKs/8gL3oY01gi6dlIB2jNOiIZ00w+j6fCkaM6qjo=,tag:+nBoOIwqf1/HCzwsR64W1w==,type:str] +ASSETS_BUCKET_NAME=ENC[AES256_GCM,data:DMraUkrh,iv:GXMwiplfSuXt48tBL5jG/dMoHRNBywOzWa2uF7WL6ec=,tag:tIcPK1s+60KJwv9/ocWDrQ==,type:str] +ASSETS_UPLOAD_KEY=ENC[AES256_GCM,data:IFoPcOcb3megcw4=,iv:Kq6zRXThjQL0mCO3/B5BeuQpJtadGQeHosCnU5axl4I=,tag:6fbMt+6ldYU6soUcGRbwQw==,type:str] +ASSETS_UPLOAD_SECRET_KEY=ENC[AES256_GCM,data:HMxhbIoiZ4WlBO0=,iv:WW/dA7tffeanUEv4jtUI1qsnstNo1utAaF+R1vm+Efc=,tag:BpmHl3kkGVas2RQVJwNDZQ==,type:str] +ASSETS_REGION=ENC[AES256_GCM,data:0xv3/gsjjPsQ,iv:uVL9OlMoJ3FSgce5kh1JoReuJW0vuqt9WaHUkNS2+aU=,tag:lB9z6XS72UjeKIcBvXPaKg==,type:str] +ASSETS_STORAGE_ENDPOINT=ENC[AES256_GCM,data:9immuxMwdiYobH8cVjxE7ZU=,iv:A+0XITtxze9nx7VsC/mp1dAS6FD7L/K0Fk2F132pyu0=,tag:I7/MXr+uKJeDsjE/KaRRwA==,type:str] +ASSETS_PUBLIC_ENDPOINT=ENC[AES256_GCM,data:dFtw7UKsUfJ5DN9qW9b/0tm0GPNcWT9b4VlMKlulrT1H+g==,iv:vtHmXQJyxBc4rLZ6RhBA7lHA1AvVa4ZzX7WxPIzsR+0=,tag:7QUYOisM88mvG4Jg8Wceew==,type:str] +S3_ENDPOINT=ENC[AES256_GCM,data:yZFUvc8DNMd2P6zqNB1HLE8=,iv:/faQgOUyl51Jljj0yxeOfH8lNxOhQEQfF5c7BCHyqFs=,tag:PsTRQScMLffKtS4J+CTf1w==,type:str] +S3_REGION=ENC[AES256_GCM,data:EW+qAXElwVgz,iv:WxUSjYhXXK2yD02AFviiwb+DMPzWQVg8Q6JigGaBa84=,tag:iUsuCSpa9JV/OAXNkjaYNw==,type:str] +SITE_BUILDER_ENDPOINT=ENC[AES256_GCM,data:cZsXNQS47ON0pxq9BUPB/XVnFKjBovrm,iv:zCoEAxGvu7c7/lCERMhMDRVsxShcAjpIoGTndKxMvfM=,tag:a0t+pKnLlFn/FCE2eQ+2EA==,type:str] +SITE_BUILDER_API_KEY=ENC[AES256_GCM,data:Ye8x,iv:0vlogy8jO33CFn6yB4Klv42GEl8yhh37I0iinNiO5ww=,tag:3FDAurEcIf9yZRBjlPPGjw==,type:str] +MAILGUN_SMTP_HOST=ENC[AES256_GCM,data:TU9U,iv:eguxyrPZD6dddfewWZemAPmwfpJc08cu21mHky965Vs=,tag:EkCVeYq5bg24ZvbL8HLJRg==,type:str] +MAILGUN_SMTP_PORT=ENC[AES256_GCM,data:0FYB,iv:lN0gxHtglWA9kTEdQ7tqijTBR/cPv1ikCjhsrVQ8ayU=,tag:DOlLM/GWANhD170Fid2G2A==,type:str] +MAILGUN_SMTP_PASSWORD=ENC[AES256_GCM,data:rr/w,iv:cJSdskUc3n65RIObcPqtDDVLZMASSd7uQhE796Sn8r8=,tag:yEFybDTzla/JRYzAcLUArQ==,type:str] +MAILGUN_SMTP_USERNAME=ENC[AES256_GCM,data:/S7r,iv:igzmz4XbjT+zy4xAmofwYEj7wUisl6jK9aRSd3oHX8E=,tag:79g/DcDmFOwqVUxoOD/1aw==,type:str] +GCLOUD_KEY_FILE=ENC[AES256_GCM,data:elEm,iv:qvRnJ4vjD1MIckiq//ShotqjK9U53n+yMK5G2wTQIl0=,tag:vQQXypwOXATC91Qz/Vaiow==,type:str] +OTEL_SERVICE_NAME=ENC[AES256_GCM,data:2HZugiYSIbDCszsXljx5BqA=,iv:jglJYTXb4MjMH2aYS8/bRLNRqkeXLyEBlbjIaQpTta4=,tag:jNY4FOUICS6TXqS6aEXNEQ==,type:str] +HONEYCOMB_API_KEY=ENC[AES256_GCM,data:tJJ9,iv:CVm+pxPoczTW/WBR8d1XIvohjDQ5Io0/wolUJVrdnX8=,tag:KAvW7aId60xzXUQLLAyhQg==,type:str] +API_KEY=ENC[AES256_GCM,data:CpYn,iv:X4Y/U3Hc7m+vA0NYtwHgRC6hG7SgCZ+JiHTkTDxuSvk=,tag:UNPAuAXs6uM2tISTvUF+Vw==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLdnhGOEVHTFJVa2lWc3BJ\nMU1BTGF4K1ZrVVZna1MwZWFnTzIvbmYxWWljCmY2aXNtaW9xN0JCWWV6R0lzTFlv\nY1h3RU5YdDNLN1hjb25YMEN5aEZyZW8KLS0tIC9qK0x6WVRtbVlRZG9mcjBVNnJM\nc1BFQnRPWlNUMUFGUFVTOVhaREk3TWsKrFmcT1F07M5LqgQgJgfVIa5waj+1spJl\n7few3D2Cq9wRRtf9ZuJ79Sc4beCh0Bvva4mtuttW9+blCrhYSJQRSA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBL3ZDZE85UzhsVmgxNmFD\nZk9Bb1Vaem9BcVk0M1EzbTR1czJCOWp1Z2xRCnFCRXNiZkpkM2tVNitqTG8zKy9x\ncUhlMGFyUG8vbVFaV2wzMENtTkUzcFEKLS0tICtPWlBWVGlQNW1RSlByQmZzdWRX\naDdzM3hZUjBISUJmN3V0TE9WNUxaR1EKu3o0JrF0HBl70WpbURrRgcrqdMqNnk4+\nCNOg3w3FcVXLDDqpX+o7WUTC+x0BM18njBZZOrnHYt061AxuEw12bA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_recipient=age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3Tm1FbXM1N0JJeis5YXBm\nVVp1RGVTQUQ5TXhqa1BqeHJhTlVEeXIwTXprCjg3Tk41UmwzMnJKaWpJbkZwZHJQ\nTUh6bTNjMTg2TTEvdDMyMUtVWHdPWkEKLS0tIHc5bTExaVZvTGpNbEtlck9iMlRJ\ndE9DeFZaa29Qa1NnZnlNWkFYZGJ1TG8KXeZWwVJm+GrGXnuiK7nn16GkiDi542Bs\nJS2BOXvGRWAtU8/uAbQ/EoPttrf2HbpvMMxrPZNXJE4uvzdsaDvPSA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_recipient=age1slx6e48k7fre0ddyu7dtm2wwcqaywn5ke2mkngym3rpazxwvvuyq9qjknk +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxeDY1K1hFa3VJbUpKYlJU\nVS9RRUJubForTERKT0t5ZEsvbTVFaHFJcDBJCkdGb0Y4UEkwNWcyZkJKZEJsbmdH\nTDVVcVhrOTlqUzAwSHM0WG81bDF4UkUKLS0tIGNtMjBMaTh0U3hnNmhLTWlSN3JP\nYVcrY2lwUytCVlIxOXlSM2pjQzRtdEkKBBUNB8fmAbKGt9ESGb1bq3BhQtS7cvjI\neR4Pd7N2wWePYf+Bo0TRyU5hmOv/yJ+06hgdyhDNQnvgXwknitPEyg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDUitPZHNpWEt1UGsrYk14\nU3Foa1JvYll2YTRmK3ArTWtoZHFnMVV1NkFRCklvdWhOUWNOdUNUaGZKQm8wamFW\nbk9xN0tpbFNUYTRWbG9HTGw2ZWVML1UKLS0tIE9ZQXgreDBDa0poY2U1VEFGZlFI\nMHRjc1hJSnV3cnlOZUg5WVJicnJSOTQKGnG7VgD0uP/cZI6zf/M0JSg/4fdL8/W4\ndfXLEOMPw5KQRpOA6ombnm4M+RUeYil3eRxXvQyTUnrK2yVFK2LLzA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_recipient=age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrUVFPd04rRFVUZ2NCWXdz\ncmoxczgybkhPd3dCamNFa2lvZ2gyZjhMcFZnClMwOGxmenZ1dzRLTGJ5SDVEa2hO\nVXppb3Z4R2dsZjlFRDV0cVZPaFhqL28KLS0tIHI3Q1RhWnN6YXJBdjduVGdsNWJl\nTFdQL1BMV1g1bWN1QTBNMjIrZHFoU3MKfdGU6T+QT+KTV/97Tpv8O7+xgEf7Wi7X\nHRnInfAJDhws1Bi2SfDcmzu5H4F99omJuVK2XN/E978IxGxHBxT/Lw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy +sops_age__list_6__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkUTNxRFlXbkdJZDA4MUJV\nRGZ4TWtVSDkvZFZLTHRWcEI1eXhlSkpqdkFvCjFVSFI0R2tjNkJ1MlkweUUyMDdz\nV0poUDhHdFExMVdKQ2hNWFF1ZGxwY3MKLS0tIHVxa2RwTTJrM3gwOU03TElhVzBs\namlpWlU3bk8vYTJwMkZMQTVPcmpiUlkK3QiDHmRoZzUc4EK893quus9qS4K8nOTt\n15uyLed80XG772j3KXu8iz+yZPVqGFnOUcZlK8AMYf+g1He6wim9lQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_6__map_recipient=age15fsv503cl9ajfg9fphw45sfq0ytlptnr8wtlt48asytz4ev6rfks2pwt5s +sops_lastmodified=2026-04-02T18:55:28Z +sops_mac=ENC[AES256_GCM,data:FkocA/nrhQMafwOeLkKbcvrMOG6vcdkvJ+DwMM8uErg2k++awvJHItrO1FtgXxDUZkxOBWtjvuUYVV4yg1qvOf7cuIcC/Zy1Hqxyn3D/lqwbjnCTF25rFZ01dwkJ7fofuyjnGeAZcmLfdWjRntOg0ZVb967Qke+Pl6ZwalnRKO4=,iv:tJ+JIIweLpkwl5D5q2xm0oWpbDn9t0CTNze4db+elTw=,tag:EVEywBkkaV7X/u+DIEmyFw==,type:str] +sops_unencrypted_suffix=_unencrypted +sops_version=3.12.1 diff --git a/infra/.sops.yaml b/infra/.sops.yaml index 51f479ecc3..40eddb2aa2 100644 --- a/infra/.sops.yaml +++ b/infra/.sops.yaml @@ -14,3 +14,4 @@ creation_rules: - age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h - age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx - age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy + - age15fsv503cl9ajfg9fphw45sfq0ytlptnr8wtlt48asytz4ev6rfks2pwt5s From 72fe9dee9d46d4e245a8164a1da99722adbf7c02 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 2 Apr 2026 20:58:01 +0200 Subject: [PATCH 16/16] fix: wait for rollouot --- .github/workflows/preview.yml | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 7aab542734..34e5e08a9a 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -119,7 +119,35 @@ jobs: sudo docker stack services "$STACK_NAME" sudo docker image prune -f - EOS + + # wait until rollout is complete and then clear cache + wait_rollout() { + echo "Beginning wait for rollout..." + svc="$1" + timeout="${2:-600}" + end=$((SECONDS+timeout)) + + while (( SECONDS < end )); do + desired="$(sudo docker service inspect "$svc" --format '{{.Spec.Mode.Replicated.Replicas}}' 2>/dev/null || echo "")" + running="$(sudo docker service ps "$svc" --filter desired-state=running --format '{{.CurrentState}}' 2>/dev/null | grep -c '^Running' || true)" + state="$(sudo docker service inspect "$svc" --format '{{if .UpdateStatus}}{{.UpdateStatus.State}}{{end}}' 2>/dev/null || echo "")" + echo " $svc: desired=$desired running=$running state=$state" + + if [[ -n "$desired" && "$running" == "$desired" ]] && { [[ -z "$state" ]] || [[ "$state" == "completed" ]]; }; then + echo " $svc rollout complete" + return 0 + fi + + sleep 5 + done + + echo "Rollout timeout for $svc" + return 1 + } + + wait_rollout $STACK_NAME 600 + + EOS - name: Teardown preview stack if: inputs.action == 'teardown'