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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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' From a3b5b38f39206488eadbc57fd5e28fdcb0a67d5f Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 11:55:53 +0200 Subject: [PATCH 17/78] fix: don't replicate one off containers --- infra/stack.preview.yml | 5 +++++ infra/stack.yml | 1 + 2 files changed, 6 insertions(+) diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index 7e924a8ff5..2b4d8068b5 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -1,4 +1,7 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/swarmlibs/dockerstack-schema/main/schema/dockerstack-spec.json + services: + proxy: image: ghcr.io/pubpub/caddy-sites:latest env_file: [.env] @@ -66,6 +69,7 @@ services: command: ["pnpm", "--filter", "core", "reset"] networks: [appnet] deploy: + mode: replicated-job replicas: 1 restart_policy: condition: on-failure @@ -135,6 +139,7 @@ services: /usr/bin/mc admin policy attach myminio readwrite --user "$${ASSETS_UPLOAD_KEY}";' networks: [appnet] deploy: + mode: replicated-job replicas: 1 restart_policy: condition: on-failure diff --git a/infra/stack.yml b/infra/stack.yml index b74df9d710..3e15f82f33 100644 --- a/infra/stack.yml +++ b/infra/stack.yml @@ -78,6 +78,7 @@ services: command: ['pnpm', '--filter', 'core', 'migrate-docker'] networks: [appnet] deploy: + mode: replicated-job replicas: 1 restart_policy: condition: on-failure From 7c71256ce9bcecd5f0435e61ac764386426c974e Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 11:57:20 +0200 Subject: [PATCH 18/78] fix: use .env --- infra/stack.preview.yml | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index 2b4d8068b5..c75152a4a0 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -90,10 +90,7 @@ services: db: image: postgres:15 - environment: - POSTGRES_USER: preview - POSTGRES_PASSWORD: preview - POSTGRES_DB: preview + env_file: [.env] volumes: - pgdata:/var/lib/postgresql/data networks: [appnet] @@ -112,9 +109,7 @@ services: minio: image: minio/minio:latest - environment: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin + env_file: [.env] command: server --console-address ":9001" /data networks: [appnet] deploy: @@ -124,12 +119,7 @@ services: 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 + env_file: [.env] entrypoint: > /bin/sh -c ' /usr/bin/mc alias set myminio http://minio:9000 "$${MINIO_ROOT_USER}" "$${MINIO_ROOT_PASSWORD}"; From 095be876cda83ed1f859379e330b628f9109073e Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 12:11:38 +0200 Subject: [PATCH 19/78] fix: update images --- infra/stack.preview.yml | 10 +++++----- infra/stack.yml | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index c75152a4a0..98f4dc8b6d 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -3,7 +3,7 @@ services: proxy: - image: ghcr.io/pubpub/caddy-sites:latest + image: caddy:latest env_file: [.env] volumes: - ./Caddyfile.preview:/etc/caddy/Caddyfile:ro @@ -25,7 +25,7 @@ services: condition: any platform: - image: ghcr.io/pubpub/platform:${IMAGE_TAG} + image: ghcr.io/knowledgefutures/platform:${IMAGE_TAG} env_file: [.env] environment: HOSTNAME: "0.0.0.0" @@ -52,7 +52,7 @@ services: condition: on-failure platform-jobs: - image: ghcr.io/pubpub/platform-jobs:${IMAGE_TAG} + image: ghcr.io/knowledgefutures/platform-jobs:${IMAGE_TAG} env_file: [.env] environment: NODE_ENV: production @@ -64,7 +64,7 @@ services: condition: on-failure platform-migrations: - image: ghcr.io/pubpub/platform-migrations:${IMAGE_TAG} + image: ghcr.io/knowledgefutures/platform-migrations:${IMAGE_TAG} env_file: [.env] command: ["pnpm", "--filter", "core", "reset"] networks: [appnet] @@ -76,7 +76,7 @@ services: max_attempts: 3 site-builder: - image: ghcr.io/pubpub/platform-site-builder:${IMAGE_TAG} + image: ghcr.io/knowledgefutures/platform-site-builder:${IMAGE_TAG} env_file: [.env] environment: NODE_ENV: production diff --git a/infra/stack.yml b/infra/stack.yml index 3e15f82f33..a74b037ee0 100644 --- a/infra/stack.yml +++ b/infra/stack.yml @@ -1,6 +1,6 @@ services: proxy: - image: ghcr.io/pubpub/caddy-sites:latest + image: ghcr.io/knowledgefutures/caddy-sites:latest env_file: [.env] ports: - target: 80 @@ -22,7 +22,7 @@ services: condition: any platform: - image: ghcr.io/pubpub/platform:${IMAGE_TAG} + image: ghcr.io/knowledgefutures/platform:${IMAGE_TAG} env_file: [.env] environment: HOSTNAME: '0.0.0.0' @@ -56,7 +56,7 @@ services: condition: on-failure platform-jobs: - image: ghcr.io/pubpub/platform-jobs:${IMAGE_TAG} + image: ghcr.io/knowledgefutures/platform-jobs:${IMAGE_TAG} env_file: [.env] environment: NODE_ENV: production @@ -73,7 +73,7 @@ services: condition: on-failure platform-migrations: - image: ghcr.io/pubpub/platform-migrations:${IMAGE_TAG} + image: ghcr.io/knowledgefutures/platform-migrations:${IMAGE_TAG} env_file: [.env] command: ['pnpm', '--filter', 'core', 'migrate-docker'] networks: [appnet] @@ -85,7 +85,7 @@ services: max_attempts: 3 site-builder: - image: ghcr.io/pubpub/platform-site-builder:${IMAGE_TAG} + image: ghcr.io/knowledgefutures/platform-site-builder:${IMAGE_TAG} env_file: [.env] environment: NODE_ENV: production From 1dbff9beb7ecdfe74804e1b3a93fb01666262d21 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 12:20:03 +0200 Subject: [PATCH 20/78] fix: don't use env vars in env --- infra/.env.preview.enc | 88 ++++++++++++++++++++--------------------- infra/stack.preview.yml | 4 +- 2 files changed, 47 insertions(+), 45 deletions(-) diff --git a/infra/.env.preview.enc b/infra/.env.preview.enc index 9d0815896f..b54e63e378 100644 --- a/infra/.env.preview.enc +++ b/infra/.env.preview.enc @@ -1,53 +1,53 @@ -#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 +#ENC[AES256_GCM,data:nidgqMhF6BPpOAkjybDjyID3Jk+ruJ0tzTP95NR/YSDr,iv:vMZEEnhdVR9GNULBaxMCO7pdmYKqlool0CSuOQCE9yo=,tag:VUPmLscJ4GugwPMoSJJOgQ==,type:comment] +#ENC[AES256_GCM,data:iyxd17y6Gfg3Rk5rIWFqKC9ymy8wfdybz5Qu2YPniSEy/xSdFVkNN64t3pFbQ4WbRrcWFW1TIVu202Y=,iv:6VCYUtnyxvtdy5s1pNRmbzZbE963e2JZXKkMP93AGsU=,tag:o+KV24A8e+VIeW5rQIx07Q==,type:comment] +#ENC[AES256_GCM,data:zrYTBtr23insJhc0cxCpSpnYLQcbiDlcG66mN339swoLuh18WehqdLU1CSncNcnzpZEKmeuTH647Kz737AvycbBd8w==,iv:M8D9kzlCHkv7fevW13cbOP5i+QKNpvo8sbYe28U175Q=,tag:UkBQg7psnnJSNkWWr0sjVA==,type:comment] +#ENC[AES256_GCM,data:Qu91fCDWhz17mbOOuVnbcQ2WaYX/HSPyyt89hmaO2aq7uRm0oNh6IiTzdKAxXRgGZQ==,iv:Zt6yhJaPOVhJUvzMuca0bA8FF3vNFrYFqukjFsNqoNU=,tag:umXp+S+sRUGTo8jjk0apdA==,type:comment] +#ENC[AES256_GCM,data:DzDLZhFRMXxWrEdUuC+ByAbcB/LS0oOuO98QUmEydk97Nol5GnTQlZL83RFNP+ZXBHhLVg==,iv:0yBbu0BYoJeKcVgtEbyDVkqAcnz1B2O8n3oBYOcXnbY=,tag:JJGzlEuMKv/A53xJi+jlRw==,type:comment] +POSTGRES_USER=ENC[AES256_GCM,data:edyPS/UuBGk=,iv:Ig7o0WjEbvj/PmIL1GH8N1pcV9B/EhH7Lbjg88MAXMc=,tag:4CLTcQWrRZdpOZHXdMGLrw==,type:str] +POSTGRES_PASSWORD=ENC[AES256_GCM,data:hY1Fed8D8NQ=,iv:vj+YUqJ0OWHwg8bFIvfDwC5A1jQ9MiL1dNbWQanxA2k=,tag:+enO5GiEuqYFuXV5D8AqPA==,type:str] +POSTGRES_DB=ENC[AES256_GCM,data:Ne0ZLOnF6Q==,iv:bw7t5um/M5ua2a/pNEy9/sYXe7s1HELp/6HqMNNcrhg=,tag:yngVsBi77evrIWXkLLNyJw==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:+56qlymhMVMFCUqbz4LT5PVuvcfpK+Id3a+/XVPs63TDzlJkO4o4voiE6FIaRA==,iv:E2qbXcRrU1ybOrOjm7dsP7YODeO9KTJaRF4OFdTAZUo=,tag:nveUbdReIYKw2Wd3r3pm/Q==,type:str] +PGHOST=ENC[AES256_GCM,data:298=,iv:uDnWvQEQOp5H6AEvBE+Tq+mj1ArazT4ZQpKO62G9gnU=,tag:/w68jrEyc2luumAEZ52DYg==,type:str] +PGPORT=ENC[AES256_GCM,data:Y94+3w==,iv:qY4d8lhQX52hwru3zaA+A8MG+m/ytFbS33GAYCir7lU=,tag:ETxNdnqZGdALfYZrvBaw5A==,type:str] +PGUSER=ENC[AES256_GCM,data:xkGh3t6J7f8=,iv:NGd8aVC0+oucIhR7OZudQrRzkjlw+sdRcok2fjVt0hU=,tag:ZADkDIN/Cp/HV8p4uDZyrw==,type:str] +PGPASSWORD=ENC[AES256_GCM,data:14Gen0lNZQQ=,iv:S03hq+IBEhCk1Ca5Y2SHNPAZiyGcwlIto1mn3+uH34Y=,tag:Z3//YEGsR+VzV4bB7779iw==,type:str] +PGDATABASE=ENC[AES256_GCM,data:mG/2eH1mBw==,iv:5a/fWA5vkPRTmjHheuyca2y9bzY9Gslpm167eHhcDm8=,tag:NQfpX0JmGWyG1zZmpyjLkA==,type:str] +VALKEY_HOST=ENC[AES256_GCM,data:MTnv2DA=,iv:NGo+0LzXkwlt6dymj9VRLnPEtYXHqZNk4tUDU7uaUMg=,tag:5eoBdPAHIk0OQWB9+nLiAA==,type:str] +MINIO_ROOT_USER=ENC[AES256_GCM,data:aWdSWwbHnD4pNNyMuQ==,iv:VoqB1E6WFxhPny+Y852E75jFflGXjvpVRVqNHTIzBek=,tag:s88CasPCB/x82S6EE1jUyQ==,type:str] +MINIO_ROOT_PASSWORD=ENC[AES256_GCM,data:JNoM6CwnYq4/a3j4Dw==,iv:FgzTPfavvDPOz/xTIRYqo66EKDPwCX+nHQcOvRPyKBw=,tag:wttQ4f4tRbHVF8p6IZe3Tw==,type:str] +ASSETS_BUCKET_NAME=ENC[AES256_GCM,data:EOFuwF2385RW6bCbDf8=,iv:In2u9+UycnY66GVO/7LhQMad3wBE4PRksfiMDI3c7LI=,tag:xR7J05fI5OBM35bYpNeEsQ==,type:str] +ASSETS_UPLOAD_KEY=ENC[AES256_GCM,data:N4wK3CkgdjtQVSk=,iv:c6WxlOBCMWSVYY6GNRpQ5tzJWnP8pRf+n6MJ8nY5ADg=,tag:byB7Y9WEWk2ybAHGtSEd4A==,type:str] +ASSETS_UPLOAD_SECRET_KEY=ENC[AES256_GCM,data:d5CDPp0kCTypKWE=,iv:+40LcVyf95aZu9b2MQZuRxB4N8fej3Ry/U1LtS2flqQ=,tag:jjYp8gmml1UtCeyCc6JSGA==,type:str] +ASSETS_REGION=ENC[AES256_GCM,data:0i+m6S8yS6Kt,iv:XaPxNxCegxPr4RFoqAmXJYuuF4BWxKJf8lJOBmmL0n4=,tag:M37uTRNktpK61fNm+20k0w==,type:str] +ASSETS_STORAGE_ENDPOINT=ENC[AES256_GCM,data:BT26SR8XeWZi3/5Z6M28wyw=,iv:MwWYvcLuANIFoQoBb/9qY0VT2mX7wQUeZ7e6XSLknd0=,tag:sb1GKWqh0fINwgfN9lGsxA==,type:str] +#ENC[AES256_GCM,data:3qx8dRHgjA0Udb10GcGi1QUatiVRCeRqFzlXv4yLN948DElPcw+OvQHTg60y8CB+MIPQ5YvxgI6mKbxMDL78WS+wI3oGpHA=,iv:mdENMvhkmc2cM6fhSnYkbX+V7V8lXNePS3EEUyj9IDU=,tag:S98fTQo8QxdwAqjfnsjEig==,type:comment] +S3_ENDPOINT=ENC[AES256_GCM,data:9Tp5RS5cjO0Dkgjdas8dAF8=,iv:Keg2C6EE6DkCQYb3kjfOEhXiOOcb8bZOcVM68Te5BOY=,tag:qP8k0wTIHi+ojF6BRskUpw==,type:str] +S3_REGION=ENC[AES256_GCM,data:azBHBE28sj1P,iv:ad2DndTnrG6kWuuBALqNSL5zh+yYKFVwegMZcw6OvCE=,tag:KS9DMy2EM2lwzQi9u2atZw==,type:str] +SITE_BUILDER_ENDPOINT=ENC[AES256_GCM,data:aLr+wDnJxNAqPt0H21jee/NDxY892IdV,iv:jh+oFIB+xVCvjq1hxlNURglDG7GKmbEldbL68yqlJUc=,tag:ISFJuSjAX8T1aR3t039xRA==,type:str] +SITE_BUILDER_API_KEY=ENC[AES256_GCM,data:evI4,iv:oKxHDI58Lpr+9h29H1hs5HMEvDrHAqNS6ZWQFTiTKqI=,tag:TELsFmnewcdX9wJM7yYZcw==,type:str] +MAILGUN_SMTP_HOST=ENC[AES256_GCM,data:d6yI,iv:d8WViy1XCNcnNJX7amkTWNLfDFg7lsdoip7vqrQvJB0=,tag:u5zbmoJ5JUpY4V+OMBtXNQ==,type:str] +MAILGUN_SMTP_PORT=ENC[AES256_GCM,data:FdGM,iv:QWUsPJJ4WvA2rXyA4bXsQVtCcSYLYoepaRPGt/td8Tk=,tag:hnX58fNtnPCiatgaq950pQ==,type:str] +MAILGUN_SMTP_PASSWORD=ENC[AES256_GCM,data:tl/7,iv:qMgwLCKffCnwt8CcyAWpbAd0Z5aNCEpqP9ewWg2RDW8=,tag:FvcsrI+x24OdFwYw8QKAqw==,type:str] +MAILGUN_SMTP_USERNAME=ENC[AES256_GCM,data:YQPk,iv:YaS2mgQSW14lihgNTjaK8ll/7mpxja4Wm5oOpARPN9g=,tag:IBv4sYKLRLGsogvCEK3gpA==,type:str] +GCLOUD_KEY_FILE=ENC[AES256_GCM,data:Co9X,iv:gt4oJT7yUdGtKH6odeiMjA1GM3nBlLLq7+eXVOOrRjk=,tag:ZLMmerHqj9Sk+yOyOi0UPg==,type:str] +OTEL_SERVICE_NAME=ENC[AES256_GCM,data:mgGMnpFqu3oBJ/5bV9ECVw1/,iv:9XqB0uULPa1PiW8muEX1i2u+Vu1B52q28U0giHpBeck=,tag:BDOEkreoRF1J3MfbvTj2FQ==,type:str] +HONEYCOMB_API_KEY=ENC[AES256_GCM,data:qckp,iv:4ITYPDkGlkL8N3AIebAnWNzya5egljfJegi19YRFKzE=,tag:3d4dQ/7B9XkTIOroLCdo7A==,type:str] +API_KEY=ENC[AES256_GCM,data:fzdU,iv:Fs+bbbM/GRoVLR86rSnxlOlVKCOi/h7njCo1+nrXzpc=,tag:KXp+CO7ai8c8jjfVooLTVg==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlZzFvR05QcFJtM1ZFdU1M\nemNXdHkvYys3ZTdyRTNpUGtET2duRE9pUTNnClFqNHYzYkxiRG4vYWJSU0ZkcC9H\nUFpxY2dPdnU1WVlCeFRjL2k3ZGxXa3MKLS0tIEtpN1BEMkFIOW5ZVXpxTGprZ0Zl\nR0drNmpSZXBGb3hEblpoODB0c3gydGcKNn+ecg9qgAGyEbOU4gtnT39VDzZvT4ak\nfnueK5RXVY50Ly8bnoj9WTZRyr02AKq/ozxgRWPDFa5eMEDVcXqyLw==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBUThtcHpiN0JsSFBla1RB\nL3hiU3g2cDdkdXorRGJ1QVpEdGFRZDcrdTA0CmE3K2psTEd6MTVPdVN0RHpXem05\naC9Nb0dEMFNBK2JPNFdTMCs5WksvUFEKLS0tIGJTaWh5NklScm4wT2ljeGJYenFh\nMUpOTmpFbG9Sa3c4OTVvZ2svdm11OHMKCdmF8SCgwGBXIqOZMF5tlqgz17sOKzSM\nfp/M2ozHYQ8xP8P8ybhnP1iLc2nB/1ck26uSqalkfzjOPu/s4l3AOg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzc3h6ZDZuby9hZDhNcVE0\nakRwWjJnVVdWRDFrM29KaWJCTi80dGhrNVU4CmFxeWpiTGY2aFYxaDE3cHNhaU9m\ndWZORmJEVWY0ZGFXZndicENkems1bXMKLS0tIEhsNVhlamNhNTVZSVJrZVhOZ3cx\nalo5K0QvTjdBSXhtK2kvNVI5R2FjY0EKqzHsDR5RplJN5ci0eexxho+QV8Vt+vpY\nO20x9wArHYnV5amSBKeQm7JhHeZulgC9+fiA8o3T7qNCMRSkH6R3nw==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1slx6e48k7fre0ddyu7dtm2wwcqaywn5ke2mkngym3rpazxwvvuyq9qjknk -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1azAwRE9tend1bVdPSTZ4\nNkFkd0N5TURBL2Rma1hCU0ZKRGdJNThZZFZzCmZwQmtuN0ZmSnl2cG40TVBFK0tP\naUZsU0hYWU9NdUMzMi8xZGhwMTFhdXcKLS0tIElPVEc4cW0vL3lMekhrUStLTXZQ\nUG1rbk9XWkNQMVkzUkpqZlpNeUIwS2MKR7Vm+f4uwJkSktJNYsyZF4PZFVN/DYIn\nQDtnazxr8ZFTH1VVNn61rHYsQmlxtHGg9SmPbndzFr2bo4tsXzacYA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByMy9QcUhtZkg3dE1Tc21U\nU1pOMnZKTEloZnRTVFE5bWVTZUtTTkpNaG5FCjNoL2J5VGFIQk5XdkNhRjVTR3Qz\nY3diK1duSm9NbXRjUEovZW1mYUtGbnMKLS0tIFVJM1d2OE9GS2VPNTdwRFRkcWJz\nbk1uZE9ZYkl4ampzLzJ0NjVxTW15UEkKSPbbPfAw4Bv2GjXYCXpQWemuYel3BmFH\nStn+foFMzwqB429gsmvy2U7CoisQLE7manKIlnetjnEOzV6aoclIEg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhN1J0cFdxU3F0MmxlYjNm\nRXIwRlVSZU8xQUFWbTVBR2dYZjJNUm9LOFRVClMzdGZXa2VFZGVJU2VFWEREWWMz\nRUpGVzhEa2FDRW9PaG84OCtITVZWaG8KLS0tIDNuaWF3Sy9KRU03R3JMbkV1VkhU\ndWhnNEpFbk5IdDdiQUZTeWFFL1Vjak0KDv+ZaTh5ZB5sBQS+2osR/ZTaHILW43e4\nRgbKXOQRkW8kzeXHaBuYdjGOxf5oIEyfuJSo9RrheNhaWeoogOTt8g==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_5__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBITkNsdFBnOTc3UHFnemVm\nSnlYQSttSEtlZWRmOTVwTE1xMGU4aG55TUJjCmdjcCswK2lrME9EcTl1TzNDU3hU\nMHMrU3RYMlBkS29zaXVrRDV3ZU1SUEEKLS0tIG9TajFsclpqZGdtS2tKb1dWMGY1\nd1lrc29QMldBSEpZNnRLRk9tdFVCTUkKrq6IHWu5lV71KHn6Ei+JThztwFrASP9Y\n18UdGQjw+zkjm8GnPQLlIZNedvp9Q7k0TI4Ouxb5OXfstlNjLhDQGg==\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_lastmodified=2026-04-06T10:19:51Z +sops_mac=ENC[AES256_GCM,data:Ja6VtiRTT9m3EIHmwg8A/YeeIBIJkupopCQFj3ZZELLHlqICpWAwBUnLcDS912e5vC35/ybuPsDVc+uocs091D950R3GvnleIEtEld+x/hX3hQpgiURtzBnvNlaRoXJ/60YiuXwxNWvLy73RVXJpFg2RN3kb7QL3yhE6d4U7Uqg=,iv:2Zf52DAsW7D7z9EGtXrG6oAQrQvk7WuQla7DyvtIm9o=,tag:ZSREwuoFiNBSfKzZVwM2vQ==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.12.1 diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index 98f4dc8b6d..f1ae87cfb7 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -31,8 +31,10 @@ services: HOSTNAME: "0.0.0.0" NODE_ENV: production PORT: "3000" - PUBPUB_URL: https://${PREVIEW_HOST} + PUBPUB_URL: https://preview-${PR_NUMBER}.pubstar.org + PUBPUB_HOSTNAME: preview-${PR_NUMBER}.pubstar.org SITE_BUILDER_ENDPOINT: http://site-builder:4000 + ASSETS_PUBLIC_ENDPOINT: https://preview-${PR_NUMBER}.pubstar.org/assets FLAGS: "uploads:off,invites:off,disabled-actions:http+email" networks: [appnet] healthcheck: From 85d5f81b32d6e923aa1b98ba36b19a2fffbb3507 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 12:34:13 +0200 Subject: [PATCH 21/78] fix: rename assets env vars --- .env.example | 10 +-- Caddyfile.test | 2 +- core/lib/env/env.ts | 18 ++-- core/lib/server/assets.db.test.ts | 4 +- core/lib/server/assets.ts | 38 ++++---- dev.Caddyfile | 6 +- development/docker-compose.dev.yml | 4 +- docker-compose.base.yml | 8 +- docker-compose.test.yml | 10 +-- infra/.env.example | 12 +-- infra/.env.preview.enc | 86 +++++++++--------- infra/.env.sandbox.enc | 88 +++++++++---------- infra/Caddyfile | 8 +- infra/Caddyfile.preview | 8 +- infra/stack.preview.yml | 10 +-- .../context-editor/src/stories/mockUtils.ts | 12 +-- self-host/.env.example | 12 +-- self-host/README.md | 20 ++--- self-host/caddy/Caddyfile | 8 +- self-host/minio-init.sh | 8 +- 20 files changed, 183 insertions(+), 189 deletions(-) diff --git a/.env.example b/.env.example index 12034ca718..db873a652b 100644 --- a/.env.example +++ b/.env.example @@ -15,11 +15,11 @@ 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 +S3_BUCKET_NAME=assets.pubpub.local +S3_ACCESS_KEY=pubpubuser +S3_SECRET_KEY=pubpubpass +S3_REGION=us-east-1 +S3_ENDPOINT=http://localhost:9000 # Email configuration MAILGUN_SMTP_HOST=localhost diff --git a/Caddyfile.test b/Caddyfile.test index 6f645d8b26..e46c20c054 100644 --- a/Caddyfile.test +++ b/Caddyfile.test @@ -24,7 +24,7 @@ example.com { # if you want to use a different domain for your files, you can do so here # for instance, now all your files will be accessible at assets.example.com -# if you go this route, be sure to update your ASSETS_STORAGE_ENDPOINT in .env and restart your services +# if you go this route, be sure to update your S3_ENDPOINT in .env and restart your services # assets.example.com { # reverse_proxy minio:9000 # } diff --git a/core/lib/env/env.ts b/core/lib/env/env.ts index 9fb30ab208..16b8b0a8e1 100644 --- a/core/lib/env/env.ts +++ b/core/lib/env/env.ts @@ -16,20 +16,16 @@ export const env = createEnv({ server: { SELF_HOSTED: z.string().optional(), API_KEY: z.string(), - ASSETS_BUCKET_NAME: z.string(), - ASSETS_REGION: z.string(), - ASSETS_UPLOAD_KEY: z.string(), - ASSETS_UPLOAD_SECRET_KEY: z.string(), - ASSETS_STORAGE_ENDPOINT: z.string().url().optional(), - ASSETS_PUBLIC_ENDPOINT: z + S3_BUCKET_NAME: z.string(), + S3_REGION: z.string(), + S3_ACCESS_KEY: z.string(), + S3_SECRET_KEY: z.string(), + S3_ENDPOINT: z.string().url().optional(), + S3_PUBLIC_ENDPOINT: z .string() .url() .optional() - .transform((val) => - !val && process.env.ASSETS_STORAGE_ENDPOINT - ? process.env.ASSETS_STORAGE_ENDPOINT - : val - ), + .transform((val) => (!val && process.env.S3_ENDPOINT ? process.env.S3_ENDPOINT : val)), /** * Whether or not to verbosely log `memoize` cache hits and misses */ diff --git a/core/lib/server/assets.db.test.ts b/core/lib/server/assets.db.test.ts index 6aef731ca8..83ff57b9b6 100644 --- a/core/lib/server/assets.db.test.ts +++ b/core/lib/server/assets.db.test.ts @@ -11,13 +11,13 @@ const { getTrx } = createForEachMockedTransaction() beforeAll(async () => { // check if minio is up - if (!env.ASSETS_STORAGE_ENDPOINT) { + if (!env.S3_ENDPOINT) { throw new Error( "You should only run this test against a local minio instance, not to prod S3" ) } - const check = await fetch(env.ASSETS_STORAGE_ENDPOINT, { + const check = await fetch(env.S3_ENDPOINT, { method: "OPTIONS", }) diff --git a/core/lib/server/assets.ts b/core/lib/server/assets.ts index 06df71d6ea..5cbcf63763 100644 --- a/core/lib/server/assets.ts +++ b/core/lib/server/assets.ts @@ -63,13 +63,13 @@ export const generateMetadataFromS3 = async ( } export const getS3Client = () => { - const region = env.ASSETS_REGION - const key = env.ASSETS_UPLOAD_KEY - const secret = env.ASSETS_UPLOAD_SECRET_KEY + const region = env.S3_REGION + const key = env.S3_ACCESS_KEY + const secret = env.S3_SECRET_KEY logger.info({ msg: "Initializing S3 client", - endpoint: env.ASSETS_STORAGE_ENDPOINT, + endpoint: env.S3_ENDPOINT, region, key, secret, @@ -79,13 +79,13 @@ export const getS3Client = () => { } s3Client = new S3Client({ - endpoint: env.ASSETS_STORAGE_ENDPOINT, + endpoint: env.S3_ENDPOINT, region: region, credentials: { accessKeyId: key, secretAccessKey: secret, }, - forcePathStyle: !!env.ASSETS_STORAGE_ENDPOINT, // Required for MinIO + forcePathStyle: !!env.S3_ENDPOINT, // Required for MinIO }) logger.info({ @@ -99,10 +99,10 @@ export const getS3Client = () => { // this is bc, when using `minio` locally, the server // uses `minio:9000`, but for the client this does not make sense export const getPublicS3Client = () => { - const region = env.ASSETS_REGION - const key = env.ASSETS_UPLOAD_KEY - const secret = env.ASSETS_UPLOAD_SECRET_KEY - const publicEndpoint = env.ASSETS_PUBLIC_ENDPOINT || env.ASSETS_STORAGE_ENDPOINT + const region = env.S3_REGION + const key = env.S3_ACCESS_KEY + const secret = env.S3_SECRET_KEY + const publicEndpoint = env.S3_PUBLIC_ENDPOINT || env.S3_ENDPOINT return new S3Client({ endpoint: publicEndpoint, @@ -125,7 +125,7 @@ export const generateSignedAssetUploadUrl = async ( const client = getPublicS3Client() // use public client for signed URLs - const bucket = env.ASSETS_BUCKET_NAME + const bucket = env.S3_BUCKET_NAME const command = new PutObjectCommand({ Bucket: bucket, Key: key, @@ -140,7 +140,7 @@ export const generateSignedAssetUploadUrl = async ( const generateSignedUploadUrl = async (key: string) => { const client = getPublicS3Client() - const bucket = env.ASSETS_BUCKET_NAME + const bucket = env.S3_BUCKET_NAME const command = new PutObjectCommand({ Bucket: bucket, Key: key, @@ -175,9 +175,9 @@ export class InvalidFileUrlError extends Error { */ export const deleteFileFromS3 = async (fileUrl: string) => { const client = getPublicS3Client() - const bucket = env.ASSETS_BUCKET_NAME + const bucket = env.S3_BUCKET_NAME - const fileKey = fileUrl.split(new RegExp(`^.+${env.ASSETS_BUCKET_NAME}/`))[1] + const fileKey = fileUrl.split(new RegExp(`^.+${env.S3_BUCKET_NAME}/`))[1] if (!fileKey) { logger.error({ msg: "Unable to parse URL of uploaded file", fileUrl }) @@ -209,7 +209,7 @@ export const makeFileUploadPermanent = async ( }, trx = db ) => { - const matches = tempUrl.match(`(^.+${env.ASSETS_BUCKET_NAME}/)(temporary/.+)`) + const matches = tempUrl.match(`(^.+${env.S3_BUCKET_NAME}/)(temporary/.+)`) const prefix = matches?.[1] const source = matches?.[2] if (!source || !fileName || !prefix) { @@ -233,8 +233,8 @@ export const makeFileUploadPermanent = async ( }) const copyCommand = new CopyObjectCommand({ - CopySource: `${env.ASSETS_BUCKET_NAME}/${source}`, - Bucket: env.ASSETS_BUCKET_NAME, + CopySource: `${env.S3_BUCKET_NAME}/${source}`, + Bucket: env.S3_BUCKET_NAME, Key: newKey, }) @@ -256,7 +256,7 @@ export const makeFileUploadPermanent = async ( maxWaitTime: 10, minDelay: 1, }, - { Bucket: env.ASSETS_BUCKET_NAME, Key: newKey } + { Bucket: env.S3_BUCKET_NAME, Key: newKey } ) logger.debug({ msg: "successfully copied temp file to permanent directory", newKey, tempUrl }) await trx @@ -305,7 +305,7 @@ export const uploadFileToS3 = async ( } ): Promise => { const client = getS3Client() - const bucket = env.ASSETS_BUCKET_NAME + const bucket = env.S3_BUCKET_NAME const key = `${id}/${fileName}` const parallelUploads3 = new Upload({ diff --git a/dev.Caddyfile b/dev.Caddyfile index f8ece58b87..21f1093e76 100644 --- a/dev.Caddyfile +++ b/dev.Caddyfile @@ -45,13 +45,13 @@ # visit: http://localhost:8080/my-community/journal-2024/ handle_path /sites/* { - import s3site {$ASSETS_BUCKET_NAME:assets} {$S3_ENDPOINT:garage:3900} + import s3site {$S3_BUCKET_NAME:assets} {$S3_ENDPOINT:garage:3900} } # simpler path without /sites prefix # handles /{communitySlug}/{subpath}/... handle /* { - import s3site "{$ASSETS_BUCKET_NAME:assets}/sites" {$S3_ENDPOINT:garage:3900} + import s3site "{$S3_BUCKET_NAME:assets}/sites" {$S3_ENDPOINT:garage:3900} } } @@ -72,7 +72,7 @@ # # { # filesystem sites s3 { -# bucket {$ASSETS_BUCKET_NAME:assets} +# bucket {$S3_BUCKET_NAME:assets} # region {$S3_REGION:garage} # endpoint {$S3_ENDPOINT:http://garage:3900} # use_path_style diff --git a/development/docker-compose.dev.yml b/development/docker-compose.dev.yml index eb6607257c..497ae24538 100644 --- a/development/docker-compose.dev.yml +++ b/development/docker-compose.dev.yml @@ -86,8 +86,8 @@ services: volumes: - ../dev.Caddyfile:/etc/caddy/Caddyfile environment: - - ASSETS_BUCKET_NAME=${ASSETS_BUCKET_NAME:-assets.v7.pubpub.org} - - S3_REGION=${ASSETS_REGION:-garage} + - S3_BUCKET_NAME=${S3_BUCKET_NAME:-assets.v7.pubpub.org} + - S3_REGION=${S3_REGION:-garage} - S3_ENDPOINT=garage:3900 ports: - "8080:8080" diff --git a/docker-compose.base.yml b/docker-compose.base.yml index 5a61122955..0a9be47737 100644 --- a/docker-compose.base.yml +++ b/docker-compose.base.yml @@ -32,10 +32,10 @@ services: 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}";' + /usr/bin/mc mb --ignore-existing myminio/"$${S3_BUCKET_NAME}"; + /usr/bin/mc anonymous set download myminio/"$${S3_BUCKET_NAME}"; + /usr/bin/mc admin user add myminio "$${S3_ACCESS_KEY}" "$${S3_SECRET_KEY}"; + /usr/bin/mc admin policy attach myminio readwrite --user "$${S3_ACCESS_KEY}";' db: image: postgres:15 diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 5e7cc46fc8..f00f4f8242 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -130,11 +130,11 @@ services: - integration environment: - PUBPUB_URL=http://integration-tests:3000 - - S3_ENDPOINT=${ASSETS_STORAGE_ENDPOINT:-http://minio:9000} - - S3_REGION=${ASSETS_REGION:-us-east-1} - - S3_ACCESS_KEY=${ASSETS_UPLOAD_KEY:-preview-different} - - S3_SECRET_KEY=${ASSETS_UPLOAD_SECRET_KEY:-preview-different123} - - S3_BUCKET_NAME=${ASSETS_BUCKET_NAME:-byron} + - S3_ENDPOINT=${S3_ENDPOINT:-http://minio:9000} + - S3_REGION=${S3_REGION:-us-east-1} + - S3_ACCESS_KEY=${S3_ACCESS_KEY:-preview-different} + - S3_SECRET_KEY=${S3_SECRET_KEY:-preview-different123} + - S3_BUCKET_NAME=${S3_BUCKET_NAME:-byron} - PORT=4000 volumes: diff --git a/infra/.env.example b/infra/.env.example index f8e6ec7ab3..b90d0eb5d7 100644 --- a/infra/.env.example +++ b/infra/.env.example @@ -20,12 +20,12 @@ 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_BUCKET_NAME=assets +S3_ACCESS_KEY= +S3_SECRET_KEY= +S3_REGION=us-east-1 +S3_ENDPOINT=http://minio:9000 +S3_ENDPOINT=https://app.pubpub.org/a S3_ENDPOINT=http://minio:9000 S3_REGION=us-east-1 diff --git a/infra/.env.preview.enc b/infra/.env.preview.enc index b54e63e378..2a40937253 100644 --- a/infra/.env.preview.enc +++ b/infra/.env.preview.enc @@ -1,53 +1,51 @@ -#ENC[AES256_GCM,data:nidgqMhF6BPpOAkjybDjyID3Jk+ruJ0tzTP95NR/YSDr,iv:vMZEEnhdVR9GNULBaxMCO7pdmYKqlool0CSuOQCE9yo=,tag:VUPmLscJ4GugwPMoSJJOgQ==,type:comment] -#ENC[AES256_GCM,data:iyxd17y6Gfg3Rk5rIWFqKC9ymy8wfdybz5Qu2YPniSEy/xSdFVkNN64t3pFbQ4WbRrcWFW1TIVu202Y=,iv:6VCYUtnyxvtdy5s1pNRmbzZbE963e2JZXKkMP93AGsU=,tag:o+KV24A8e+VIeW5rQIx07Q==,type:comment] -#ENC[AES256_GCM,data:zrYTBtr23insJhc0cxCpSpnYLQcbiDlcG66mN339swoLuh18WehqdLU1CSncNcnzpZEKmeuTH647Kz737AvycbBd8w==,iv:M8D9kzlCHkv7fevW13cbOP5i+QKNpvo8sbYe28U175Q=,tag:UkBQg7psnnJSNkWWr0sjVA==,type:comment] -#ENC[AES256_GCM,data:Qu91fCDWhz17mbOOuVnbcQ2WaYX/HSPyyt89hmaO2aq7uRm0oNh6IiTzdKAxXRgGZQ==,iv:Zt6yhJaPOVhJUvzMuca0bA8FF3vNFrYFqukjFsNqoNU=,tag:umXp+S+sRUGTo8jjk0apdA==,type:comment] -#ENC[AES256_GCM,data:DzDLZhFRMXxWrEdUuC+ByAbcB/LS0oOuO98QUmEydk97Nol5GnTQlZL83RFNP+ZXBHhLVg==,iv:0yBbu0BYoJeKcVgtEbyDVkqAcnz1B2O8n3oBYOcXnbY=,tag:JJGzlEuMKv/A53xJi+jlRw==,type:comment] -POSTGRES_USER=ENC[AES256_GCM,data:edyPS/UuBGk=,iv:Ig7o0WjEbvj/PmIL1GH8N1pcV9B/EhH7Lbjg88MAXMc=,tag:4CLTcQWrRZdpOZHXdMGLrw==,type:str] -POSTGRES_PASSWORD=ENC[AES256_GCM,data:hY1Fed8D8NQ=,iv:vj+YUqJ0OWHwg8bFIvfDwC5A1jQ9MiL1dNbWQanxA2k=,tag:+enO5GiEuqYFuXV5D8AqPA==,type:str] -POSTGRES_DB=ENC[AES256_GCM,data:Ne0ZLOnF6Q==,iv:bw7t5um/M5ua2a/pNEy9/sYXe7s1HELp/6HqMNNcrhg=,tag:yngVsBi77evrIWXkLLNyJw==,type:str] -DATABASE_URL=ENC[AES256_GCM,data:+56qlymhMVMFCUqbz4LT5PVuvcfpK+Id3a+/XVPs63TDzlJkO4o4voiE6FIaRA==,iv:E2qbXcRrU1ybOrOjm7dsP7YODeO9KTJaRF4OFdTAZUo=,tag:nveUbdReIYKw2Wd3r3pm/Q==,type:str] -PGHOST=ENC[AES256_GCM,data:298=,iv:uDnWvQEQOp5H6AEvBE+Tq+mj1ArazT4ZQpKO62G9gnU=,tag:/w68jrEyc2luumAEZ52DYg==,type:str] -PGPORT=ENC[AES256_GCM,data:Y94+3w==,iv:qY4d8lhQX52hwru3zaA+A8MG+m/ytFbS33GAYCir7lU=,tag:ETxNdnqZGdALfYZrvBaw5A==,type:str] -PGUSER=ENC[AES256_GCM,data:xkGh3t6J7f8=,iv:NGd8aVC0+oucIhR7OZudQrRzkjlw+sdRcok2fjVt0hU=,tag:ZADkDIN/Cp/HV8p4uDZyrw==,type:str] -PGPASSWORD=ENC[AES256_GCM,data:14Gen0lNZQQ=,iv:S03hq+IBEhCk1Ca5Y2SHNPAZiyGcwlIto1mn3+uH34Y=,tag:Z3//YEGsR+VzV4bB7779iw==,type:str] -PGDATABASE=ENC[AES256_GCM,data:mG/2eH1mBw==,iv:5a/fWA5vkPRTmjHheuyca2y9bzY9Gslpm167eHhcDm8=,tag:NQfpX0JmGWyG1zZmpyjLkA==,type:str] -VALKEY_HOST=ENC[AES256_GCM,data:MTnv2DA=,iv:NGo+0LzXkwlt6dymj9VRLnPEtYXHqZNk4tUDU7uaUMg=,tag:5eoBdPAHIk0OQWB9+nLiAA==,type:str] -MINIO_ROOT_USER=ENC[AES256_GCM,data:aWdSWwbHnD4pNNyMuQ==,iv:VoqB1E6WFxhPny+Y852E75jFflGXjvpVRVqNHTIzBek=,tag:s88CasPCB/x82S6EE1jUyQ==,type:str] -MINIO_ROOT_PASSWORD=ENC[AES256_GCM,data:JNoM6CwnYq4/a3j4Dw==,iv:FgzTPfavvDPOz/xTIRYqo66EKDPwCX+nHQcOvRPyKBw=,tag:wttQ4f4tRbHVF8p6IZe3Tw==,type:str] -ASSETS_BUCKET_NAME=ENC[AES256_GCM,data:EOFuwF2385RW6bCbDf8=,iv:In2u9+UycnY66GVO/7LhQMad3wBE4PRksfiMDI3c7LI=,tag:xR7J05fI5OBM35bYpNeEsQ==,type:str] -ASSETS_UPLOAD_KEY=ENC[AES256_GCM,data:N4wK3CkgdjtQVSk=,iv:c6WxlOBCMWSVYY6GNRpQ5tzJWnP8pRf+n6MJ8nY5ADg=,tag:byB7Y9WEWk2ybAHGtSEd4A==,type:str] -ASSETS_UPLOAD_SECRET_KEY=ENC[AES256_GCM,data:d5CDPp0kCTypKWE=,iv:+40LcVyf95aZu9b2MQZuRxB4N8fej3Ry/U1LtS2flqQ=,tag:jjYp8gmml1UtCeyCc6JSGA==,type:str] -ASSETS_REGION=ENC[AES256_GCM,data:0i+m6S8yS6Kt,iv:XaPxNxCegxPr4RFoqAmXJYuuF4BWxKJf8lJOBmmL0n4=,tag:M37uTRNktpK61fNm+20k0w==,type:str] -ASSETS_STORAGE_ENDPOINT=ENC[AES256_GCM,data:BT26SR8XeWZi3/5Z6M28wyw=,iv:MwWYvcLuANIFoQoBb/9qY0VT2mX7wQUeZ7e6XSLknd0=,tag:sb1GKWqh0fINwgfN9lGsxA==,type:str] -#ENC[AES256_GCM,data:3qx8dRHgjA0Udb10GcGi1QUatiVRCeRqFzlXv4yLN948DElPcw+OvQHTg60y8CB+MIPQ5YvxgI6mKbxMDL78WS+wI3oGpHA=,iv:mdENMvhkmc2cM6fhSnYkbX+V7V8lXNePS3EEUyj9IDU=,tag:S98fTQo8QxdwAqjfnsjEig==,type:comment] -S3_ENDPOINT=ENC[AES256_GCM,data:9Tp5RS5cjO0Dkgjdas8dAF8=,iv:Keg2C6EE6DkCQYb3kjfOEhXiOOcb8bZOcVM68Te5BOY=,tag:qP8k0wTIHi+ojF6BRskUpw==,type:str] -S3_REGION=ENC[AES256_GCM,data:azBHBE28sj1P,iv:ad2DndTnrG6kWuuBALqNSL5zh+yYKFVwegMZcw6OvCE=,tag:KS9DMy2EM2lwzQi9u2atZw==,type:str] -SITE_BUILDER_ENDPOINT=ENC[AES256_GCM,data:aLr+wDnJxNAqPt0H21jee/NDxY892IdV,iv:jh+oFIB+xVCvjq1hxlNURglDG7GKmbEldbL68yqlJUc=,tag:ISFJuSjAX8T1aR3t039xRA==,type:str] -SITE_BUILDER_API_KEY=ENC[AES256_GCM,data:evI4,iv:oKxHDI58Lpr+9h29H1hs5HMEvDrHAqNS6ZWQFTiTKqI=,tag:TELsFmnewcdX9wJM7yYZcw==,type:str] -MAILGUN_SMTP_HOST=ENC[AES256_GCM,data:d6yI,iv:d8WViy1XCNcnNJX7amkTWNLfDFg7lsdoip7vqrQvJB0=,tag:u5zbmoJ5JUpY4V+OMBtXNQ==,type:str] -MAILGUN_SMTP_PORT=ENC[AES256_GCM,data:FdGM,iv:QWUsPJJ4WvA2rXyA4bXsQVtCcSYLYoepaRPGt/td8Tk=,tag:hnX58fNtnPCiatgaq950pQ==,type:str] -MAILGUN_SMTP_PASSWORD=ENC[AES256_GCM,data:tl/7,iv:qMgwLCKffCnwt8CcyAWpbAd0Z5aNCEpqP9ewWg2RDW8=,tag:FvcsrI+x24OdFwYw8QKAqw==,type:str] -MAILGUN_SMTP_USERNAME=ENC[AES256_GCM,data:YQPk,iv:YaS2mgQSW14lihgNTjaK8ll/7mpxja4Wm5oOpARPN9g=,tag:IBv4sYKLRLGsogvCEK3gpA==,type:str] -GCLOUD_KEY_FILE=ENC[AES256_GCM,data:Co9X,iv:gt4oJT7yUdGtKH6odeiMjA1GM3nBlLLq7+eXVOOrRjk=,tag:ZLMmerHqj9Sk+yOyOi0UPg==,type:str] -OTEL_SERVICE_NAME=ENC[AES256_GCM,data:mgGMnpFqu3oBJ/5bV9ECVw1/,iv:9XqB0uULPa1PiW8muEX1i2u+Vu1B52q28U0giHpBeck=,tag:BDOEkreoRF1J3MfbvTj2FQ==,type:str] -HONEYCOMB_API_KEY=ENC[AES256_GCM,data:qckp,iv:4ITYPDkGlkL8N3AIebAnWNzya5egljfJegi19YRFKzE=,tag:3d4dQ/7B9XkTIOroLCdo7A==,type:str] -API_KEY=ENC[AES256_GCM,data:fzdU,iv:Fs+bbbM/GRoVLR86rSnxlOlVKCOi/h7njCo1+nrXzpc=,tag:KXp+CO7ai8c8jjfVooLTVg==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlZzFvR05QcFJtM1ZFdU1M\nemNXdHkvYys3ZTdyRTNpUGtET2duRE9pUTNnClFqNHYzYkxiRG4vYWJSU0ZkcC9H\nUFpxY2dPdnU1WVlCeFRjL2k3ZGxXa3MKLS0tIEtpN1BEMkFIOW5ZVXpxTGprZ0Zl\nR0drNmpSZXBGb3hEblpoODB0c3gydGcKNn+ecg9qgAGyEbOU4gtnT39VDzZvT4ak\nfnueK5RXVY50Ly8bnoj9WTZRyr02AKq/ozxgRWPDFa5eMEDVcXqyLw==\n-----END AGE ENCRYPTED FILE-----\n +#ENC[AES256_GCM,data:01FuyTOAi8hcwTlE3h5bzuxLnxIcAUUOaBWOgewecUak,iv:wlTDb983ry3QmHPTNO831TCRc/y1rNfVBdEBltsjQHk=,tag:Jx4ihkq9zJe5DrhKh3VzUg==,type:comment] +#ENC[AES256_GCM,data:F8lt5UbzAe/bl9JjbNo9PMP/XbpxNDqubPdlBIy5G7FlBXHKAs4kMOUrcG749TP7QfdjRbUVxGWTeRE=,iv:VCsmfGyoATmwB7ZA7q+cE/5hAdvhctqUR3V36uyvugc=,tag:+iAPVAkvIv5mBoKmyAcRXg==,type:comment] +#ENC[AES256_GCM,data:SfJkrX4rrOWnflZeLZB+OTo5ZEjmoa+hGALSh6OXyWhnMwYuWgj4imsw/Pt06BnD6mULcUX6w/WSbxiChpK6Rz2Uow==,iv:fxo360dsbqLLuyQRoxnbgLsP0XFurYGwCV7Q2mule/o=,tag:96vqOoeBrInNq0iAvHygFA==,type:comment] +#ENC[AES256_GCM,data:S1LlGLvEHPWvhx4deVBIM4LlBQCRLoyZBpwhLqqabmUHJmg5ZU4u7h15VZdxulQUCA==,iv:HB1P2F3SbjlG6WfU/M03X/Ix0iZQhvZlTCmQG+9Dm+k=,tag:fYGwJb5upU4BbHTFn5sXDA==,type:comment] +#ENC[AES256_GCM,data:18ixZWR90g7X5C3/wu/lG8mCRS5fvfe5PrCThAwBqCqSpYmnCmw3kX9NtKHVTwt1Po0mkw==,iv:hxsNw3R9ionzxa70IjaXLyk++KfW6lCEfCI8yot3Ecg=,tag:61sgT8b7SWWlW7u/hAbbUQ==,type:comment] +POSTGRES_USER=ENC[AES256_GCM,data:NykRLilL23I=,iv:uSUgclbI+u5HlqSazcXTdQMF3+6MPnF5gBqhgFrdxQ8=,tag:XFNiQkZRzfClqyzaPDqGGg==,type:str] +POSTGRES_PASSWORD=ENC[AES256_GCM,data:cjdQF1aMDBA=,iv:tkZMn6+06SKK95jTNmuTsdrWcuB3vQfTkOYaIKTwpCc=,tag:V09wYVU5J+mtkCrmQ9+2Mw==,type:str] +POSTGRES_DB=ENC[AES256_GCM,data:euB33ubvYg==,iv:eq7IpQpUOqO4RGLmR+i93W1Th9ldQxO+s6UOKMrP+do=,tag:K//krGc7ryYjXqdJoAZu/w==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:0lZeeWD8t16+wVoVx3GG5avuecZvJtEjfRq3r3DldeNQq66C6umx/s84LcdoVA==,iv:pBG3RlFFjyUSgCEVVMesLd21GM/ChoPw6Kw5ZY9ErII=,tag:aHv0zEgRyjUO/HyjkrIShg==,type:str] +PGHOST=ENC[AES256_GCM,data:ti8=,iv:Y+g2zJxw6AZnm3JXLDuUJFtOkPUMiyTFCsm+0GoA1VI=,tag:2jp/X+VV7SNVbMUmC1e7xA==,type:str] +PGPORT=ENC[AES256_GCM,data:2RhQ/g==,iv:8rikqp+jWc6t8cJLLuLTrGhetFI7wu+LLUxI2QMCMAI=,tag:HtqOSp2JKUAZkHq6m6KDoQ==,type:str] +PGUSER=ENC[AES256_GCM,data:rjuJ2J89/Dw=,iv:BAK/cCaQYwyJiMV+pAVTRToBF5s12/A8WXTxT9RRjN4=,tag:n4hBU76ZReuJdNpCs+cowA==,type:str] +PGPASSWORD=ENC[AES256_GCM,data:EQHAp8G72pU=,iv:xKbh12AcnOBUcw+swC3McPYb23nyL12RV9iDt8qKBj0=,tag:IahOy866FQBSqaIvFCbq3w==,type:str] +PGDATABASE=ENC[AES256_GCM,data:nYwXkfTO+w==,iv:qq86WLG0XxFiwiQuhGiQXduAkqUjSzEgLPxJTnc02V0=,tag:VVf9bzQtqR5O56X+yMtyeQ==,type:str] +VALKEY_HOST=ENC[AES256_GCM,data:OlI8Ois=,iv:apN+8DyufGyGHh/+fGJG/4Zt1SSLHr8h9IJzWAVcc5g=,tag:mJoNwGnuv/yGygA4SE81Gw==,type:str] +MINIO_ROOT_USER=ENC[AES256_GCM,data:R1xiVwU4mHU8le4zaA==,iv:hQE2tkfqR0x2WisPx+g89YtPC1+YX4LkHyZ608OxzI8=,tag:fYJvFtC/vwW4hfFj2k32Iw==,type:str] +MINIO_ROOT_PASSWORD=ENC[AES256_GCM,data:JJoqNeEK/bEFlsP4xQ==,iv:pdBqmcJ+Pxhv3ZrIs5DTQSwHmsfjqhlbpCkO1Nv/OZk=,tag:sWZ0GxJfxQHYE51SFtghxA==,type:str] +S3_BUCKET_NAME=ENC[AES256_GCM,data:33QLz1weIvl27fkuz0M=,iv:4QWmPSPE2YpYP6dGc3hNzHq4EW2vRRANG0Sn/1OSQLE=,tag:0Tqwb5DvhwprvXeNw4YNCw==,type:str] +S3_ACCESS_KEY=ENC[AES256_GCM,data:GtYFUF+JbS57Nmw=,iv:AxwCMu9e4yJ/tiemM+Wyk6QqYdzQ3oZAjicDMKVEXWA=,tag:hXplo3UDLXy0suWJ2KGM+w==,type:str] +S3_SECRET_KEY=ENC[AES256_GCM,data:qV3HZp7Qem7u2+Q=,iv:Dwat6yRxAFMu2C1Lhl1J6K+iZ2X6/n9jmdjlz2N7AK4=,tag:YreDcISrsSTMpUPcGsHvaA==,type:str] +S3_REGION=ENC[AES256_GCM,data:liRqnsqwJ0Ct,iv:XUfhFifl/MZWf9Yy/02Bhjvlp4jBuQEML29dg+18SvY=,tag:ICTYnLtmRbVVjyYAK3nTnw==,type:str] +S3_ENDPOINT=ENC[AES256_GCM,data:jzzGDkgL33OjHDCn/JrCeac=,iv:4xyA52QskQeXZLaMWqAF8h+JYiUKNdoaBqoWknxLe0U=,tag:pVdb89AY/ZEirwQTpOwvLQ==,type:str] +#ENC[AES256_GCM,data:ZvethfQn3G93hzb9HylzPPV+SNHU1dLdUWddhZKfKYDXR48m4/pxI0Bfem2xEuG6N5vI756KFzhnjwDqEyk/zzKO1g==,iv:gqU2h8tctMH8UEixoGeze/u1osGk2HjPIn6KJkA7oss=,tag:bba0OSt+ukemN2PAA+m1mA==,type:comment] +SITE_BUILDER_ENDPOINT=ENC[AES256_GCM,data:P3Aohsj9hrSbbEB4cIZH5e4Dwic4i131,iv:NRSCdyFm+LcaoSEoin27cFkIQEYVHUZcnyKtHChlDn8=,tag:5jQP0xeUwsS8YU3fi3oNRw==,type:str] +SITE_BUILDER_API_KEY=ENC[AES256_GCM,data:h/Tt,iv:OHa3LPP4HAC+/6ynmQyqu/4kIOW5jTBhPLmmjBAXerA=,tag:UAUvhlNI9t/gMcG0bZAfzA==,type:str] +MAILGUN_SMTP_HOST=ENC[AES256_GCM,data:il5J,iv:2qhdXyt8M9Waj2Blp3/bZ6Lbd1CSnL44ni0w9BKMk2M=,tag:6L7LrjsV5dzd4SJ2CAOwbA==,type:str] +MAILGUN_SMTP_PORT=ENC[AES256_GCM,data:Cq0A,iv:HyZccNohY3LOBL83imGrKJRXaI5HNnOAbfkf7jsgUUU=,tag:onTdFiHQVfmMq38ffw7QLA==,type:str] +MAILGUN_SMTP_PASSWORD=ENC[AES256_GCM,data:Ux3L,iv:b72bNCwUdbI/ncfm0mozrkgnF7vPsBTFFWydptlh10w=,tag:HzbugC9DzQfnym0y6O91fg==,type:str] +MAILGUN_SMTP_USERNAME=ENC[AES256_GCM,data:u/w/,iv:+8s/rhemnPmmFFKg8HiB5KFT/kHfVwXrh6F7NKVBdwA=,tag:EZ9veppRkG4miHNCDKzETA==,type:str] +GCLOUD_KEY_FILE=ENC[AES256_GCM,data:1dSh,iv:rr9/qGYRH+BGmI+h12CmcG8qccbXhji7RPXMg3O2oTk=,tag:XeeEFc4MvUq80c9CWCL8pg==,type:str] +OTEL_SERVICE_NAME=ENC[AES256_GCM,data:geghunAlbZHbEr97CPbirtSo,iv:9DDQOV0iuBVAlLrkEMoPFAFAqxZQx+3wdea8QkzhbxM=,tag:WbpHuAka03EJLZSf82pjGg==,type:str] +HONEYCOMB_API_KEY=ENC[AES256_GCM,data:L2rw,iv:7ZXT6tLTPX8K8cdFFmr8XB9aM5PiXoLabzk+dXDAerM=,tag:RRicQ25cXLFSfXdHO0iXmw==,type:str] +API_KEY=ENC[AES256_GCM,data:qcnV,iv:8aVJLcGajFM/1Wclg6UiitCRRl94es2VBZycM0/yvII=,tag:VZVCW6CPEJeZV0T5LSfP+w==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1cXRndVVCQmF5OUg3RUtW\nS0RMUGdhcFRUR0poUis1cm5nclRZbDZsVGxrCk1EVG9kRHRPRzl3ci9KcVBzQ09G\nZmFvTG5CbXA1bk5ZWHdEZGRMWk9pR0kKLS0tIFo4dytQQzlQK3lVdTdxVlZBQ2M3\nMDZYSFJZTUF4MmNtOEtQSGF4SDduM1EKevzGXoKoCU0hEWKcecBnZ6+9B3kY35c8\na9gU3yrDoMP2fA7+6Iu5+cgJt2Ise+ddGSY52kg7XobY4DCg/Ufo/A==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBUThtcHpiN0JsSFBla1RB\nL3hiU3g2cDdkdXorRGJ1QVpEdGFRZDcrdTA0CmE3K2psTEd6MTVPdVN0RHpXem05\naC9Nb0dEMFNBK2JPNFdTMCs5WksvUFEKLS0tIGJTaWh5NklScm4wT2ljeGJYenFh\nMUpOTmpFbG9Sa3c4OTVvZ2svdm11OHMKCdmF8SCgwGBXIqOZMF5tlqgz17sOKzSM\nfp/M2ozHYQ8xP8P8ybhnP1iLc2nB/1ck26uSqalkfzjOPu/s4l3AOg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKYThXaFhnN0tVUU9sVjJ6\nRFY1Mk9rVDRFcjZ5eTZjbTNuNWpROFpXdXlRCkRNUkhhYVRjZjhRSEIxU1NJdlpD\nQVR5MDRMc2pZMnovU1NiOTdDLzBOSXMKLS0tIDhQUU1ReDlpWThPU1V2QSszd3ZT\nVHFGYW9YOTNmTmxOQlU5OGM2clIxdW8KEHDxeC7oWHY04chaVsUzutowlbQAlR14\n/sgSspLISHdXNw/7vksI62GB5OmbCF9/316LY01yClFpKvbGcZe3fA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj -sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzc3h6ZDZuby9hZDhNcVE0\nakRwWjJnVVdWRDFrM29KaWJCTi80dGhrNVU4CmFxeWpiTGY2aFYxaDE3cHNhaU9m\ndWZORmJEVWY0ZGFXZndicENkems1bXMKLS0tIEhsNVhlamNhNTVZSVJrZVhOZ3cx\nalo5K0QvTjdBSXhtK2kvNVI5R2FjY0EKqzHsDR5RplJN5ci0eexxho+QV8Vt+vpY\nO20x9wArHYnV5amSBKeQm7JhHeZulgC9+fiA8o3T7qNCMRSkH6R3nw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTdFMrWVlYWnQyS0FaR21t\na3Nsb1FSVVRxMDN5b0F6TDBoMHNDNW0yc2lFCmpKRE0yWEhPM1BWbXBJT0U1SHNl\nNzJKSnJRREZsajBCVjFreDNWcE5XNkEKLS0tIE9LMUxOdUlNQzdoNnFKRW82ZHA5\nZ3JjdHFaVG94TlRZVUpFK1BTRXVHeXMKbUrmQBwDMgY3c3OlIeN7UHQZHkHTBzBP\n16trH9mVU3vsbYCxanrY+hy6E90LeaU5tnntHXb0HMqGDl6vgCNHYQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1slx6e48k7fre0ddyu7dtm2wwcqaywn5ke2mkngym3rpazxwvvuyq9qjknk -sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1azAwRE9tend1bVdPSTZ4\nNkFkd0N5TURBL2Rma1hCU0ZKRGdJNThZZFZzCmZwQmtuN0ZmSnl2cG40TVBFK0tP\naUZsU0hYWU9NdUMzMi8xZGhwMTFhdXcKLS0tIElPVEc4cW0vL3lMekhrUStLTXZQ\nUG1rbk9XWkNQMVkzUkpqZlpNeUIwS2MKR7Vm+f4uwJkSktJNYsyZF4PZFVN/DYIn\nQDtnazxr8ZFTH1VVNn61rHYsQmlxtHGg9SmPbndzFr2bo4tsXzacYA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5bUFyTGwwL0ZwaVArSnZ1\ndUJkQnV2NVhTQnQyK2JGSUc3aHAyZ3N5UVNVCkF5WnFPU0RxMUFqOUtQeitQbzdD\nNHRNaGZBNUtTMlJFbzcvRGo3YklmTFUKLS0tIFlQZHdTSVd1V0JERDZEcjR3Q0RZ\nZlc4c0VLalFQK2krQjcxT0ZHb21PcEkK0tfMklh/5YsvkJ4P/iIT1TjWEcmtYJob\n7kOqk4B10aA1v4NmeqGBpf+stqXebUblpQmEXq5gmMQFlawFWHFPIQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h -sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByMy9QcUhtZkg3dE1Tc21U\nU1pOMnZKTEloZnRTVFE5bWVTZUtTTkpNaG5FCjNoL2J5VGFIQk5XdkNhRjVTR3Qz\nY3diK1duSm9NbXRjUEovZW1mYUtGbnMKLS0tIFVJM1d2OE9GS2VPNTdwRFRkcWJz\nbk1uZE9ZYkl4ampzLzJ0NjVxTW15UEkKSPbbPfAw4Bv2GjXYCXpQWemuYel3BmFH\nStn+foFMzwqB429gsmvy2U7CoisQLE7manKIlnetjnEOzV6aoclIEg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0Y0RNMGh4eTdNWWNobWlY\ncGsxTDVrZ2xmOGhnRjlob01sQ21mSmJvdWdVCmcwaVJBNnczQ2VXZmM3ZXlGbUVH\ncUtBMnRYZHk3M0xZcVZyQ29jMTVxSTAKLS0tIHdVNGNZWkJxaGZnNDBKMTFFSDJl\nbnVYZ0d1L05qbFhZWXoxazFvQ0tRYkkKA6qOZA6z/d01xmV6zomUA8JUrTUywKro\nUz0bkgsHnvElWv13CBaYEDMHZmkhWq+shnr+tYUXrXx4RLbq3a70EA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx -sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhN1J0cFdxU3F0MmxlYjNm\nRXIwRlVSZU8xQUFWbTVBR2dYZjJNUm9LOFRVClMzdGZXa2VFZGVJU2VFWEREWWMz\nRUpGVzhEa2FDRW9PaG84OCtITVZWaG8KLS0tIDNuaWF3Sy9KRU03R3JMbkV1VkhU\ndWhnNEpFbk5IdDdiQUZTeWFFL1Vjak0KDv+ZaTh5ZB5sBQS+2osR/ZTaHILW43e4\nRgbKXOQRkW8kzeXHaBuYdjGOxf5oIEyfuJSo9RrheNhaWeoogOTt8g==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBScWxBREtCWWZ3TXYrR3Qw\nbFA4Q05VVktiVUZ1RjZqUi9pRllwRDU3Sm1FCmJhbUZQTFZZL2kyN0tNUlQwWVlD\ndDBtekZndlgzUDlxN01MWE13ZVJaTGsKLS0tIFRpRlN4a3lOSWJuU2p3UWRsb2ti\ncnoweWV3ZlBhOEx5cXZzNTBKbDN3eTgKgV/v4wrEnBXdDe0wiT9iX/abq1RxJ6Kv\nh6USU/cUJqElUU5Nu8rYqlVyrJw9zF5hq+L0XomOGWyYdfwWW9qXaQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_5__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -sops_age__list_6__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBITkNsdFBnOTc3UHFnemVm\nSnlYQSttSEtlZWRmOTVwTE1xMGU4aG55TUJjCmdjcCswK2lrME9EcTl1TzNDU3hU\nMHMrU3RYMlBkS29zaXVrRDV3ZU1SUEEKLS0tIG9TajFsclpqZGdtS2tKb1dWMGY1\nd1lrc29QMldBSEpZNnRLRk9tdFVCTUkKrq6IHWu5lV71KHn6Ei+JThztwFrASP9Y\n18UdGQjw+zkjm8GnPQLlIZNedvp9Q7k0TI4Ouxb5OXfstlNjLhDQGg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_6__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzTS9mK0ljd1NRbUJtU2J2\nY1NHbWZzem9NZ3R6VnQvMEJnSm0zdG4xaUVZCi9zL3FNRytMUmd2eVU5ekxYZVpz\ndXVwd3Nrd25kamRMZW1wbHM0RUo2N1EKLS0tIHNSUGVESHBjRmQ0eVY4WndsRWlQ\ncWNqUUF5cUtDUFhTVFBIdTRUNjcxWDAKWtdnGqv+POmO3gyPfn4gxEF6bndbVMPN\nkr2jYlNlk+yLxin5t2eZXnwEdHKD6/JPrzobaV722Y3gcy4SMBrx3g==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_6__map_recipient=age15fsv503cl9ajfg9fphw45sfq0ytlptnr8wtlt48asytz4ev6rfks2pwt5s -sops_lastmodified=2026-04-06T10:19:51Z -sops_mac=ENC[AES256_GCM,data:Ja6VtiRTT9m3EIHmwg8A/YeeIBIJkupopCQFj3ZZELLHlqICpWAwBUnLcDS912e5vC35/ybuPsDVc+uocs091D950R3GvnleIEtEld+x/hX3hQpgiURtzBnvNlaRoXJ/60YiuXwxNWvLy73RVXJpFg2RN3kb7QL3yhE6d4U7Uqg=,iv:2Zf52DAsW7D7z9EGtXrG6oAQrQvk7WuQla7DyvtIm9o=,tag:ZSREwuoFiNBSfKzZVwM2vQ==,type:str] +sops_lastmodified=2026-04-06T10:33:35Z +sops_mac=ENC[AES256_GCM,data:FhzbTx/c9Cikj+q3nFeunA5flcDL/IDuLzbTNqFiMloqUl2nLgfqP/MnPzti3/IS45EgIQlK8Tc3prSN5B6NDKH0UN0M1xvbe9p2eeoDQJk8I/OqpqrhqkfnClbk82DIQvjps4TqhJZidu7LHV0g+lF33Uwl0F3a1rMwkRL6umo=,iv:SkKnb7RqNRQGix2rKrv0oALUDOc0leChmb+abgcB4UY=,tag:HlDtB4atTYGXEt+0OCnSMA==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.12.1 diff --git a/infra/.env.sandbox.enc b/infra/.env.sandbox.enc index e02b30bb84..71249e26d9 100644 --- a/infra/.env.sandbox.enc +++ b/infra/.env.sandbox.enc @@ -1,53 +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 +#ENC[AES256_GCM,data:EUk6PIzXsseMFS4wuOnFQj91hGFwV1hzfi3w2DQtroWV,iv:Gr9+RpcxLEZ0/s37D5tEdejqHCKFe2PBonqh7J4NKUQ=,tag:ELZGUZkCUoirFfUC+SWTmg==,type:comment] +#ENC[AES256_GCM,data:zD2eJw0olC7BZUYmFfI+ui2rJOHotGFvfXsJtVV6Fs9eddGy2puY27q1MqfFJZ1HS8qd1YpWTqprPb0=,iv:WM22N8dLhJSE/zNA+ASjwENeSuZ9Cfx+FuF7T2tM9Yw=,tag:CBzIFOJIyHUJprY0+fAvJw==,type:comment] +#ENC[AES256_GCM,data:GgsiNd0FA23Ml4AOVn0gpzzjrFimTi6QEBIDJoWjR8lFjNSEQIbBUzdwSBUJtRW9/kpE9nciaRioyRhuzbf5riEy4g==,iv:v/rX0FvZBCVQjH6KJvOsRvBOzzapSpJ1S5xtD5BeuJ0=,tag:va2QU1onE+vV7leJjjuajA==,type:comment] +PUBPUB_HOSTNAME=ENC[AES256_GCM,data:0rThpmSzlemAo9bEedewZKEhZw==,iv:mHZIqB+V3LY7qkxq8aSmd+J8OLTpDJQAIK/r7Sr9zJU=,tag:RPtpmXfP0dzNn2h4WDREKw==,type:str] +PUBPUB_URL=ENC[AES256_GCM,data:hZlG9x0gFG0f0FzC+SPCWnV1s/Tulm18OhOY,iv:7UM8dBIV3yaGOakYSkm49dpEEnoWYojpoNppLHfPGFY=,tag:1tu98rRDaRdiD/aHBVThNw==,type:str] +POSTGRES_USER=ENC[AES256_GCM,data:uZ5vn1rC3WA=,iv:TkJZImdU6aafkJ0Ye6cdaT3/Z+w4/7QemlAS6guKCus=,tag:UjqhwiubHrWGKKZqwLvu8A==,type:str] +POSTGRES_PASSWORD=ENC[AES256_GCM,data:hCmhLFmyazk=,iv:7Cyq18Hb4iK5PQfBqb4xwJ8vL2POEJyElYUa5hx93gU=,tag:pH0L4Z1ijcO9kPMQIdKRMw==,type:str] +POSTGRES_DB=ENC[AES256_GCM,data:ISSMJidN,iv:0xkYpUb1dg9Bp13XCdIPIJwelfZGd07hNDY7pIBKJjg=,tag:wtZVSRcSuW/oSGqqHWX+4g==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:3LAclVCQWrG+7CdFXPnj47QVzEvXAyUbQDesSeCl4jZ4TG6dl99x56fJTBTF+jYOszdkjnps37yQ2du6U8zFoVHUdbgh3bEJBw==,iv:sgxvIo8HqTZDrFQGN0TawKvcwnbRCV1mKf6fAwbxepU=,tag:zKeU/TY3o8vo7kWA7x05jQ==,type:str] +PGHOST=ENC[AES256_GCM,data:Wu8=,iv:bf6higOwpwLgZ2gSHU3vtsBWIknD8IqSRE+FJymZNvY=,tag:xWgR/KULnc5q+ht5r/q6sg==,type:str] +PGPORT=ENC[AES256_GCM,data:wUDbkA==,iv:vu5OWULm4wUsdvla8ng9MYYuavj28z31xRMH0FkpB5o=,tag:iie9hQz66wEC1ButsZNC0w==,type:str] +PGUSER=ENC[AES256_GCM,data:w2SIl1KPFkKE8XWEZEPApg==,iv:qcPcosmKNdNBap2ZvXkODc1+AL2LRCpuyoySXvZx97I=,tag:9l9dP2RqRXe323VIuwHbNQ==,type:str] +PGPASSWORD=ENC[AES256_GCM,data:tskXQm/SiPq4v/mtnzfvZZjqhqU=,iv:kMCrdDbO3IIYrr6Fq3wlBDnFAKeFDX0XXA5CMFfpksc=,tag:acZGDyb30y7ZShxmtIOEYw==,type:str] +PGDATABASE=ENC[AES256_GCM,data:k7pH4TISVtrgCQZBNKQ=,iv:FT/tVKCe8Uzj6fKt3tjGEQ5sm/MJZTb2cfCcbhH8yzI=,tag:XERwNJlVJya6OJyGtws6IQ==,type:str] +VALKEY_HOST=ENC[AES256_GCM,data:eOykTCQ=,iv:bEYSu68W3pHnDPJBG4qeBcnr30gWHgKRTS0QiLH6Jjk=,tag:fXp6f3nNtKnXACcRbzax5w==,type:str] +MINIO_ROOT_USER=ENC[AES256_GCM,data:B9teVQllUozOjWAclQ==,iv:WlwU2agbABSFfZ0Ai8MAf3nLmDm2f0ngXjyYV2ctZVg=,tag:pfJesr2Wc+1JkT6cS4Qdig==,type:str] +MINIO_ROOT_PASSWORD=ENC[AES256_GCM,data:m5IhnP4bmLwvhRH1MQ==,iv:YwrFzZy4V9DTgIHB6kjuaHkf9bYR3qlWOg387lioC2E=,tag:j94mHdsseLMCz2qWX1XN0w==,type:str] +ASSETS_BUCKET_NAME=ENC[AES256_GCM,data:J7+yZke6,iv:nmGO4MnTHkF02hT+Swus0AdzMUnLQAqnlVQbmaV8Ibc=,tag:+lygXbeROCrFv7WY0FniOQ==,type:str] +ASSETS_UPLOAD_KEY=ENC[AES256_GCM,data:2MvQ0tTCJKlELzw=,iv:mVOi3FAknw098g2AY+h43mbtZ9YT3PNt6fkOmzBTaKo=,tag:cDIcHbJvB8h6P5ZXULv1pA==,type:str] +ASSETS_UPLOAD_SECRET_KEY=ENC[AES256_GCM,data:fR04nwkgZ8jFC0s=,iv:lJcMu0xS5AsKN9X+LZ70RELGF4u1eI0uKTbpyQcoTQQ=,tag:SSaKB4Sn620gwXm9ilRYkQ==,type:str] +ASSETS_REGION=ENC[AES256_GCM,data:AHtDF/GoJc/C,iv:F7XuISy3MXp77A9xAaMryKs4mQZPrb7Oebl9bBVAfJQ=,tag:kK4IHFGO7tCsV+16Xg4Dng==,type:str] +ASSETS_STORAGE_ENDPOINT=ENC[AES256_GCM,data:NKCglvQc9KIoARg9kNhx8qM=,iv:d4RdlJQURPKGeNDlXDONDkTW0vA1yYCvWQZlRLQh2EI=,tag:AdpBoDVZ2/IznMTlqPIxrw==,type:str] +ASSETS_PUBLIC_ENDPOINT=ENC[AES256_GCM,data:PVQHF4MFGv02V5c5l2X/UYsix+q5E4Phim3j7qhuXkv+Nw==,iv:+wE5+eDgFT8CPqDrzXuR8Jc8sm7Et1KIGXMuMBkXK/U=,tag:hSEpd5211TbOINqpT9lpiw==,type:str] +S3_ENDPOINT=ENC[AES256_GCM,data:GE+jlsFsEhlT/NrhRASHOUo=,iv:UgnJ9aS4Et+otQCIyAQN0A6FasbHLBVxeYuG+/BW7Vc=,tag:KDtGbGOWtME8RYGMYEeY9Q==,type:str] +S3_REGION=ENC[AES256_GCM,data:bfsZhVI6SUQt,iv:eqbKTZ/JEi8CViY4nbOLIp2txcdUxXk+BFfPQt0oYXE=,tag:XpbvEPh1dPTC3dfn7HZxuA==,type:str] +SITE_BUILDER_ENDPOINT=ENC[AES256_GCM,data:WaJmPKLU0xgz2filfafc7YfCXKNpQLrt,iv:qbnYSvd+j/vJge3jdckmjhg6GKc2+QTK6EBAE34HA4g=,tag:tfbitWsNF2k6hSfLoawytQ==,type:str] +SITE_BUILDER_API_KEY=ENC[AES256_GCM,data:nUfu,iv:YkcgqQqaw3EhllxJrODxKcxK6XLRamKl6R409oP8wLs=,tag:kAei09a2qd9iAKi9AXjbXQ==,type:str] +MAILGUN_SMTP_HOST=ENC[AES256_GCM,data:jTdA,iv:ulzrjrcYakwL0CVZLmyhXkHXpq+xqXbQiQAKQL11P6s=,tag:ObCFVmmbd3VN620xGQKvxw==,type:str] +MAILGUN_SMTP_PORT=ENC[AES256_GCM,data:+PaC,iv:4yAKdeIlgnF6xHNOT0Z6CR7Nu3rG6waJCvkgfhNOFL8=,tag:jEiAaXp0kUn3s+cP8bjKyg==,type:str] +MAILGUN_SMTP_PASSWORD=ENC[AES256_GCM,data:Fkgy,iv:tDcWCgTpMQvAhMXzu/HokUCYNlzfiEc+FjGcI3VkaZQ=,tag:/a9h4511XDbmk16s+DmcCA==,type:str] +MAILGUN_SMTP_USERNAME=ENC[AES256_GCM,data:OKMx,iv:38RLXgWXNzA/XgJ22Qil4ZBo2Obo2uKHA3SYtWfOCC8=,tag:2LhnzeXIkabpXBwWXNTe9w==,type:str] +GCLOUD_KEY_FILE=ENC[AES256_GCM,data:eXcO,iv:jOd0V1n+g/CV04x92oLLD7BHj0FOaTKjTqeOv4GdT+M=,tag:M1pg4Sf/VhDIw/8dQRWtPA==,type:str] +OTEL_SERVICE_NAME=ENC[AES256_GCM,data:/osaHe00IZD9Uz/gDyca4Sg=,iv:cGZ4j0VC4v5Th8Qn9r4hT3vH6Ne6SE4EA/yLarIWHNI=,tag:an3A9I/ze6AYxwLVEJiMCA==,type:str] +HONEYCOMB_API_KEY=ENC[AES256_GCM,data:47eY,iv:tMJArSXqZiDqWiotXFmFyW0WqR+3kvkxuCk2lp2IhyU=,tag:3qWyEBnHXFlm8qfniQJ93g==,type:str] +API_KEY=ENC[AES256_GCM,data:r9MF,iv:7JBUNs84+/+l9vWP/eMY51YcvxeiCAWYfBRfKZQUSpc=,tag:i2eKY5CYm2dDJmZFV5S9Ew==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXUmtQMy9ZREpPa0NLRGsv\naU1VMW83cUJlWjFTMjJreW42UnltWUI0L2h3CmJxdzRXa2FPaERHR0xyUHY5SDFv\nb282VktXSWlrdk1NN1hSbzgzSDNhYUEKLS0tIHg0MHR5WHhJcDFna0JDbUR3c0I5\ncVJqaGw1UE9VclhOSDNaeUR0UzZVeEkKmrjBiCf23E1uxNtCXr8zpVvP2gMAdUFC\nV7fk+zpa3443cc/2AvSAiWxyXHblykN+cxc+suNUDNv1QtQlqG7RIg==\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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnME9uSXF3TU90VFFEb0Vr\nTHlFT1JCZGxUMVBLNDJ0bkdMTmU0YktNY0NRCjIvakQrRjc4OVB3WDdwWEt0Sjd0\nWUxvT001SnVsZGhrWjV1MzJjOGNnS3MKLS0tIC8xK01yYnRiMzIyTDd1dVpGaUNh\nU1lmZnREUktRRkpQOHlpcVBOQlVlVVEKQW699/iBFbah1OOVbcClLoyWadsDGYaf\nMz+5EvDFwO7LPxld3uTkXNh0tNuXmi6T85c09FM7BPqarNMsBbqVJA==\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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByY3F3ZFU5KzBVYklIbmR1\nQjloZFFnZEMvekxrTUpOb3FMUnpFekZXODNZCmhRNjZiNGZjVHVlTTFPSDJnNGFG\nS3NyWFF0UFBxL0ZGYUhxeGpGVldBVEEKLS0tIGVwclczRERPTENWMGl2VmtTaTBQ\nTWVIanRTMURaWmFJSWZwTkZSRUlRbEkKqMEEWUGjLYbDwd3q8isW/mg5c+MNPcod\naSh7XqVcOyDBUBr1yuQ1DY2itMY+x502HNzUl84i67mep0k619jgTA==\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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtU05jcmxMeUJWZnpISkd6\nTFZFNkxPMjBXOVNSSmxwWUNZczdtZXYxWTFVCitCclkyV2hsUnJzcGhvbXluc3N2\nOGVsUldjaEpxMVFMRVZzNm9mV0N6SjQKLS0tIHBEOFhlbDNFTThOZW1HZU0veldX\nMmZiNkZBZzA4QVlyRTJRTzBSVXJPSUkKEGiTA2DpTTOUI/dxrQSYoQhiS6DM4Q/t\nOMAA3OU05t2n0SeWUoJ5yFJ2PcFWTkXnUG173e/ip5zpdL/el7dDRw==\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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1NzZpYk9RMithS0xieWZ1\nVGo0VURsOWQ3VTJjK1V3amtVR3dCQ2twZjF3Cm5IbkliWmp5dXpFSUl1YURJelZD\nb0ZDaExNc3NNQUFpK2p0UUZCdHhMNjgKLS0tIHRRck13c2crMXF1TnFha2NpMHBD\nVlovMGxyMmx4TWVYdUF3NHhQRTVNa3cKE/patQW9NnZ6LnbGT21gs6F9ggCCbveI\nff0kfaLVFaG073PGSRlS/6pI6kY10E8VWWUTjsA1tBZM3vLsZ0fq7A==\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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYajZNZkxacjY5L2Q4KzBu\nS25iZ082a1FSSVpwODR5S2I1U09CTE5wWEVFCjAxOEZ4STAzcDZILzBxeWVYK090\nNTBQUlpSMmlhSDhRNkUxaWFLb1p3SlkKLS0tIE1MYXVuaEhDd1ROdGdlbUZKckZv\nRWFLRWNGUWZ2TVg1Qi9hZCttSlc4TlUKR5vNkVuEqf4sBxCPFdse4XInlB8xqA0z\n3szMwE8r8fIzKg8j8chh3yb3H2aHel2WJanF7+omB8H19Fvq1+3NcQ==\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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSWTRCcGowV1c2blhrZjRP\nczBBaldMemM0SGI4dzRiVWdMbEkxM0VXRlhzCkNpaVFqSnZJbU5RWkIweGo0WU9L\neW5WTzN1T1djR1Z5WlVDdmZteVJ0SmcKLS0tIHZ4WUxhdzJXQzFYTG5ET1dCUi9m\nOW5IbWpESUd6K1ZZNHh1M20rNWxTTEUKTjqTQD+SCcfLdj8J9PFMgNfUkqScy3DN\ncSYOwbpH2ObVhvvmgLqjR+RQdLkpb2itf4cufKspSi5d6uA2AQfwtw==\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_lastmodified=2026-04-06T10:33:40Z +sops_mac=ENC[AES256_GCM,data:oZ7HHxAKttSyIEhkQ1pWPUwKD95EXNYUXKaO90T1ye4AWRpFPcBIfOkCk0eyOJyD1iPpj7MgzTcNcyCC14Q4G2ZnNQE/Gy4ma4UQSCcGqG85AqYWJ81Blj4thzWxZLpE1ZDqGNJilI0zQeMu2fH39rrFrBqt5b81X6TKYMCjvEc=,iv:sBkqMnLIEHmg8Bxv/wO109qKItTO+zFfGvHAhwwLoqI=,tag:5t82yBl7V6ITFVoh3imWoA==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.12.1 diff --git a/infra/Caddyfile b/infra/Caddyfile index d1af2b1081..d65c80d7de 100644 --- a/infra/Caddyfile +++ b/infra/Caddyfile @@ -21,11 +21,11 @@ root * /sites file_server { fs s3 { - bucket {$ASSETS_BUCKET_NAME:assets} - region {$ASSETS_REGION:us-east-1} + bucket {$S3_BUCKET_NAME:assets} + region {$S3_REGION:us-east-1} endpoint {$S3_ENDPOINT:http://minio:9000} - access_key {$ASSETS_UPLOAD_KEY} - secret_key {$ASSETS_UPLOAD_SECRET_KEY} + access_key {$S3_ACCESS_KEY} + secret_key {$S3_SECRET_KEY} } } } diff --git a/infra/Caddyfile.preview b/infra/Caddyfile.preview index 2d7f8975be..f4c286f7e0 100644 --- a/infra/Caddyfile.preview +++ b/infra/Caddyfile.preview @@ -30,11 +30,11 @@ root * /sites file_server { fs s3 { - bucket {$ASSETS_BUCKET_NAME:assets} - region {$ASSETS_REGION:us-east-1} + bucket {$S3_BUCKET_NAME:assets} + region {$S3_REGION:us-east-1} endpoint {$S3_ENDPOINT:http://minio:9000} - access_key {$ASSETS_UPLOAD_KEY} - secret_key {$ASSETS_UPLOAD_SECRET_KEY} + access_key {$S3_ACCESS_KEY} + secret_key {$S3_SECRET_KEY} } } } diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index f1ae87cfb7..98479bfa56 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -34,7 +34,7 @@ services: PUBPUB_URL: https://preview-${PR_NUMBER}.pubstar.org PUBPUB_HOSTNAME: preview-${PR_NUMBER}.pubstar.org SITE_BUILDER_ENDPOINT: http://site-builder:4000 - ASSETS_PUBLIC_ENDPOINT: https://preview-${PR_NUMBER}.pubstar.org/assets + S3_PUBLIC_ENDPOINT: https://preview-${PR_NUMBER}.pubstar.org/assets FLAGS: "uploads:off,invites:off,disabled-actions:http+email" networks: [appnet] healthcheck: @@ -125,10 +125,10 @@ services: 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}";' + /usr/bin/mc mb --ignore-existing myminio/"$${S3_BUCKET_NAME}"; + /usr/bin/mc anonymous set download myminio/"$${S3_BUCKET_NAME}"; + /usr/bin/mc admin user add myminio "$${S3_ACCESS_KEY}" "$${S3_SECRET_KEY}"; + /usr/bin/mc admin policy attach myminio readwrite --user "$${S3_ACCESS_KEY}";' networks: [appnet] deploy: mode: replicated-job diff --git a/packages/context-editor/src/stories/mockUtils.ts b/packages/context-editor/src/stories/mockUtils.ts index c4415b9f73..c966cebd02 100644 --- a/packages/context-editor/src/stories/mockUtils.ts +++ b/packages/context-editor/src/stories/mockUtils.ts @@ -20,18 +20,18 @@ export const getPubs = async (filter: string) => { * Requires minio server to be running * */ const getS3Client = () => { - const region = import.meta.env.STORYBOOK_ASSETS_REGION - const key = import.meta.env.STORYBOOK_ASSETS_UPLOAD_KEY - const secret = import.meta.env.STORYBOOK_ASSETS_UPLOAD_SECRET_KEY + const region = import.meta.env.STORYBOOK_S3_REGION + const key = import.meta.env.STORYBOOK_S3_ACCESS_KEY + const secret = import.meta.env.STORYBOOK_S3_SECRET_KEY const s3Client = new S3Client({ - endpoint: import.meta.env.STORYBOOK_ASSETS_STORAGE_ENDPOINT, + endpoint: import.meta.env.STORYBOOK_S3_ENDPOINT, region: region, credentials: { accessKeyId: key, secretAccessKey: secret, }, - forcePathStyle: import.meta.env.STORYBOOK_ASSETS_STORAGE_ENDPOINT, // Required for MinIO + forcePathStyle: import.meta.env.STORYBOOK_S3_ENDPOINT, // Required for MinIO }) return s3Client @@ -40,7 +40,7 @@ const getS3Client = () => { export const generateSignedAssetUploadUrl = async (key: string) => { const client = getS3Client() - const bucket = import.meta.env.STORYBOOK_ASSETS_BUCKET_NAME + const bucket = import.meta.env.STORYBOOK_S3_BUCKET_NAME const command = new PutObjectCommand({ Bucket: bucket, Key: key, diff --git a/self-host/.env.example b/self-host/.env.example index e8faa80fd0..fc212917c0 100644 --- a/self-host/.env.example +++ b/self-host/.env.example @@ -17,15 +17,15 @@ POSTGRES_PORT=5432 # don't forget to update the port in docker-compose.yml if yo MINIO_ROOT_USER= # change this! this is the username for your file server! MINIO_ROOT_PASSWORD= # change this! this is the password for your file server! -ASSETS_BUCKET_NAME=assets -ASSETS_UPLOAD_KEY= # change this! example: asset-user -ASSETS_UPLOAD_SECRET_KEY= # change this! -ASSETS_REGION=us-east-1 # leave this unchanged, unless you are hosting files on a different region on actual AWS +S3_BUCKET_NAME=assets +S3_ACCESS_KEY= # change this! example: asset-user +S3_SECRET_KEY= # change this! +S3_REGION=us-east-1 # leave this unchanged, unless you are hosting files on a different region on actual AWS # this is the default value but you ideally should set this up more nicely using our caddy service -ASSETS_STORAGE_ENDPOINT="http://localhost:9000" +S3_ENDPOINT="http://localhost:9000" # you could also set this to the secured endpoint of your file server -# ASSETS_STORAGE_ENDPOINT="https://example.com/assets" +# S3_ENDPOINT="https://example.com/assets" MAILGUN_SMTP_HOST=localhost MAILGUN_SMTP_PORT=54325 diff --git a/self-host/README.md b/self-host/README.md index 60537c378a..25e1fa9871 100644 --- a/self-host/README.md +++ b/self-host/README.md @@ -92,11 +92,11 @@ The hosted version of Platfrom uses AWS S3 to host files. When self-hosting, you If you want to use your own S3-compatible storage service, you will need to set the following environment variables: ```sh -ASSETS_BUCKET_NAME="your-bucket-name" -ASSETS_UPLOAD_KEY="your-access-key" -ASSETS_UPLOAD_SECRET_KEY="your-secret-key" -ASSETS_REGION="your-region" -ASSETS_STORAGE_ENDPOINT="your-storage-endpoint" # only necessary if you are using non-AWS S3-compatible storage service +S3_BUCKET_NAME="your-bucket-name" +S3_ACCESS_KEY="your-access-key" +S3_SECRET_KEY="your-secret-key" +S3_REGION="your-region" +S3_ENDPOINT="your-storage-endpoint" # only necessary if you are using non-AWS S3-compatible storage service ``` You should also remove the `minio` and `minio-init` services from the `docker-compose.yml` file. @@ -123,7 +123,7 @@ openssl rand -base64 32 [System.Web.Security.Membership]::GeneratePassword(32,8) ``` -Run one of these commands twice, and use one for `MINIO_ROOT_PASSWORD` and one for `ASSETS_UPLOAD_SECRET_KEY`. +Run one of these commands twice, and use one for `MINIO_ROOT_PASSWORD` and one for `S3_SECRET_KEY`. ```sh # not needed if you're using a remote file server like AWS S3 @@ -131,10 +131,10 @@ MINIO_ROOT_USER= # change this! this is the username for your file server! MINIO_ROOT_PASSWORD= # change this! this is the password for your file server! # these are either the values of an existing S3-compatible storage service, or the values that will be used to create a new MinIO service -ASSETS_BUCKET_NAME= # example: assets -ASSETS_UPLOAD_KEY= # example: asset-user -ASSETS_UPLOAD_SECRET_KEY= # example: a strong secure password -ASSETS_REGION=us-east-1 # leave this unchanged, unless you are hosting files on a different region on actual AWS +S3_BUCKET_NAME= # example: assets +S3_ACCESS_KEY= # example: asset-user +S3_SECRET_KEY= # example: a strong secure password +S3_REGION=us-east-1 # leave this unchanged, unless you are hosting files on a different region on actual AWS ``` Then, after running `docker compose up -d`, you should be able to visit the MinIO console at `http://localhost:9001`. diff --git a/self-host/caddy/Caddyfile b/self-host/caddy/Caddyfile index f1389fe552..d381c02f35 100644 --- a/self-host/caddy/Caddyfile +++ b/self-host/caddy/Caddyfile @@ -23,11 +23,11 @@ example.com { root * /sites file_server { fs s3 { - bucket {$ASSETS_BUCKET_NAME:assets} + bucket {$S3_BUCKET_NAME:assets} region {$S3_REGION:us-east-1} endpoint {$S3_ENDPOINT:http://minio:9000} - access_key {$ASSETS_UPLOAD_KEY} - secret_key {$ASSETS_UPLOAD_SECRET_KEY} + access_key {$S3_ACCESS_KEY} + secret_key {$S3_SECRET_KEY} } } } @@ -40,7 +40,7 @@ example.com { # if you want to use a different domain for your files, you can do so here # for instance, now all your files will be accessible at assets.example.com -# if you go this route, be sure to update your ASSETS_STORAGE_ENDPOINT in .env and restart your services +# if you go this route, be sure to update your S3_ENDPOINT in .env and restart your services # assets.example.com { # reverse_proxy minio:9000 # } diff --git a/self-host/minio-init.sh b/self-host/minio-init.sh index 42f08174ce..cb458681c7 100644 --- a/self-host/minio-init.sh +++ b/self-host/minio-init.sh @@ -1,5 +1,5 @@ /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}"; \ No newline at end of file +/usr/bin/mc mb --ignore-existing myminio/"${S3_BUCKET_NAME}"; +/usr/bin/mc anonymous set download myminio/"${S3_BUCKET_NAME}"; +/usr/bin/mc admin user add myminio "${S3_ACCESS_KEY}" "${S3_SECRET_KEY}"; +/usr/bin/mc admin policy attach myminio readwrite --user "${S3_ACCESS_KEY}"; \ No newline at end of file From cbc8e65b326aacaf3e4ac8d15a35358b1fc71930 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 12:35:00 +0200 Subject: [PATCH 22/78] fix: renable full pr ci to rebuild stuff --- .github/workflows/on_pr.yml | 198 ++++++++++++++++++------------------ 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 8f473338ab..78e5af4ec9 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -10,101 +10,101 @@ 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') @@ -116,8 +116,8 @@ jobs: pull-requests: write with: action: deploy - # image_tag: ${{ github.event.pull_request.head.sha }} - image_tag: 307af6bfa9b083598febb0d4af841a9c66b892f7 + image_tag: ${{ github.event.pull_request.head.sha }} + # image_tag: 307af6bfa9b083598febb0d4af841a9c66b892f7 secrets: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_USER: ${{ secrets.SSH_USER }} @@ -170,12 +170,12 @@ jobs: # 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 6da045df9f501c71de39d42d4905579847df3530 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 12:47:51 +0200 Subject: [PATCH 23/78] fix: go back to quick preview again --- .github/workflows/on_pr.yml | 198 ++++++++++++++++++------------------ 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 78e5af4ec9..7bc8b9ec6e 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -10,101 +10,101 @@ 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') @@ -116,8 +116,8 @@ jobs: pull-requests: write with: action: deploy - image_tag: ${{ github.event.pull_request.head.sha }} - # image_tag: 307af6bfa9b083598febb0d4af841a9c66b892f7 + # image_tag: ${{ github.event.pull_request.head.sha }} + image_tag: cbc8e65b326aacaf3e4ac8d15a35358b1fc71930 secrets: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_USER: ${{ secrets.SSH_USER }} @@ -170,12 +170,12 @@ jobs: # 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 fbb267697c57f47cbc47b5b38aa288b6057704c2 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 12:56:35 +0200 Subject: [PATCH 24/78] fix: rm on demand tls --- infra/Caddyfile.preview | 8 -------- 1 file changed, 8 deletions(-) diff --git a/infra/Caddyfile.preview b/infra/Caddyfile.preview index f4c286f7e0..80d43b29ff 100644 --- a/infra/Caddyfile.preview +++ b/infra/Caddyfile.preview @@ -1,17 +1,9 @@ { admin :2019 - on_demand_tls { - interval 2m - burst 5 - } } :443 { - tls { - on_demand - } - encode gzip handle_path /assets* { From d238cad4e5774755750f9513757eeb7829992126 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 13:02:11 +0200 Subject: [PATCH 25/78] fix: rm s3 thing --- infra/Caddyfile.preview | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/infra/Caddyfile.preview b/infra/Caddyfile.preview index 80d43b29ff..b54ba29d2d 100644 --- a/infra/Caddyfile.preview +++ b/infra/Caddyfile.preview @@ -3,6 +3,33 @@ } + +(s3site) { + # strip trailing slash from paths (except root) + @pathWithSlash path_regexp dir (.+)/$ + handle @pathWithSlash { + redir {re.dir.1} permanent + } + + # rewrite to include bucket path + rewrite * /{args[0]}{uri} + + # reverse proxy to garage S3 API + reverse_proxy {args[1]} { + # handle 403/404 by trying index.html + @error status 403 404 + handle_response @error { + rewrite * {uri}/index.html + reverse_proxy {args[1]} { + @nestedError status 404 + handle_response @nestedError { + respond "Not found" 404 + } + } + } + } +} + :443 { encode gzip @@ -19,16 +46,7 @@ } handle_path /sites/* { - root * /sites - file_server { - fs s3 { - bucket {$S3_BUCKET_NAME:assets} - region {$S3_REGION:us-east-1} - endpoint {$S3_ENDPOINT:http://minio:9000} - access_key {$S3_ACCESS_KEY} - secret_key {$S3_SECRET_KEY} - } - } + import s3site {$S3_BUCKET_NAME:assets} {$S3_ENDPOINT:http://minio:9000} } handle_path /emails/* { From eb4c075168191ca76f4fa39df73dfc7fa889698d Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 13:12:26 +0200 Subject: [PATCH 26/78] fix: tls internal? --- infra/Caddyfile.preview | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infra/Caddyfile.preview b/infra/Caddyfile.preview index b54ba29d2d..36ced6a23c 100644 --- a/infra/Caddyfile.preview +++ b/infra/Caddyfile.preview @@ -1,5 +1,6 @@ { admin :2019 + debug } @@ -31,6 +32,10 @@ } :443 { + tls internal { + on_demand + } + encode gzip handle_path /assets* { From fdb42387d108643b35f7f2f319548f75231febf0 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 13:16:08 +0200 Subject: [PATCH 27/78] fix: slightly better on_demand --- infra/Caddyfile.preview | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/infra/Caddyfile.preview b/infra/Caddyfile.preview index 36ced6a23c..4e1210a9f4 100644 --- a/infra/Caddyfile.preview +++ b/infra/Caddyfile.preview @@ -1,9 +1,22 @@ { admin :2019 debug - + on_demand_tls { + ask http://localhost:8888/allow-domain + } } +# this only allows preview-*.pubstar.org domains to be used for TLS certificates +# prevents abuse +:8080 { + @allow expression {path} == "/allow-domain" && {query.domain}.matches(`^preview-[0-9]+\.pubstar\.org$`) + handle @allow { + respond "OK" 200 + } + + respond "Not allowed" 403 +} + (s3site) { # strip trailing slash from paths (except root) From 8853e477647af7487c48caf2848dfe513f5f5914 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 13:27:20 +0200 Subject: [PATCH 28/78] fix: match ask --- infra/Caddyfile.preview | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/Caddyfile.preview b/infra/Caddyfile.preview index 4e1210a9f4..d817ec3dc5 100644 --- a/infra/Caddyfile.preview +++ b/infra/Caddyfile.preview @@ -8,7 +8,7 @@ # this only allows preview-*.pubstar.org domains to be used for TLS certificates # prevents abuse -:8080 { +:8888 { @allow expression {path} == "/allow-domain" && {query.domain}.matches(`^preview-[0-9]+\.pubstar\.org$`) handle @allow { respond "OK" 200 From 2c234f5bd258a56aa5b876de924a3496278b58f7 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 13:33:35 +0200 Subject: [PATCH 29/78] fix: correct ask jesus --- infra/Caddyfile.preview | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/Caddyfile.preview b/infra/Caddyfile.preview index d817ec3dc5..f9e4bbd392 100644 --- a/infra/Caddyfile.preview +++ b/infra/Caddyfile.preview @@ -9,7 +9,7 @@ # this only allows preview-*.pubstar.org domains to be used for TLS certificates # prevents abuse :8888 { - @allow expression {path} == "/allow-domain" && {query.domain}.matches(`^preview-[0-9]+\.pubstar\.org$`) + @allow expression {path} == "/allow-domain" && {query.domain}.matches(`^pr-[0-9]+\.pubstar\.org$`) handle @allow { respond "OK" 200 } From 344c7f02f87e8decc96c6d1ef9ae12e42f59c2e0 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 14:13:58 +0200 Subject: [PATCH 30/78] fix: correct matches query --- infra/Caddyfile.preview | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/Caddyfile.preview b/infra/Caddyfile.preview index f9e4bbd392..722a4fe3a7 100644 --- a/infra/Caddyfile.preview +++ b/infra/Caddyfile.preview @@ -9,7 +9,7 @@ # this only allows preview-*.pubstar.org domains to be used for TLS certificates # prevents abuse :8888 { - @allow expression {path} == "/allow-domain" && {query.domain}.matches(`^pr-[0-9]+\.pubstar\.org$`) + @allow expression {path} == "/allow-domain" && {query.domain}.matches("pr-[0-9]+.pubstar.org") handle @allow { respond "OK" 200 } From 646c8f52204d113e8491c1ac148cf2463e847148 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 14:29:27 +0200 Subject: [PATCH 31/78] fix: consistent naming, wait for platform --- .github/workflows/preview.yml | 18 ++++++++++-------- infra/stack.preview.yml | 6 +++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 34e5e08a9a..9145fb3034 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -112,17 +112,19 @@ jobs: sudo docker pull ghcr.io/knowledgefutures/platform:"$IMAGE_TAG" # deploy/update stack - sudo env IMAGE_TAG="$IMAGE_TAG" PREVIEW_HOST="$PREVIEW_HOST" \ + sudo env IMAGE_TAG="$IMAGE_TAG" PREVIEW_HOST="$PREVIEW_HOST" PR_NUMBER="$PR_NUMBER" \ docker stack deploy -c stack.preview.yml \ --with-registry-auth --resolve-image always --prune "$STACK_NAME" + # caddy's config is a bind mount, so swarm won't restart + # the proxy when only the Caddyfile changes on disk + sudo docker service update --force "${STACK_NAME}_proxy" + sudo docker stack services "$STACK_NAME" sudo docker image prune -f - - # wait until rollout is complete and then clear cache wait_rollout() { - echo "Beginning wait for rollout..." + echo "Beginning wait for rollout of $1..." svc="$1" timeout="${2:-600}" end=$((SECONDS+timeout)) @@ -132,20 +134,20 @@ jobs: 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 + wait_rollout "${STACK_NAME}_platform" 600 EOS diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index 98479bfa56..98193a9ba5 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -31,10 +31,10 @@ services: HOSTNAME: "0.0.0.0" NODE_ENV: production PORT: "3000" - PUBPUB_URL: https://preview-${PR_NUMBER}.pubstar.org - PUBPUB_HOSTNAME: preview-${PR_NUMBER}.pubstar.org + PUBPUB_URL: https://pr-${PR_NUMBER}.pubstar.org + PUBPUB_HOSTNAME: pr-${PR_NUMBER}.pubstar.org SITE_BUILDER_ENDPOINT: http://site-builder:4000 - S3_PUBLIC_ENDPOINT: https://preview-${PR_NUMBER}.pubstar.org/assets + S3_PUBLIC_ENDPOINT: https://pr-${PR_NUMBER}.pubstar.org/assets FLAGS: "uploads:off,invites:off,disabled-actions:http+email" networks: [appnet] healthcheck: From 8acc4384f7c8c0a6633452690aa546cedf6ea0d3 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 15:22:42 +0200 Subject: [PATCH 32/78] fix: properly indent --- .github/workflows/preview.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 9145fb3034..bec0c0ddb3 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -149,7 +149,7 @@ jobs: wait_rollout "${STACK_NAME}_platform" 600 - EOS + EOS - name: Teardown preview stack if: inputs.action == 'teardown' From 4116a6499724b10012c7d5da0cc68da7f921682e Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 15:24:44 +0200 Subject: [PATCH 33/78] feat: run migrations during instrumentation rather than a separate service --- .github/workflows/deploy.yml | 19 ---- .github/workflows/e2e.yml | 1 - .github/workflows/ghcr-build-all.yml | 11 -- Dockerfile | 2 + core/instrumentation.ts | 7 ++ core/lib/server/migrate.ts | 158 +++++++++++++++++++++++++++ infra/stack.preview.yml | 12 -- infra/stack.yml | 12 -- self-host/docker-compose.yml | 21 +--- 9 files changed, 170 insertions(+), 73 deletions(-) create mode 100644 core/lib/server/migrate.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9649598583..8e9d34f39c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -99,25 +99,6 @@ jobs: ${{ steps.vars.outputs.publish_latest == 'true' && 'ghcr.io/knowledgefutures/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 - secrets: | - SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - target: monorepo - tags: | - 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 uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b60a889c71..d147f1eae0 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -87,7 +87,6 @@ jobs: run: | 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 diff --git a/.github/workflows/ghcr-build-all.yml b/.github/workflows/ghcr-build-all.yml index 70c46bdf6f..00081a272e 100644 --- a/.github/workflows/ghcr-build-all.yml +++ b/.github/workflows/ghcr-build-all.yml @@ -10,9 +10,6 @@ on: 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 }} @@ -24,14 +21,6 @@ on: required: true jobs: - build-base: - uses: ./.github/workflows/ghcr-build-template.yml - with: - ghcr_image_name: platform-migrations - publish_latest: ${{ inputs.publish_latest }} - secrets: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - build-core: uses: ./.github/workflows/ghcr-build-template.yml with: diff --git a/Dockerfile b/Dockerfile index 9b052300e4..72c772c19b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -136,5 +136,7 @@ COPY --from=withpackage --chown=node:node /usr/src/app/core/.next/static ./core/ COPY --from=withpackage --chown=node:node /usr/src/app/core/public ./core/public # needed to set the database url correctly based on PGHOST variables COPY --from=withpackage --chown=node:node /usr/src/app/core/.env.docker ./core/.env +# migration sql files, applied automatically during startup instrumentation +COPY --from=withpackage --chown=node:node /usr/src/app/core/prisma/migrations ./core/prisma/migrations CMD ["node", "core/server.js"] diff --git a/core/instrumentation.ts b/core/instrumentation.ts index efaa830c1e..58d6991428 100644 --- a/core/instrumentation.ts +++ b/core/instrumentation.ts @@ -16,6 +16,7 @@ export async function register() { } logger.info(`Registering instrumentation hook for ${process.env.NEXT_RUNTIME}`) + if (process.env.NEXT_RUNTIME === "nodejs") { if (process.env.NODE_ENV === "development") { logger.info( @@ -23,6 +24,12 @@ export async function register() { ) return } + + if (!process.env.SKIP_MIGRATIONS) { + const { runMigrations } = await import("./lib/server/migrate") + await runMigrations() + } + await import("./instrumentation.node.mts") } else { logger.info("NEXT_RUNTIME is not `nodejs`; skipping OTEL registration.") diff --git a/core/lib/server/migrate.ts b/core/lib/server/migrate.ts new file mode 100644 index 0000000000..85b9766230 --- /dev/null +++ b/core/lib/server/migrate.ts @@ -0,0 +1,158 @@ +import { createHash, randomUUID } from "node:crypto" +import { existsSync, readdirSync, readFileSync } from "node:fs" +import { join, resolve } from "node:path" + +import pg from "pg" +import { logger } from "logger" + +// arbitrary but stable id used to prevent concurrent migration runs across replicas +const ADVISORY_LOCK_ID = 72_398_241 + +const CREATE_MIGRATIONS_TABLE = ` +CREATE TABLE IF NOT EXISTS "_prisma_migrations" ( + "id" VARCHAR(36) NOT NULL, + "checksum" VARCHAR(64) NOT NULL, + "finished_at" TIMESTAMPTZ, + "migration_name" VARCHAR(255) NOT NULL, + "logs" TEXT, + "rolled_back_at" TIMESTAMPTZ, + "started_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + "applied_steps_count" INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +)` + +async function connectWithRetry( + connectionString: string, + maxAttempts = 30, + intervalMs = 2000 +): Promise { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const client = new pg.Client({ connectionString }) + + try { + await client.connect() + return client + } catch (err) { + client.end().catch(() => {}) + + if (attempt === maxAttempts) { + throw new Error( + `could not connect to database after ${maxAttempts} attempts: ${err}` + ) + } + + logger.info( + `database not ready, retrying in ${intervalMs}ms (attempt ${attempt}/${maxAttempts})...` + ) + await new Promise((r) => setTimeout(r, intervalMs)) + } + } + + throw new Error("unreachable") +} + +function sha256(content: string): string { + return createHash("sha256").update(content).digest("hex") +} + +export async function runMigrations() { + const connectionString = process.env.DATABASE_URL + if (!connectionString) { + throw new Error("DATABASE_URL is required to run migrations") + } + + // in next.js standalone mode, server.js does process.chdir(__dirname) + // which sets cwd to the app directory (e.g. /usr/src/app/core) + const migrationsDir = process.env.MIGRATIONS_DIR + ? resolve(process.env.MIGRATIONS_DIR) + : resolve(process.cwd(), "prisma", "migrations") + + if (!existsSync(migrationsDir)) { + logger.warn(`migrations directory not found at ${migrationsDir}, skipping`) + return + } + + logger.info(`running migrations from ${migrationsDir}`) + const client = await connectWithRetry(connectionString) + + try { + await client.query("SELECT pg_advisory_lock($1)", [ADVISORY_LOCK_ID]) + + await client.query(CREATE_MIGRATIONS_TABLE) + + const { rows: failed } = await client.query<{ migration_name: string }>( + `SELECT "migration_name" FROM "_prisma_migrations" + WHERE "finished_at" IS NULL AND "rolled_back_at" IS NULL` + ) + + if (failed.length > 0) { + const names = failed.map((r) => r.migration_name).join(", ") + throw new Error( + `found migrations in a failed state that need manual resolution: ${names}. ` + + `mark them as rolled back or delete their rows from _prisma_migrations to proceed.` + ) + } + + const { rows: applied } = await client.query<{ migration_name: string }>( + `SELECT "migration_name" FROM "_prisma_migrations" + WHERE "finished_at" IS NOT NULL AND "rolled_back_at" IS NULL` + ) + const appliedNames = new Set(applied.map((r) => r.migration_name)) + + const dirs = readdirSync(migrationsDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + .sort() + + let count = 0 + + for (const dir of dirs) { + if (appliedNames.has(dir)) { + continue + } + + const sqlPath = join(migrationsDir, dir, "migration.sql") + if (!existsSync(sqlPath)) { + continue + } + + const sql = readFileSync(sqlPath, "utf-8") + const checksum = sha256(sql) + const id = randomUUID() + + logger.info(`applying migration: ${dir}`) + + await client.query( + `INSERT INTO "_prisma_migrations" ("id", "checksum", "migration_name", "started_at", "applied_steps_count") + VALUES ($1, $2, $3, now(), 0)`, + [id, checksum, dir] + ) + + try { + await client.query(sql) + } catch (err) { + await client.query( + `UPDATE "_prisma_migrations" SET "logs" = $1 WHERE "id" = $2`, + [String(err), id] + ) + throw err + } + + await client.query( + `UPDATE "_prisma_migrations" SET "finished_at" = now(), "applied_steps_count" = 1 WHERE "id" = $1`, + [id] + ) + + count++ + } + + if (count > 0) { + logger.info(`applied ${count} migration(s)`) + } else { + logger.info("database is up to date, no pending migrations") + } + } finally { + await client.query("SELECT pg_advisory_unlock($1)", [ADVISORY_LOCK_ID]).catch(() => {}) + await client.end() + } +} diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index 98193a9ba5..f8c9f4395b 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -65,18 +65,6 @@ services: restart_policy: condition: on-failure - platform-migrations: - image: ghcr.io/knowledgefutures/platform-migrations:${IMAGE_TAG} - env_file: [.env] - command: ["pnpm", "--filter", "core", "reset"] - networks: [appnet] - deploy: - mode: replicated-job - replicas: 1 - restart_policy: - condition: on-failure - max_attempts: 3 - site-builder: image: ghcr.io/knowledgefutures/platform-site-builder:${IMAGE_TAG} env_file: [.env] diff --git a/infra/stack.yml b/infra/stack.yml index a74b037ee0..faf65a671a 100644 --- a/infra/stack.yml +++ b/infra/stack.yml @@ -72,18 +72,6 @@ services: restart_policy: condition: on-failure - platform-migrations: - image: ghcr.io/knowledgefutures/platform-migrations:${IMAGE_TAG} - env_file: [.env] - command: ['pnpm', '--filter', 'core', 'migrate-docker'] - networks: [appnet] - deploy: - mode: replicated-job - replicas: 1 - restart_policy: - condition: on-failure - max_attempts: 3 - site-builder: image: ghcr.io/knowledgefutures/platform-site-builder:${IMAGE_TAG} env_file: [.env] diff --git a/self-host/docker-compose.yml b/self-host/docker-compose.yml index 8d19b2f79a..29741a1611 100644 --- a/self-host/docker-compose.yml +++ b/self-host/docker-compose.yml @@ -10,8 +10,6 @@ services: condition: service_started platform-jobs: condition: service_started - platform-migrations: - condition: service_completed_successfully minio-init: condition: service_completed_successfully platform: linux/amd64 @@ -24,12 +22,12 @@ services: networks: - app-network - # platfrom jobs service + # platform jobs service # takes care of longer running tasks like scheduling actions platform-jobs: depends_on: - platform-migrations: - condition: service_completed_successfully + db: + condition: service_started platform: linux/amd64 image: ghcr.io/pubpub/platform-jobs:latest env_file: .env @@ -40,19 +38,6 @@ services: networks: - app-network - platform-migrations: - platform: linux/amd64 - depends_on: - db: - condition: service_started - image: ghcr.io/pubpub/platform-migrations:latest - env_file: .env - environment: - DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} - command: ["pnpm", "--filter", "core", "migrate-docker"] - networks: - - app-network - # cache cache: image: valkey/valkey:8-alpine From 6870ae23971480c6ca8340c0d78f94cac9ab030a Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 15:32:03 +0200 Subject: [PATCH 34/78] fix: rerun builds --- .github/workflows/on_pr.yml | 200 ++++++++++++++++++------------------ 1 file changed, 100 insertions(+), 100 deletions(-) diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 7bc8b9ec6e..3564b85cbd 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -10,107 +10,107 @@ 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 @@ -170,12 +170,12 @@ jobs: # 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 1792116d0d1279eba2ad574741171449d50feb20 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 15:53:27 +0200 Subject: [PATCH 35/78] fix: also reset and seed --- core/instrumentation.ts | 11 ++-- core/lib/server/migrate.ts | 122 ++++++++++++++++++++++--------------- core/prisma/seed.ts | 36 +++++------ infra/stack.preview.yml | 4 ++ 4 files changed, 100 insertions(+), 73 deletions(-) diff --git a/core/instrumentation.ts b/core/instrumentation.ts index 58d6991428..351dc481c3 100644 --- a/core/instrumentation.ts +++ b/core/instrumentation.ts @@ -18,18 +18,15 @@ export async function register() { logger.info(`Registering instrumentation hook for ${process.env.NEXT_RUNTIME}`) if (process.env.NEXT_RUNTIME === "nodejs") { - if (process.env.NODE_ENV === "development") { - logger.info( - "NEXT_RUNTIME is `nodejs` and NODE_ENV is `development`; skipping OTEL + Sentry registration." - ) - return - } - if (!process.env.SKIP_MIGRATIONS) { const { runMigrations } = await import("./lib/server/migrate") await runMigrations() } + if (process.env.NODE_ENV === "development") { + return + } + await import("./instrumentation.node.mts") } else { logger.info("NEXT_RUNTIME is not `nodejs`; skipping OTEL registration.") diff --git a/core/lib/server/migrate.ts b/core/lib/server/migrate.ts index 85b9766230..11eb91e34b 100644 --- a/core/lib/server/migrate.ts +++ b/core/lib/server/migrate.ts @@ -1,8 +1,12 @@ +import type { Database } from "db/Database" +import type { PrismaMigrationsId } from "db/public" + import { createHash, randomUUID } from "node:crypto" import { existsSync, readdirSync, readFileSync } from "node:fs" import { join, resolve } from "node:path" - +import { Kysely, PostgresDialect, sql } from "kysely" import pg from "pg" + import { logger } from "logger" // arbitrary but stable id used to prevent concurrent migration runs across replicas @@ -21,20 +25,17 @@ CREATE TABLE IF NOT EXISTS "_prisma_migrations" ( PRIMARY KEY ("id") )` -async function connectWithRetry( - connectionString: string, - maxAttempts = 30, - intervalMs = 2000 -): Promise { - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - const client = new pg.Client({ connectionString }) +function sha256(content: string): string { + return createHash("sha256").update(content).digest("hex") +} +async function waitForDatabase(pool: pg.Pool, maxAttempts = 30, intervalMs = 2000) { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { - await client.connect() - return client + const client = await pool.connect() + client.release() + return } catch (err) { - client.end().catch(() => {}) - if (attempt === maxAttempts) { throw new Error( `could not connect to database after ${maxAttempts} attempts: ${err}` @@ -42,17 +43,11 @@ async function connectWithRetry( } logger.info( - `database not ready, retrying in ${intervalMs}ms (attempt ${attempt}/${maxAttempts})...` + `database not ready, retrying in ${intervalMs}ms (${attempt}/${maxAttempts})...` ) await new Promise((r) => setTimeout(r, intervalMs)) } } - - throw new Error("unreachable") -} - -function sha256(content: string): string { - return createHash("sha256").update(content).digest("hex") } export async function runMigrations() { @@ -72,18 +67,36 @@ export async function runMigrations() { return } + const shouldReset = !!process.env.DB_RESET + const shouldSeed = !!process.env.DB_SEED + logger.info(`running migrations from ${migrationsDir}`) - const client = await connectWithRetry(connectionString) + + // max: 1 ensures every operation (kysely typed queries + raw pool.query) + // shares the same underlying connection session, keeping the advisory lock valid + const pool = new pg.Pool({ connectionString, max: 1 }) + const db = new Kysely({ dialect: new PostgresDialect({ pool }) }) try { - await client.query("SELECT pg_advisory_lock($1)", [ADVISORY_LOCK_ID]) + await waitForDatabase(pool) - await client.query(CREATE_MIGRATIONS_TABLE) + await sql`SELECT pg_advisory_lock(${sql.lit(ADVISORY_LOCK_ID)})`.execute(db) - const { rows: failed } = await client.query<{ migration_name: string }>( - `SELECT "migration_name" FROM "_prisma_migrations" - WHERE "finished_at" IS NULL AND "rolled_back_at" IS NULL` - ) + if (shouldReset) { + logger.info("resetting database (DB_RESET is set)") + await pool.query("DROP SCHEMA public CASCADE") + await pool.query("CREATE SCHEMA public") + } + + // raw string query so pg uses the simple protocol (supports multi-statement sql) + await pool.query(CREATE_MIGRATIONS_TABLE) + + const failed = await db + .selectFrom("_prisma_migrations") + .select("migration_name") + .where("finished_at", "is", null) + .where("rolled_back_at", "is", null) + .execute() if (failed.length > 0) { const names = failed.map((r) => r.migration_name).join(", ") @@ -93,10 +106,13 @@ export async function runMigrations() { ) } - const { rows: applied } = await client.query<{ migration_name: string }>( - `SELECT "migration_name" FROM "_prisma_migrations" - WHERE "finished_at" IS NOT NULL AND "rolled_back_at" IS NULL` - ) + const applied = await db + .selectFrom("_prisma_migrations") + .select("migration_name") + .where("finished_at", "is not", null) + .where("rolled_back_at", "is", null) + .execute() + const appliedNames = new Set(applied.map((r) => r.migration_name)) const dirs = readdirSync(migrationsDir, { withFileTypes: true }) @@ -116,32 +132,33 @@ export async function runMigrations() { continue } - const sql = readFileSync(sqlPath, "utf-8") - const checksum = sha256(sql) - const id = randomUUID() + const migrationSql = readFileSync(sqlPath, "utf-8") + const checksum = sha256(migrationSql) + const id = randomUUID() as PrismaMigrationsId logger.info(`applying migration: ${dir}`) - await client.query( - `INSERT INTO "_prisma_migrations" ("id", "checksum", "migration_name", "started_at", "applied_steps_count") - VALUES ($1, $2, $3, now(), 0)`, - [id, checksum, dir] - ) + await db + .insertInto("_prisma_migrations") + .values({ id, checksum, migration_name: dir }) + .execute() try { - await client.query(sql) + await pool.query(migrationSql) } catch (err) { - await client.query( - `UPDATE "_prisma_migrations" SET "logs" = $1 WHERE "id" = $2`, - [String(err), id] - ) + await db + .updateTable("_prisma_migrations") + .set({ logs: String(err) }) + .where("id", "=", id) + .execute() throw err } - await client.query( - `UPDATE "_prisma_migrations" SET "finished_at" = now(), "applied_steps_count" = 1 WHERE "id" = $1`, - [id] - ) + await db + .updateTable("_prisma_migrations") + .set({ finished_at: new Date(), applied_steps_count: 1 }) + .where("id", "=", id) + .execute() count++ } @@ -151,8 +168,15 @@ export async function runMigrations() { } else { logger.info("database is up to date, no pending migrations") } + + if (shouldSeed) { + logger.info("running database seed (DB_SEED is set)") + const { seed } = await import("~/prisma/seed") + await seed() + } + + await sql`SELECT pg_advisory_unlock(${sql.lit(ADVISORY_LOCK_ID)})`.execute(db) } finally { - await client.query("SELECT pg_advisory_unlock($1)", [ADVISORY_LOCK_ID]).catch(() => {}) - await client.end() + await db.destroy() } } diff --git a/core/prisma/seed.ts b/core/prisma/seed.ts index d125f071c8..ad5337af06 100644 --- a/core/prisma/seed.ts +++ b/core/prisma/seed.ts @@ -16,11 +16,7 @@ const starterId = "bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbbbb" as CommunitiesId const blankId = "cccccccc-cccc-4ccc-cccc-cccccccccccc" as CommunitiesId const coarNotifyId = "dddddddd-dddd-4ddd-dddd-dddddddddddd" as CommunitiesId -async function main() { - // do not seed arcadia if the minimal seed flag is set - // this is because it will slow down ci/testing - // this flag is set in the `globalSetup.ts` file - // and in e2e.yml +export async function seed() { // eslint-disable-next-line no-restricted-properties const shouldSeedLegacy = !process.env.MINIMAL_SEED @@ -53,16 +49,22 @@ async function main() { await seedCoarNotify(coarNotifyId) } -main() - .then(async () => { - logger.info("Finished seeding, exiting...") - process.exit(0) - }) - .catch(async (e) => { - if (!isUniqueConstraintError(e)) { + +// cli entrypoint: only auto-run when executed directly as a script +const isCli = process.argv[1]?.endsWith("seed.ts") || process.argv[1]?.endsWith("seed.js") + +if (isCli) { + seed() + .then(async () => { + logger.info("Finished seeding, exiting...") + process.exit(0) + }) + .catch(async (e) => { + if (!isUniqueConstraintError(e)) { + logger.error(e) + process.exit(1) + } logger.error(e) - process.exit(1) - } - logger.error(e) - logger.info("Attempted to add duplicate entries, db is already seeded?") - }) + logger.info("Attempted to add duplicate entries, db is already seeded?") + }) +} diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index f8c9f4395b..7722e06065 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -36,6 +36,8 @@ services: SITE_BUILDER_ENDPOINT: http://site-builder:4000 S3_PUBLIC_ENDPOINT: https://pr-${PR_NUMBER}.pubstar.org/assets FLAGS: "uploads:off,invites:off,disabled-actions:http+email" + DB_RESET: true + DB_SEED: true networks: [appnet] healthcheck: test: @@ -101,6 +103,8 @@ services: image: minio/minio:latest env_file: [.env] command: server --console-address ":9001" /data + environment: + MINIO_BROWSER_REDIRECT_URL: https://pr-${PR_NUMBER}.pubstar.org/assets-ui networks: [appnet] deploy: replicas: 1 From cc36e7ecdfcf3f020f17cadeee1e6722d77946cf Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 16:05:04 +0200 Subject: [PATCH 36/78] fix: speed up test --- .github/workflows/on_pr.yml | 200 ++++++++++++++++++------------------ infra/stack.preview.yml | 4 +- 2 files changed, 102 insertions(+), 102 deletions(-) diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 3564b85cbd..8cabd0742b 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -10,114 +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 }}" + # 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: cbc8e65b326aacaf3e4ac8d15a35358b1fc71930 + image_tag: 1792116d0d1279eba2ad574741171449d50feb20 secrets: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_USER: ${{ secrets.SSH_USER }} @@ -170,12 +170,12 @@ jobs: # 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 diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index 7722e06065..ef1560e4c6 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -36,8 +36,8 @@ services: SITE_BUILDER_ENDPOINT: http://site-builder:4000 S3_PUBLIC_ENDPOINT: https://pr-${PR_NUMBER}.pubstar.org/assets FLAGS: "uploads:off,invites:off,disabled-actions:http+email" - DB_RESET: true - DB_SEED: true + DB_RESET: "true" + DB_SEED: "true" networks: [appnet] healthcheck: test: From e65d3f7dd5d5348c8e2b169c61427f46f3723bab Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 16:24:50 +0200 Subject: [PATCH 37/78] fix: add mechanism to skip autocache during seed --- core/lib/server/cache/autoCache.ts | 31 ++++++++++++-- core/lib/server/cache/autoRevalidate.ts | 25 ++++++++++-- core/lib/server/cache/skipCacheStore.ts | 54 +++++++++++++++++++++++++ core/lib/server/migrate.ts | 5 ++- core/prisma/seed/seedCommunity.ts | 4 +- 5 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 core/lib/server/cache/skipCacheStore.ts diff --git a/core/lib/server/cache/autoCache.ts b/core/lib/server/cache/autoCache.ts index b6bad2699d..c09875700c 100644 --- a/core/lib/server/cache/autoCache.ts +++ b/core/lib/server/cache/autoCache.ts @@ -4,12 +4,14 @@ import type { AutoCacheOptions, DirectAutoOutput, ExecuteFn, SQB } from "./types import { cache } from "react" import { logger } from "logger" +import { tryCatch } from "utils/try-catch" import { env } from "~/lib/env/env" import { createCacheTag, createCommunityCacheTags } from "./cacheTags" import { getCommunitySlug } from "./getCommunitySlug" import { memoize } from "./memoize" import { cachedFindTables, directAutoOutput } from "./sharedAuto" +import { shouldSkipCache as shouldSkipCacheStore } from "./skipCacheStore" import { getTablesWithLinkedTables } from "./specialTables" import { getTransactionStore, setTransactionStore } from "./transactionStorage" @@ -60,10 +62,33 @@ const executeWithCache = < options?: AutoCacheOptions ) => { const executeFn = cache(async (...args: Parameters) => { - const communitySlug = options?.communitySlug ?? (await getCommunitySlug()) - const compiledQuery = qb.compile() + const willSkipCacheStore = shouldSkipCacheStore("store") + const willSkipCacheFn = options?.skipCacheFn?.() + + const willSkipCache = willSkipCacheStore || willSkipCacheFn + + if (willSkipCache) { + logger.debug( + willSkipCacheStore + ? `Skipping cache for query ${compiledQuery.sql} because of skipCacheStore` + : `Skipping cache for query ${compiledQuery.sql} because of skipCacheFn` + ) + + return qb[method](...args) as ReturnType + } + + const [error, communitySlug] = options?.communitySlug + ? await tryCatch(getCommunitySlug()) + : [null, options?.communitySlug!] + + if (error) { + logger.error(`Error getting community slug: ${error.message}`) + logger.error(compiledQuery.sql) + throw error + } + const tables = await cachedFindTables(compiledQuery, "select") const allTables = getTablesWithLinkedTables(tables) @@ -88,7 +113,7 @@ const executeWithCache = < asOne ) - if (shouldSkipCache || options?.skipCacheFn?.()) { + if (shouldSkipCache) { if (env.CACHE_LOG) { logger.debug(`AUTOCACHE: Skipping cache for query: ${asOne}`) } diff --git a/core/lib/server/cache/autoRevalidate.ts b/core/lib/server/cache/autoRevalidate.ts index bfcd8cf961..68faaa125f 100644 --- a/core/lib/server/cache/autoRevalidate.ts +++ b/core/lib/server/cache/autoRevalidate.ts @@ -4,11 +4,13 @@ import type { AutoRevalidateOptions, DirectAutoOutput, ExecuteFn, QB } from "./t import { revalidatePath, revalidateTag } from "next/cache" import { logger } from "logger" +import { tryCatch } from "utils/try-catch" import { env } from "~/lib/env/env" import { getCommunitySlug } from "./getCommunitySlug" import { revalidateTagsForCommunity } from "./revalidate" import { cachedFindTables, directAutoOutput } from "./sharedAuto" +import { shouldSkipCache as shouldSkipCacheStore } from "./skipCacheStore" import { setTransactionStore } from "./transactionStorage" const executeWithRevalidate = < @@ -20,11 +22,28 @@ const executeWithRevalidate = < options?: AutoRevalidateOptions ) => { const executeFn = async (...args: Parameters) => { - const communitySlug = options?.communitySlug ?? (await getCommunitySlug()) + const compiledQuery = qb.compile() - const communitySlugs = Array.isArray(communitySlug) ? communitySlug : [communitySlug] + const willSkipCacheStore = shouldSkipCacheStore("invalidate") - const compiledQuery = qb.compile() + if (willSkipCacheStore) { + logger.debug( + `Skipping revalidation for query ${compiledQuery.sql} because of skipCacheStore` + ) + return qb[method](...args) as ReturnType + } + + const [error, communitySlug] = options?.communitySlug + ? await tryCatch(getCommunitySlug()) + : [null, options?.communitySlug!] + + if (error) { + logger.error(`Error getting community slug: ${error.message}`) + logger.error(compiledQuery.sql) + throw error + } + + const communitySlugs = Array.isArray(communitySlug) ? communitySlug : [communitySlug] const tables = await cachedFindTables(compiledQuery, "mutation") diff --git a/core/lib/server/cache/skipCacheStore.ts b/core/lib/server/cache/skipCacheStore.ts new file mode 100644 index 0000000000..1d690270b9 --- /dev/null +++ b/core/lib/server/cache/skipCacheStore.ts @@ -0,0 +1,54 @@ +import { AsyncLocalStorage } from "node:async_hooks" + +import { logger } from "logger" + +const SKIP_CACHE_OPTIONS = ["store", "invalidate", "both"] as const +export type SkipCacheOptions = (typeof SKIP_CACHE_OPTIONS)[number] + +// tags +export const skipCacheStore = new AsyncLocalStorage<{ + /** + * Whether to store the result in the cache or invalidate it + */ + shouldSkipCache: "store" | "invalidate" | "both" | undefined +}>() + +export const setSkipCacheStore = ({ shouldSkipCache }: { shouldSkipCache: SkipCacheOptions }) => { + const store = skipCacheStore.getStore() + + if (!store) { + logger.debug("no skip cache store found") + return + } + + store.shouldSkipCache = shouldSkipCache + + return store +} + +/** + * whether or not to skip the cache + */ +export const shouldSkipCache = (skipCacheOptions: SkipCacheOptions) => { + const store = skipCacheStore.getStore() + + if (!store) { + return false + } + + if (store.shouldSkipCache === "both") { + return true + } + + return store.shouldSkipCache === skipCacheOptions +} + +/** + * wrap a function with this to skip storing and/or invalidating the cache + * useful when outside of community contexts and you don't want to cache results + */ +export const withUncached = (fn: () => Promise, skipCacheOptions?: SkipCacheOptions) => { + return skipCacheStore.run({ shouldSkipCache: skipCacheOptions ?? "invalidate" }, async () => { + return fn() + }) +} diff --git a/core/lib/server/migrate.ts b/core/lib/server/migrate.ts index 11eb91e34b..91c0180dc3 100644 --- a/core/lib/server/migrate.ts +++ b/core/lib/server/migrate.ts @@ -172,7 +172,10 @@ export async function runMigrations() { if (shouldSeed) { logger.info("running database seed (DB_SEED is set)") const { seed } = await import("~/prisma/seed") - await seed() + + // prevents autocache from running, breaking seed + const { withUncached } = await import("~/lib/server/cache/skipCacheStore") + await withUncached(seed, "both") } await sql`SELECT pg_advisory_unlock(${sql.lit(ADVISORY_LOCK_ID)})`.execute(db) diff --git a/core/prisma/seed/seedCommunity.ts b/core/prisma/seed/seedCommunity.ts index 28ba880171..e4872c743e 100644 --- a/core/prisma/seed/seedCommunity.ts +++ b/core/prisma/seed/seedCommunity.ts @@ -1196,10 +1196,12 @@ export async function seedCommunity< logger.info( `${createdCommunity.name}: ${options?.parallelPubs ? "Parallelly" : "Sequentially"} - Creating ${createPubRecursiveInput.length} pubs` ) + if (options?.parallelPubs) { const input = createPubRecursiveInput.map((input) => createPubRecursiveNew({ ...input })) - setInterval(() => { + // using will auto clear the interval when the block is exited + using _interval = setInterval(() => { logger.info(`${createdCommunity.name}: Creating Pubs...`) }, 1000) From b40f1a90895d8418c763ef9bdc0ab3a7bd7cf187 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 16:53:10 +0200 Subject: [PATCH 38/78] fix: preview again... --- .github/workflows/on_pr.yml | 202 ++++++++++++++++++------------------ 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 8cabd0742b..ecdbff70ff 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -10,114 +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 }}" + 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: 1792116d0d1279eba2ad574741171449d50feb20 + image_tag: ${{ github.event.pull_request.head.sha }} + # image_tag: 1792116d0d1279eba2ad574741171449d50feb20 secrets: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_USER: ${{ secrets.SSH_USER }} @@ -170,12 +170,12 @@ jobs: # 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 649e4db7bda78d344e8a3f94c0f8c6fd61fc6591 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 17:03:00 +0200 Subject: [PATCH 39/78] fix: make logic make sense --- core/lib/server/cache/autoCache.ts | 4 ++-- core/lib/server/cache/autoRevalidate.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/lib/server/cache/autoCache.ts b/core/lib/server/cache/autoCache.ts index c09875700c..bea68ed00d 100644 --- a/core/lib/server/cache/autoCache.ts +++ b/core/lib/server/cache/autoCache.ts @@ -80,8 +80,8 @@ const executeWithCache = < } const [error, communitySlug] = options?.communitySlug - ? await tryCatch(getCommunitySlug()) - : [null, options?.communitySlug!] + ? [null, options.communitySlug] + : await tryCatch(getCommunitySlug()) if (error) { logger.error(`Error getting community slug: ${error.message}`) diff --git a/core/lib/server/cache/autoRevalidate.ts b/core/lib/server/cache/autoRevalidate.ts index 68faaa125f..c89015ad12 100644 --- a/core/lib/server/cache/autoRevalidate.ts +++ b/core/lib/server/cache/autoRevalidate.ts @@ -34,8 +34,8 @@ const executeWithRevalidate = < } const [error, communitySlug] = options?.communitySlug - ? await tryCatch(getCommunitySlug()) - : [null, options?.communitySlug!] + ? [null, options.communitySlug] + : await tryCatch(getCommunitySlug()) if (error) { logger.error(`Error getting community slug: ${error.message}`) From 53760805d4d600f8acfb8740448766bcc85ce00b Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 18:54:28 +0200 Subject: [PATCH 40/78] fix: inline html file --- Dockerfile | 2 +- core/prisma/seeds/legacy.ts | 3 +- ...{ponies.snippet.html => ponies.snippet.ts} | 159 +++++++++--------- 3 files changed, 83 insertions(+), 81 deletions(-) rename core/prisma/seeds/{ponies.snippet.html => ponies.snippet.ts} (96%) diff --git a/Dockerfile b/Dockerfile index 72c772c19b..a554830d7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -139,4 +139,4 @@ COPY --from=withpackage --chown=node:node /usr/src/app/core/.env.docker ./core/. # migration sql files, applied automatically during startup instrumentation COPY --from=withpackage --chown=node:node /usr/src/app/core/prisma/migrations ./core/prisma/migrations -CMD ["node", "core/server.js"] +CMD ["node", "--enable-source-maps", "core/server.js"] diff --git a/core/prisma/seeds/legacy.ts b/core/prisma/seeds/legacy.ts index 7a3468c2cc..9766a0f636 100644 --- a/core/prisma/seeds/legacy.ts +++ b/core/prisma/seeds/legacy.ts @@ -1,6 +1,5 @@ import type { CommunitiesId, PubsId } from "db/public" -import { readFile } from "node:fs/promises" import { faker } from "@faker-js/faker" import { CoreSchemaType, MemberRole } from "db/public" @@ -13,7 +12,7 @@ import { usersExisting } from "./users" const abstract = `

The development of AAV capsids for therapeutic gene delivery has exploded in popularity over the past few years. However, humans aren’t the first or only species using viral capsids for gene delivery — wasps evolved this tactic over 100 million years ago. Parasitoid wasps that lay eggs inside arthropod hosts have co-opted ancient viruses for gene delivery to manipulate multiple aspects of the host’s biology, thereby increasing the probability of survival of the wasp larvae

` export const seedLegacy = async (communityId?: CommunitiesId) => { - const poniesText = await readFile(new URL("./ponies.snippet.html", import.meta.url), "utf-8") + const { poniesText } = await import("./ponies.snippet") const articleSeed = (number = 1_000, asRelation = false) => Array.from({ length: number }, (_, idx) => { diff --git a/core/prisma/seeds/ponies.snippet.html b/core/prisma/seeds/ponies.snippet.ts similarity index 96% rename from core/prisma/seeds/ponies.snippet.html rename to core/prisma/seeds/ponies.snippet.ts index 67faad2418..827150c924 100644 --- a/core/prisma/seeds/ponies.snippet.html +++ b/core/prisma/seeds/ponies.snippet.ts @@ -1,3 +1,5 @@ +// prettier-ignore +export const poniesText = `
Experimental design >166±29kg166 \pm 29 kg166 pm 29 kgFabrication of construct >=10μm= 10 \mu m= 10 mu mFabrication of construct >300x300μm300x300 \mu m300x300 mu mFabrication of construct >1300μm1300 \mu m1300 mu mFabrication of construct >c−115 mm\cdot sec^{-1}15 mmcdot sec^{-1}Fabrication of construct >22−24°C22 - 24 \degree \text{C}22 - 24 degree \text{C}Fabrication of construct >30−50%30 - 50\%30 - 50%Fabrication of construct >70%70\%70%Fabrication of construct >l−12.2 g\cdot ml^{-1}2.2 gcdot ml^{-1}Fabrication of construct >α\alphaalphaFabrication of construct >=3.83μm= 3.83 \mu m= 3.83 mu mFabrication of construct >l−10.13 g\cdot ml^{-1}0.13 gcdot ml^{-1}Fabrication of construct >v−140\% w\cdot v^{-1}40% wcdot v^{-1}Fabrication of construct >α\alphaalphaFabrication of construct >0.22μm0.22 \mu m0.22 mu mFabrication of construct >4°C4\degree \text{C}4degree \text{C}Fabrication of construct >=250μm= 250 \mu m= 250 mu mFabrication of construct >700μm700 \mu m700 mu mFabrication of construct >70%70\%70%In Vitro pre-culture >37°C37\degree \text{C}37degree \text{C}In Vitro pre-culture >v−10.2\% w\cdot v^{-1}0.2% wcdot v^{-1}In Vitro pre-culture >v−10.075\% w\cdot v^{-1}0.075% wcdot v^{-1}In Vitro pre-culture >v−110\% v\cdot v^{-1}10% vcdot v^{-1}In Vitro pre-culture >1%1\%1%In Vitro pre-culture >L−1100 U\cdot mL^{-1}100 Ucdot mL^{-1}In Vitro pre-culture >L−1100 \mu g\cdot mL^{-1}100 mu gcdot mL^{-1}In Vitro pre-culture >100μl100 \mu l100 mu lIn Vitro pre-culture >l−1100 ng\cdot ml^{-1}100 ngcdot ml^{-1}Surgical procedure >g−110 \mu g\cdot kg^{-1}10 mu gcdot kg^{-1}Surgical procedure >g−10.1 mg\cdot kg^{-1}0.1 mgcdot kg^{-1}Surgical procedure >g−10.06 mg\cdot kg^{-1}0.06 mgcdot kg^{-1}Surgical procedure >g−12.2 mg\cdot kg^{-1}2.2 mgcdot kg^{-1}Surgical procedure >g−110 \mu g\cdot kg^{-1}10 mu gcdot kg^{-1}Surgical procedure >g−10.5 mg\cdot kg^{-1}0.5 mgcdot kg^{-1}Surgical procedure >g−10.6 mg\cdot kg^{-1}0.6 mgcdot kg^{-1}Surgical procedure >g−10.1 - 0.2 mg\cdot kg^{-1}0.1 - 0.2 mgcdot kg^{-1}Surgical procedure >g−110 - 15 mg\cdot kg^{-1}10 - 15 mgcdot kg^{-1}Surgical procedure >g−120 mg\cdot kg^{-1}20 mgcdot kg^{-1}Surgical procedure >g−10.6 mg\cdot kg^{-1}0.6 mgcdot kg^{-1}Surgical procedure >g−15 mg\cdot kg^{-1}5 mgcdot kg^{-1}Euthanasia and sample harvest >g−10.06mg\cdot kg^{-1}0.06mgcdot kg^{-1}Euthanasia and sample harvest >g−12.2 mg\cdot kg^{-1}2.2 mgcdot kg^{-1}Euthanasia and sample harvest >g−11400 mg\cdot kg^{-1}1400 mgcdot kg^{-1}Biomechanical evaluation >n−10.250 N\cdot min^{-1}0.250 Ncdot min^{-1}Biomechanical evaluation >200μm200 \mu m200 mu mBiomechanical evaluation >10−12%10-12 \%10-12 %Biochemical evaluation >60°C60\degree \text{C}60degree \text{C}Biochemical evaluation >−80°C-80\degree \text{C}-80degree \text{C}Microcomputed tomography >=200μA= 200 \mu A= 200 mu AMicrocomputed tomography >=30μm3= 30 \mu m^{3}= 30 mu m^{3}Histological evaluation >4%4\%4%Histological evaluation >5μm5 \mu m5 mu mHistological evaluation >l−11.083 mg\cdot ml^{-1}1.083 mgcdot ml^{-1}Histological evaluation >l−10.06 mg\cdot ml^{-1}0.06 mgcdot ml^{-1}Histological evaluation >4%4\%4%Histological evaluation >5μm5 \mu m5 mu mStatistical analysis >±\pmpmIn vitro >⋅\cdotcdotIn vitro >g−1199.7 \pm 67.7 \mu g\cdot \mu g^{-1}199.7 pm 67.7 mu gcdot mu g^{-1}In vitro >g−13702 \pm 2111 U\cdot\mu g^{-1}3702 pm 2111 Ucdotmu g^{-1} >g−130.46 \pm 15.95 \mu g\cdot\mu g^{-1}30.46 pm 15.95 mu gcdotmu g^{-1} >g−124.44 \pm 15.31 \mu g\cdot g^{-1}24.44 pm 15.31 mu gcdot g^{-1} >g−179.66 \pm 91.21 \mu g\cdot\mu g^{-1}79.66 pm 91.21 mu gcdotmu g^{-1} >g−1134.21\pm 153.73 \mu g\cdot\mu g^{-1}134.21pm 153.73 mu gcdotmu g^{-1}0.31 \pm 0.13 MPa0.31 pm 0.13 MPa0.42 \pm 0.19 MPa0.42 pm 0.19 MPa1.75 \pm 0.80 MPa1.75 pm 0.80 MPa2.22 \pm 0.48 MPa2.22 pm 0.48 MPa1.86 \pm 0.78 MPa1.86 pm 0.78 MPa2.19 \pm 0.77 MPa2.19 pm 0.77 MPa >6.14%±10.09%6.14\% \pm 10.09\%6.14% pm 10.09% >4.73%±4.93%4.73\% \pm 4.93\%4.73% pm 4.93% >81.38%±15.37%81.38\% \pm 15.37\%81.38% pm 15.37% >74.71%±12.44%74.71\% \pm 12.44\%74.71% pm 12.44% >12.48%±9.75%12.48\% \pm 9.75\%12.48% pm 9.75% >20.56%±10.54%20.56\% \pm 10.54\%20.56% pm 10.54% >79.02±16.18%79.02 \pm 16.18 \%79.02 pm 16.18 % >63.20±13.90%63.20 \pm 13.90 \%63.20 pm 13.90 %Supplementary Table 2. Lymphocyte.
+` From fbd7b87448a9d5f39fb4cb60b697899c31ae8963 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 19:33:11 +0200 Subject: [PATCH 41/78] fix: clear cache on seed --- core/lib/server/migrate.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/lib/server/migrate.ts b/core/lib/server/migrate.ts index 91c0180dc3..4b2fa60db6 100644 --- a/core/lib/server/migrate.ts +++ b/core/lib/server/migrate.ts @@ -1,12 +1,18 @@ import type { Database } from "db/Database" import type { PrismaMigrationsId } from "db/public" +import { exec } from "node:child_process" import { createHash, randomUUID } from "node:crypto" import { existsSync, readdirSync, readFileSync } from "node:fs" import { join, resolve } from "node:path" +import { promisify } from "node:util" import { Kysely, PostgresDialect, sql } from "kysely" import pg from "pg" +import { tryCatch } from "utils/try-catch" + +const execAsync = promisify(exec) + import { logger } from "logger" // arbitrary but stable id used to prevent concurrent migration runs across replicas @@ -176,6 +182,18 @@ export async function runMigrations() { // prevents autocache from running, breaking seed const { withUncached } = await import("~/lib/server/cache/skipCacheStore") await withUncached(seed, "both") + + // try and reset cache + logger.info(`Clearing cache...`) + const [error, output] = await tryCatch( + execAsync("echo 'FLUSHALL' | nc $VALKEY_HOST 6379") + ) + + if (error || output.stderr) { + logger.error(`Error clearing cache: ${error?.message || output?.stderr}`) + } else { + logger.info(`Cache cleared: ${output.stdout}`) + } } await sql`SELECT pg_advisory_unlock(${sql.lit(ADVISORY_LOCK_ID)})`.execute(db) From feb5357ca2c2ff34e4f63b3cb09e48f35e013389 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 6 Apr 2026 19:41:07 +0200 Subject: [PATCH 42/78] fix: test setup errors --- .env.docker-compose.dev | 16 ++++++---------- core/lib/editor/to-html.test.ts | 5 ++--- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/.env.docker-compose.dev b/.env.docker-compose.dev index ba4f907456..017fef7222 100644 --- a/.env.docker-compose.dev +++ b/.env.docker-compose.dev @@ -1,18 +1,14 @@ # garage admin token for init script GARAGE_ADMIN_TOKEN=dev_admin_token -ASSETS_BUCKET_NAME=assets.v7.pubpub.org -ASSETS_UPLOAD_KEY=pubpubuser -ASSETS_UPLOAD_SECRET_KEY=pubpubpass -# set to same as above for s3fs/caddy to work -AWS_ACCESS_KEY_ID=pubpubuser -AWS_SECRET_ACCESS_KEY=pubpubpass - -ASSETS_REGION=garage +S3_BUCKET_NAME=assets.v7.pubpub.org +S3_ACCESS_KEY=pubpubuser +S3_SECRET_KEY=pubpubpass +S3_REGION=garage # internal endpoint used by backend services running in Docker -ASSETS_STORAGE_ENDPOINT=http://garage:3900 +S3_ENDPOINT=http://minio:3900 # public endpoint used for signed URLs accessible from browsers -ASSETS_PUBLIC_ENDPOINT=http://localhost:3900 +S3_PUBLIC_ENDPOINT=http://localhost:3900 POSTGRES_PORT=54322 POSTGRES_USER=postgres diff --git a/core/lib/editor/to-html.test.ts b/core/lib/editor/to-html.test.ts index 62885083d8..1574a17271 100644 --- a/core/lib/editor/to-html.test.ts +++ b/core/lib/editor/to-html.test.ts @@ -1,13 +1,12 @@ import { describe, expect, it } from "vitest" -// @ts-expect-error -import ponies from "../../prisma/seeds/ponies.snippet.html?raw" +import { poniesText } from "../../prisma/seeds/ponies.snippet" import { processEditorHTML } from "./process-editor-html" import { htmlToProsemirrorServer, prosemirrorToHTMLServer } from "./serialize-server" describe("renderNodeToHTML", () => { it("should be able to round trip a node and not lose any information", async () => { - const html = ponies + const html = poniesText expect(html).toBeDefined() From 091864eafa27592c8b47c3e5aa95a22b8a3d4af7 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 16:33:43 +0200 Subject: [PATCH 43/78] fix: double escape string --- core/prisma/seeds/ponies.snippet.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/core/prisma/seeds/ponies.snippet.ts b/core/prisma/seeds/ponies.snippet.ts index 827150c924..3b2c852fd4 100644 --- a/core/prisma/seeds/ponies.snippet.ts +++ b/core/prisma/seeds/ponies.snippet.ts @@ -1,6 +1,5 @@ // prettier-ignore -export const poniesText = ` -
+export const poniesText = `
β\beta\\beta22−24°C22 - 24 degree \text{C}22 - 24 degree \\text{C}4°C4degree \text{C}4degree \\text{C}37°C37degree \text{C}37degree \\text{C}60°C60degree \text{C}60degree \\text{C}−80°C-80degree \text{C}-80degree \\text{C}
-` +` as string From 249de57e82a5466a1da38b123caf1b81a35e5a0f Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 17:11:00 +0200 Subject: [PATCH 44/78] fix: better infra --- .../{preview.yml => deploy-stack.yml} | 207 +- .github/workflows/deploy.yml | 250 -- .github/workflows/ghcr-build-all.yml | 6 + .github/workflows/ghcr-build-template.yml | 8 +- .github/workflows/on_main.yml | 25 + .github/workflows/on_pr.yml | 24 +- .github/workflows/on_tag.yml | 94 + core/prisma/seeds/ponies.snippet.html | 2871 +++++++++++++++++ infra/Caddyfile.gateway | 46 + infra/Caddyfile.preview | 81 - infra/Caddyfile.site.template | 31 + infra/stack.gateway.yml | 34 + infra/stack.preview.yml | 43 +- 13 files changed, 3280 insertions(+), 440 deletions(-) rename .github/workflows/{preview.yml => deploy-stack.yml} (52%) delete mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/on_tag.yml create mode 100644 core/prisma/seeds/ponies.snippet.html create mode 100644 infra/Caddyfile.gateway delete mode 100644 infra/Caddyfile.preview create mode 100644 infra/Caddyfile.site.template create mode 100644 infra/stack.gateway.yml diff --git a/.github/workflows/preview.yml b/.github/workflows/deploy-stack.yml similarity index 52% rename from .github/workflows/preview.yml rename to .github/workflows/deploy-stack.yml index bec0c0ddb3..6f69945edc 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/deploy-stack.yml @@ -1,4 +1,4 @@ -name: PR Preview +name: Deploy Stack on: workflow_call: @@ -10,13 +10,37 @@ on: image_tag: required: false type: string - description: "image tag to deploy (only needed for deploy)" + description: "image tag to deploy" + stack_name: + required: true + type: string + description: "docker swarm stack name" + hostname: + required: true + type: string + description: "public hostname for this deployment" + env_file: + required: true + type: string + description: "sops-encrypted env file to decrypt (relative to infra/)" + stack_file: + required: true + type: string + description: "compose file to deploy (e.g. stack.yml or stack.preview.yml)" + uses_gateway: + required: true + type: boolean + description: "if true, deploy behind the shared Caddy gateway" + ssh_host_secret: + required: true + type: string + description: "name of the SSH_HOST secret to use (value passed as secret)" secrets: SSH_PRIVATE_KEY: required: true SSH_USER: required: true - SSH_HOST_PREVIEW: + SSH_HOST: required: true GHCR_USER: required: true @@ -28,19 +52,13 @@ permissions: pull-requests: write jobs: - preview: + deploy: + if: inputs.action == 'deploy' 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.pubstar.org" >> $GITHUB_OUTPUT - - name: Start SSH agent uses: webfactory/ssh-agent@v0.9.0 with: @@ -49,24 +67,25 @@ jobs: - name: Add known hosts run: | mkdir -p ~/.ssh - ssh-keyscan -H "${{ secrets.SSH_HOST_PREVIEW }}" >> ~/.ssh/known_hosts + ssh-keyscan -H "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts - - name: Deploy preview stack - if: inputs.action == 'deploy' + - name: Deploy stack env: SSH_USER: ${{ secrets.SSH_USER }} - SSH_HOST: ${{ secrets.SSH_HOST_PREVIEW }} + SSH_HOST: ${{ secrets.SSH_HOST }} REPO: ${{ github.repository }} - BRANCH: ${{ github.head_ref }} + BRANCH: ${{ github.head_ref || github.ref_name }} 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 }} - PR_NUMBER: ${{ steps.pr.outputs.number }} + STACK_NAME: ${{ inputs.stack_name }} + DEPLOY_HOST: ${{ inputs.hostname }} + ENV_FILE: ${{ inputs.env_file }} + STACK_FILE: ${{ inputs.stack_file }} + USES_GATEWAY: ${{ inputs.uses_gateway }} 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}' DEPLOY_HOST='${DEPLOY_HOST}' ENV_FILE='${ENV_FILE}' STACK_FILE='${STACK_FILE}' USES_GATEWAY='${USES_GATEWAY}' bash -s -- '${REPO}' '${BRANCH}'" <<'EOS' set -euo pipefail REPO="${1:?missing repo}" @@ -76,8 +95,9 @@ jobs: : "${GHCR_USER:?missing GHCR_USER}" : "${GHCR_TOKEN:?missing GHCR_TOKEN}" : "${STACK_NAME:?missing STACK_NAME}" - : "${PREVIEW_HOST:?missing PREVIEW_HOST}" - : "${PR_NUMBER:?missing PR_NUMBER}" + : "${DEPLOY_HOST:?missing DEPLOY_HOST}" + : "${ENV_FILE:?missing ENV_FILE}" + : "${STACK_FILE:?missing STACK_FILE}" REPO_NAME="${REPO##*/}" APP_DIR="/srv/${REPO_NAME}" @@ -93,12 +113,13 @@ jobs: fi cd "${APP_DIR}" - git fetch --prune origin - git checkout "origin/${BRANCH}" --detach + git fetch --prune --tags origin + git checkout --detach "${IMAGE_TAG}" 2>/dev/null || git checkout --detach "origin/${BRANCH}" cd infra + umask 077 - sops -d --input-type dotenv --output-type dotenv ".env.preview.enc" > .env + 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}')" @@ -106,25 +127,48 @@ jobs: echo "$GHCR_TOKEN" | sudo docker login ghcr.io -u "$GHCR_USER" --password-stdin - 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" PR_NUMBER="$PR_NUMBER" \ - docker stack deploy -c stack.preview.yml \ - --with-registry-auth --resolve-image always --prune "$STACK_NAME" + if [[ "$USES_GATEWAY" == "true" ]]; then + # ensure the gateway stack is running + if ! sudo docker stack ls --format '{{.Name}}' | grep -qx gateway; then + echo "deploying gateway stack..." + sudo docker stack deploy -c stack.gateway.yml --prune gateway + sleep 5 + fi + + # write per-site caddy config from template + sudo mkdir -p /srv/caddy/sites.d + + export DEPLOY_HOST STACK_NAME + envsubst '$DEPLOY_HOST $STACK_NAME' < Caddyfile.site.template \ + | sudo tee /srv/caddy/sites.d/${STACK_NAME}.caddy > /dev/null + + # deploy the app stack + sudo env IMAGE_TAG="$IMAGE_TAG" DEPLOY_HOST="$DEPLOY_HOST" \ + docker stack deploy -c "$STACK_FILE" \ + --with-registry-auth --resolve-image always --prune "$STACK_NAME" + + # reload caddy to pick up the new site config + gateway_container=$(sudo docker ps --filter "label=com.docker.swarm.service.name=gateway_proxy" --format '{{.ID}}' | head -1) + if [[ -n "$gateway_container" ]]; then + sudo docker exec "$gateway_container" caddy reload --config /etc/caddy/Caddyfile + fi + + else + # standalone mode (production) -- deploy full stack with its own caddy + echo "deploying with IMAGE_TAG=$IMAGE_TAG" - # caddy's config is a bind mount, so swarm won't restart - # the proxy when only the Caddyfile changes on disk - sudo docker service update --force "${STACK_NAME}_proxy" + sudo env IMAGE_TAG="$IMAGE_TAG" \ + docker stack deploy -c "$STACK_FILE" \ + --with-registry-auth --resolve-image always --prune "$STACK_NAME" + fi sudo docker stack services "$STACK_NAME" sudo docker image prune -f wait_rollout() { - echo "Beginning wait for rollout of $1..." + echo "waiting for rollout of $1..." svc="$1" timeout="${2:-600}" end=$((SECONDS+timeout)) @@ -143,7 +187,7 @@ jobs: sleep 5 done - echo "Rollout timeout for $svc" + echo "rollout timeout for $svc" return 1 } @@ -151,45 +195,16 @@ jobs: 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' + if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | - const body = `Preview deployed at https://${{ steps.pr.outputs.host }}`; + const body = `Preview deployed at https://${{ inputs.hostname }}`; const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: ${{ steps.pr.outputs.number }}, + issue_number: context.issue.number, }); const existing = comments.find(c => c.body.includes('Preview deployed at')); if (existing) { @@ -203,7 +218,59 @@ jobs: await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: ${{ steps.pr.outputs.number }}, + issue_number: context.issue.number, body, }); } + + teardown: + if: inputs.action == 'teardown' + runs-on: ubuntu-latest + timeout-minutes: 10 + 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 "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts + + - name: Teardown stack + env: + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST: ${{ secrets.SSH_HOST }} + STACK_NAME: ${{ inputs.stack_name }} + USES_GATEWAY: ${{ inputs.uses_gateway }} + run: | + ssh "${SSH_USER}@${SSH_HOST}" \ + "env STACK_NAME='${STACK_NAME}' USES_GATEWAY='${USES_GATEWAY}' bash -s" <<'EOS' + set -euo pipefail + : "${STACK_NAME:?missing STACK_NAME}" + + echo "tearing down stack $STACK_NAME" + + if sudo docker stack ls --format '{{.Name}}' | grep -qx "$STACK_NAME"; then + sudo docker stack rm "$STACK_NAME" + sleep 10 + 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 + + if [[ "$USES_GATEWAY" == "true" ]]; then + sudo rm -f "/srv/caddy/sites.d/${STACK_NAME}.caddy" + + gateway_container=$(sudo docker ps --filter "label=com.docker.swarm.service.name=gateway_proxy" --format '{{.ID}}' | head -1) + if [[ -n "$gateway_container" ]]; then + sudo docker exec "$gateway_container" caddy reload --config /etc/caddy/Caddyfile + fi + fi + + sudo docker image prune -f + + EOS diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 8e9d34f39c..0000000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,250 +0,0 @@ -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 pubstar - 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 - secrets: | - SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - target: next-app-core - tags: | - 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-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 - secrets: | - SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - target: jobs - tags: | - 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 - 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 - secrets: | - SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - target: jobs - tags: | - 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: - 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 \ - pubstar - - sudo docker stack services pubstar - 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 pubstar 600 - - EOS diff --git a/.github/workflows/ghcr-build-all.yml b/.github/workflows/ghcr-build-all.yml index 00081a272e..d87e910442 100644 --- a/.github/workflows/ghcr-build-all.yml +++ b/.github/workflows/ghcr-build-all.yml @@ -6,6 +6,9 @@ on: publish_latest: type: boolean default: false + image_tag: + type: string + description: "Override the image tag (e.g. v1.2.3). Falls back to git describe." outputs: core-image: description: 'Core image ref' @@ -27,6 +30,7 @@ jobs: package: core ghcr_image_name: platform publish_latest: ${{ inputs.publish_latest }} + image_tag: ${{ inputs.image_tag }} secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} @@ -37,6 +41,7 @@ jobs: target: jobs ghcr_image_name: platform-jobs publish_latest: ${{ inputs.publish_latest }} + image_tag: ${{ inputs.image_tag }} secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} @@ -47,5 +52,6 @@ jobs: target: jobs ghcr_image_name: platform-site-builder publish_latest: ${{ inputs.publish_latest }} + image_tag: ${{ inputs.image_tag }} 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 32da2fa6f3..58ecb4a598 100644 --- a/.github/workflows/ghcr-build-template.yml +++ b/.github/workflows/ghcr-build-template.yml @@ -16,6 +16,9 @@ on: publish_latest: type: boolean default: false + image_tag: + type: string + description: "Override the image tag (e.g. v1.2.3). Falls back to git describe." outputs: image-sha: description: 'Full GHCR image ref with SHA tag' @@ -54,7 +57,10 @@ jobs: - name: Compute image tags id: label run: | - sha_short=$(git describe --always --abbrev=40 --dirty) + sha_short="${{ inputs.image_tag || '' }}" + if [[ -z "$sha_short" ]]; then + sha_short=$(git describe --always --abbrev=40 --dirty) + fi if [[ -z "${{ inputs.package }}" ]]; then echo "target=monorepo" >> $GITHUB_OUTPUT diff --git a/.github/workflows/on_main.yml b/.github/workflows/on_main.yml index 3b287557ec..612b5c8f81 100644 --- a/.github/workflows/on_main.yml +++ b/.github/workflows/on_main.yml @@ -4,10 +4,13 @@ on: push: branches: - main + workflow_dispatch: permissions: + id-token: write contents: read packages: write + pull-requests: write jobs: ci: @@ -30,6 +33,28 @@ jobs: - build-all uses: ./.github/workflows/e2e.yml + deploy-sandbox: + needs: build-all + permissions: + contents: read + pull-requests: write + uses: ./.github/workflows/deploy-stack.yml + with: + action: deploy + image_tag: ${{ github.sha }} + stack_name: sandbox + hostname: sandbox.pubstar.org + env_file: .env.sandbox.enc + stack_file: stack.preview.yml + uses_gateway: true + ssh_host_secret: SSH_HOST_PREVIEW + secrets: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST: ${{ secrets.SSH_HOST_PREVIEW }} + GHCR_USER: ${{ secrets.GHCR_USER }} + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} + deploy-docs: permissions: contents: write diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index ecdbff70ff..8937b39a01 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -8,6 +8,7 @@ permissions: id-token: write contents: read packages: write + pull-requests: write jobs: path-filter: @@ -108,7 +109,7 @@ jobs: deploy-preview: if: needs.build-all.result == 'success' && contains(github.event.pull_request.labels.*.name, 'preview') - uses: ./.github/workflows/preview.yml + uses: ./.github/workflows/deploy-stack.yml needs: - build-all permissions: @@ -116,27 +117,38 @@ jobs: pull-requests: write with: action: deploy - image_tag: ${{ github.event.pull_request.head.sha }} - # image_tag: 1792116d0d1279eba2ad574741171449d50feb20 + image_tag: ${{ github.event.pull_request.head.sha }} + stack_name: preview-pr-${{ github.event.pull_request.number }} + hostname: pr-${{ github.event.pull_request.number }}.preview.pubstar.org + env_file: .env.preview.enc + stack_file: stack.preview.yml + uses_gateway: true + ssh_host_secret: SSH_HOST_PREVIEW secrets: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_USER: ${{ secrets.SSH_USER }} - SSH_HOST_PREVIEW: ${{ secrets.SSH_HOST_PREVIEW }} + SSH_HOST: ${{ secrets.SSH_HOST_PREVIEW }} GHCR_USER: ${{ secrets.GHCR_USER }} GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} close-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 + uses: ./.github/workflows/deploy-stack.yml permissions: contents: read pull-requests: write with: action: teardown + stack_name: preview-pr-${{ github.event.pull_request.number }} + hostname: pr-${{ github.event.pull_request.number }}.preview.pubstar.org + env_file: .env.preview.enc + stack_file: stack.preview.yml + uses_gateway: true + ssh_host_secret: SSH_HOST_PREVIEW secrets: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_USER: ${{ secrets.SSH_USER }} - SSH_HOST_PREVIEW: ${{ secrets.SSH_HOST_PREVIEW }} + SSH_HOST: ${{ secrets.SSH_HOST_PREVIEW }} GHCR_USER: ${{ secrets.GHCR_USER }} GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} diff --git a/.github/workflows/on_tag.yml b/.github/workflows/on_tag.yml new file mode 100644 index 0000000000..19b4f841de --- /dev/null +++ b/.github/workflows/on_tag.yml @@ -0,0 +1,94 @@ +name: Release & deploy + +on: + push: + tags: + - "v[0-9]*.[0-9]*.[0-9]*" + +permissions: + id-token: write + contents: write + packages: write + pull-requests: write + +concurrency: + group: deploy-prod-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + validate-tag: + runs-on: ubuntu-latest + outputs: + prerelease: ${{ steps.meta.outputs.prerelease }} + steps: + - name: Validate semver and detect pre-release + id: meta + run: | + tag="${{ github.ref_name }}" + + if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc|experimental)\.[0-9]+)?$ ]]; then + echo "::error::Tag '$tag' does not match semver format (vMAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH-{alpha,beta,rc,experimental}.N)" + exit 1 + fi + + if [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "prerelease=false" >> $GITHUB_OUTPUT + else + echo "prerelease=true" >> $GITHUB_OUTPUT + fi + + build-all: + needs: validate-tag + permissions: + contents: read + packages: write + uses: ./.github/workflows/ghcr-build-all.yml + with: + image_tag: ${{ github.ref_name }} + publish_latest: ${{ needs.validate-tag.outputs.prerelease == 'false' }} + secrets: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + + create-release: + needs: validate-tag + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + flags="--generate-notes" + + if [[ "${{ needs.validate-tag.outputs.prerelease }}" == "true" ]]; then + flags="$flags --prerelease" + fi + + gh release create "${{ github.ref_name }}" $flags + + deploy-prod: + needs: + - validate-tag + - build-all + if: needs.validate-tag.outputs.prerelease == 'false' + permissions: + contents: read + uses: ./.github/workflows/deploy-stack.yml + with: + action: deploy + image_tag: ${{ github.ref_name }} + stack_name: pubstar + hostname: ${{ vars.PROD_HOSTNAME }} + env_file: .env.enc + stack_file: stack.yml + uses_gateway: false + ssh_host_secret: SSH_HOST_PROD + secrets: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST: ${{ secrets.SSH_HOST_PROD }} + GHCR_USER: ${{ secrets.GHCR_USER }} + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} diff --git a/core/prisma/seeds/ponies.snippet.html b/core/prisma/seeds/ponies.snippet.html new file mode 100644 index 0000000000..153dc062f8 --- /dev/null +++ b/core/prisma/seeds/ponies.snippet.html @@ -0,0 +1,2871 @@ +
+ +
+

+

+

Take home message

+

+ The collapse of the osteal anchor of these osteochondral implants affected the integration of + the construct with the native tissue and seriously compromised the mechanical stability. This + precluded drawing firm conclusions about the potential for in vivo cartilage repair of + the chondral compartment, consisting of bone morphogenetic protein-9 (BMP-9) stimulated + progenitor cells. Collapse occurred because bone regeneration competent materials, which had + shown success in a different anatomical location, were unable to correctly anchor the implant in + the complex biomechanical environment of the joint. The imperfect dimensional match, originating + from the intrinsic variability of the 3D printing process, had a much bigger influence than in + previous studies. When combined with the brittle nature of the material, the implant was unable + to withstand the complex loading of the knee joint. +

+

Purpose

+

+ The current study aimed at evaluating a cell-laden and a cell-free version of an osteochondral + composite scaffold for cartilage repair that consisted of an in vivo proven osteogenic + bone scaffold for the osteal compartment and made use of a novel interface for the connection of + the chondral and osteal compartments. +

+

Introduction

+

+ Focal cartilage damage is a major challenge in human healthcare since it leads to an increased + risk of developing early osteoarthritis !unsupported node 'citation'!. Most of + the available repair approaches are palliative with limited alleviation time, generating fibrous + tissue with reduced mechanical strength !unsupported node 'citation'!. There + are no effective treatments that can fully restore the anatomical structure and function of + focal cartilage defects. This unmet clinical need drives the ongoing quest for regenerative + medicine and tissue-engineering approaches for articular cartilage repair + !unsupported node 'citation'!. +

+

+ Many new promising technologies + !unsupported node 'citation'!!unsupported node 'citation'! are currently being + developed and tested with the aim of finding an implant that is effective in facilitating + regeneration of cartilage. Given the difficulties associated with the fixation of chondral + constructs in the joint + !unsupported node 'citation'!!unsupported node 'citation'!, an alternative + approach is the use of composite osteochondral constructs composed of distinct osteal and + chondral compartments that can be surgically press-fitted into suitably prepared defects, + thereby avoiding the risk of dislodgement !unsupported node 'citation'!. + However, the latter approach still faces many challenges, including design and optimization of + the osteal compartment to act as an anchor for the overlying chondral compartment, production of + a firm and durable connection between osteal and chondral compartment + !unsupported node 'citation'!, and optimization of the composition and + structure of the chondral compartment + !unsupported node 'citation'!!unsupported node 'citation'!. +

+

+ To address those challenges, biomaterials that hold the potential for facilitating + osteoregeneration within the osteal compartment were recently developed and investigated. First, + 3D printed brushite-based scaffolds have been shown to be effective in promoting new bone growth + after 6 months in an equine model that used the tuber coxae as implantation site + !unsupported node 'citation'!. However, these materials are usually processed + using aggressive acidic treatments, precluding the direct incorporation of cells and/or some + types of polymer during the fabrication phase. Therefore, an apatite-based scaffold that could + harden under physiological conditions into a calcium-deficient hydroxyapatite (CDHA) was + developed. This material had been shown to be effective in a 7-month long + in vivo study, upon implantation in a critical size defect in the tuber coxae of + horses. That study was performed to compare two sophisticated architectures with constant and + gradient pore size, respectively. The material was shown to facilitate excellent new bone + formation, particularly when using the scaffold with constant pore size + !unsupported node 'citation'!. This showed the potential for using such + material as an osteal anchor of a tissue-engineered osteochondral graft. +

+

+ Another major challenge is the connection between the osteal and chondral compartments of the + tissue-engineered osteochondral graft when using cell-friendly materials of strongly different + mechanical characteristics. Recently, a technique for attaching the chondral compartment to the + osteal compartment using melt electrowriting (MEW) was developed + !unsupported node 'citation'!, in which MEW fibers of the chondral compartment + were partially incorporated into the slowly setting apatite-based osteal compartment, thereby + binding the two compartments together. This strategy allows both for optimizing the mechnical + properties of the MEW-reinforced chondral compartment, and for integrating the chondral and + osteal compartments. +

+

+ Regarding the seeding of regeneration-competent cells within the chondral compartment, articular + cartilage-derived progenitor cells have been recently identified and characterized in both + humans and horses as a distinct cell population that has the potential for cartilage repair + !unsupported node 'citation'!!unsupported node 'citation'!. This potential was + further shown to be retained in combination with biomaterials + !unsupported node 'citation'!!unsupported node 'citation'!, making this cell + type a promising candidate for a comprehensive regenerative approach. Additionally, it was + recently discovered that supplementation with BMP-9 during in vitro culture of ACPCs + resulted in higher expression of gene-markers related with hyaline-like extracellular matrix + production, compared to supplementation with transforming growth factor (TGFβ\beta), a more commonly used growth factor in cartilage tissue engineering + !unsupported node 'citation'!. This observation sparked interest in further + investigation of the potential of BMP-9-stimulated ACPCs for cartilage repair in vivo. +

+

+ The current study aimed at evaluating an osteochondral composite scaffold for cartilage repair. + These constructs were composed of a combination of a previously proven osteogenic CDHA 3D + printed scaffold for the osteal compartment, onto which a chondral compartment composed by MEW + micro-fibrous meshes is tightly anchored. For the chondral compartment, an experimental group in + which the MEW structure was seeded with ACPCs that had been stimulated for 28 days with BMP-9 + before implantation !unsupported node 'citation'!, was compared with an implant + featuring a non-filled, cell-free MEW cartilage scaffold as control. It was hypothesized that 1) + the CDHA scaffold would show comparable performance in the horse when implanted in the + subchondral bone as in the tuber coxae in terms of firmly anchoring in the surrounding tissue + and inducing bone growth; 2) the novel interface would provide a lasting connection between the + osteal and chondral compartments of osteochondral graft; and 3) the engineered chondral + compartment of the osteochondral graft containing the stimulated ACPCs would outperform the + cell-free structures in terms of in vivo cartilage matrix production (specifically, + amount and density of type II collagen and GAGs, with resulting mechanical properties as close + as possible to those of native, healthy cartilage). +

+

Materials and Methods

+

Experimental design

+

+ To assess the performance of integrated 3D printed osteochondral grafts that contained a + cell-laden or a cell-free chondral compartment, the constructs were orthotopically implanted in + a large animal model. Eight Shetland ponies (female, age + 4−124-12 + years, weight + 149−217kg149 - 217 kg + (166±29kg166 pm 29 kg)) were used and samples were implanted in the medial femoral ridge in the stifle joints. + Healing was monitored for 6 months, after which the animals were humanely euthanized. The study + was approved by the ethical and animal welfare body of the Utrecht University (Approval nr. + AVD108002015307 WP23). +

+

+ Ponies were housed in individual boxes and fed a limited ration of concentrates together with + hay for maintenance and free access to water. Quantitative gait analysis and radiographic + examination of the stifle joints were performed before surgery for baseline values. + Post-operatively, the animals were kept stabled for 6 weeks with daily monitoring of vital + signs, lameness checks at walk, and examination of the operated joints for swelling or other + signs of inflammation. In week 5 and 6, they were hand-walked for 10 minutes twice daily and + from week 7 on, they were kept at pasture. Quantitative gait analysis and radiographic exams + were performed at 3 weeks, 3 months, and 6 months post-operatively. After 6 months, ponies were + humanely euthanized for harvesting samples for both quantitative and qualitative analyses. The + timeline of the experiment is represented in Figure 1. +

+
+ +
+ Flow chart representing timeline of the experiment including health monitoring at each phase + of the experiment. +
+
+

Fabrication of construct

+

+ Microfiber meshes were produced from medical-grade PCL (Purasorb® PC 12 Corbion PURAC, The + Netherlands) by using MEW technology as previously described + !unsupported node 'citation'!. The meshes were produced by horizontally + patterning the microfiber (diameter + =10μm= 10 mu m) to form continuously uniform square spacing (300x300μm300x300 mu m) and vertically stacking the same pattern until reaching + 1300μm1300 mu m + in total thickness. This structure was achieved by printing with a temperature of 90C, a + pressure of 1.25 bar, voltage of 10 kV, and collector velocity of + 15mm⋅sec−115 mmcdot sec^{-1}. Additionally, printing was performed at ambient temperature (22−24°C22 - 24 degree \text{C}) with a humidity between + 30−50%30 - 50%. Subsequently, PCL microfiber meshes were hydrolyzed by soaking them in sodium hydroxide (1M + NaOH) for 15 minutes and washed in Milli Q water for 10 minutes 4 times. Finally, sterilization + was carried out by immersion of the mesh in + 70%70% + ethanol for + 1515 + minutes, followed by air-drying in a sterile cabinet until use. +

+

+ Printable calcium phosphate (PCaP) paste was prepared as earlier described + !unsupported node 'citation'!. In short, + 2.2g⋅ml−12.2 gcdot ml^{-1} + of alpha-tri calcium phosphate (αalpha-TCP, average particle size + =3.83μm= 3.83 mu m, Cambioceramics, Leiden, the Netherlands) and + 0.13g⋅ml−10.13 gcdot ml^{-1} + of nano-hydroxyapatite (nano-HA, particle size + =200nm=200 nm, Ca5(OH)(PO4)3, Sigma-Aldrich) were mixed with + 40%w⋅v−140% wcdot v^{-1} + poloxamer solution (Pluronic® F-127, Sigma-Aldrich). + αalpha-TCP and nano-HA powder were disinfected with UV-light for 1 hour before mixing. The poloxamer + solution was disinfected by filtration through a + 0.22μm0.22 mu m + sterile filter (Millex®-GS). This paste was loaded to a cartridge and kept at + 4°C4degree \text{C} + until use. +

+

+ Osteochondral constructs were produced by combining the PCL microfiber mesh and the PCaP paste + to form the reinforcement of the chondral compartment and the biomimetic bone compartment, + respectively. Fabrication was performed by directly depositing the PCaP paste (approximated + strand diameter + =250μm= 250 mu m) onto the hydrolyzed MEW mesh (Figure 2). Eighty percent of the + mesh thickness was set as the initial height for depositing the first of non-macroporous PCaP + layer, as this proved to be the height that did not damage the mesh structure and ensured an + optimal integration between the bone compartment and the chondral compartment. The first two + layers of PCaP were deposited without macro-spacing, to mimic the subchondral bone plate, and + followed by layers with designed macro-spacing of + 700μm700 mu m + to mimic the cancellous bone section (diameter + =6mm= 6 mm, height + =5mm= 5 mm). +

+

+ After finishing the fabrication process, the osteochondral constructs were allowed to set at 37C + under saturated relative humidity to form a solid, biomimetic bone compartment through + conversion of the PCaP composite to CDHA. Finally, the osteochondral constructs were disinfected + in + 70%70% + ethanol and exposed to UV-light for + 11 + hour, prior to seeding of cells. +

+

In Vitro pre-culture

+

+ Allogeneic articular cartilage progenitor cells (ACPCs) were obtained as previously described + !unsupported node 'citation'!!unsupported node 'citation'! from animals that + were euthanized at the Utrecht University Veterinary Hospital for causes unrelated to disease or + impairment of the musculoskeletal system and whose remains were donated for research purposes. + Briefly, hyaline cartilage was collected in a sterile fashion, minced, and digested at + 37°C37degree \text{C} + with + 0.2%w⋅v−10.2% wcdot v^{-1} + pronase solution for + 22 + hours, followed by + 1212 + hours in + 0.075%w⋅v−10.075% wcdot v^{-1} + collagenase solution. ACPCs were then selected using a fibronectin adhesion assay + !unsupported node 'citation'!. Cells were expanded in culture and stored in + liquid nitrogen until further use. After thawing, cells were expanded until passage 3 prior to + their use for the experiment. +

+

+ The constructs made of the combined CDHA and MEW meshes were disinfected in ethanol and exposed + to UV-light for 1 hour as mentioned above. To avoid any pH changes that might affect the cells, + the constructs were subsequently washed 3 times for 10 minutes with PBS and then immersed for 1 + week in cell culture medium consisting of Dulbecco’s Modified Eagle Medium/Nutrient Mixture F-12 + (DMEM/F-12, 11320033, Gibco, The Netherlands) supplemented with + 10%v⋅v−110% vcdot v^{-1} + heat-inactivated fetal calf serum (FCS, Gibco, The Netherlands), + 0.2mM0.2 mM + L-ascorbic acid 2-phosphate (Sigma), + 1%1% + MEM Non-Essential Amino Acids Solution (11140035, Gibco, The Netherlands) and + 100U⋅mL−1100 Ucdot mL^{-1} + penicillin with + 100μg⋅mL−1100 mu gcdot mL^{-1} + streptomycin (Life Technologies, The Netherlands). Media were refreshed every 2-3 days. +

+

+ On the day of seeding, medium was refreshed 2 hours before seeding and scaffolds were placed + inside a custom-made polydimethylsiloxane (PDMS) ring (Figure 2) that + prevented overflow of the cell suspension from the cartilage compartment to the bone scaffold. + Ten million cells were suspended in + 100μl100 mu l + of medium and seeded on top of the constructs. The cell suspension was left to settle at the + bottom of the cartilage part for 30 minutes. Afterwards, + 2ml2 ml + of cartilage medium supplemented with + 100ng⋅ml−1100 ngcdot ml^{-1} + of BMP-9 (PeproTech, The Netherlands) was carefully added to the well. The seeded constructs + were cultured for 4 weeks prior to implantation, refreshing the medium 3 times a week. +

+
+ +
+ Schematic picture representing the fabrication process of the tissue engineered + osteochondral constructs. + Fabrication techniques and processes for forming osteochondral graft (A), Material + composition of osteochondral graft and process during in vitro preculture (B). +
+
+

Surgical procedure

+

+ Ponies were premedicated with detomidine (intravenous (IV), + 10μg⋅kg−110 mu gcdot kg^{-1}) and morphine (IV, + 0.1mg⋅kg−10.1 mgcdot kg^{-1}) and anesthesia was induced with midazolam (IV, + 0.06mg⋅kg−10.06 mgcdot kg^{-1}) and ketamine (IV, + 2.2mg⋅kg−12.2 mgcdot kg^{-1}). Anesthesia was maintained with isoflurane in oxygen together with continuous rate infusion + of detomidine (IV, + 10μg⋅kg−110 mu gcdot kg^{-1}/h) and ketamine (IV, + 0.5mg⋅kg−10.5 mgcdot kg^{-1}/h). Meloxicam (IV, + 0.6mg⋅kg−10.6 mgcdot kg^{-1}), morphine (epidural injection, + 0.1−0.2mg⋅kg−10.1 - 0.2 mgcdot kg^{-1}) and ampicillin (IV, + 10−15mg⋅kg−110 - 15 mgcdot kg^{-1}) were administered pre-operatively as analgesic medication and antibacterial preventative + therapy, respectively. +

+

+ The medial femoral ridge of the stifle joint was exposed by arthrotomy and an osteochondral + lesion (diameter + =6mm= 6 mm, depth + =6mm= 6 mm) was surgically created using a power drill. The surgical area was flushed by saline for + cooling and removal of debris. Cell-laden constructs were implanted press-fit in a randomly + chosen hind limb, with the cell-free control being implanted in the contralateral limb. After + closing the arthrotomy wound in four layers in routine fashion, procaine penicillin was + administered (Procapen, intramuscular (IM), + 20mg⋅kg−120 mgcdot kg^{-1}). Post-operatively, nonsteroidal anti-inflammatory medication (metacam, per os (PO), SID, + 0.6mg⋅kg−10.6 mgcdot kg^{-1}) was administered for 5 days and opioids (tramadol, PO, BID, + 5mg⋅kg−15 mgcdot kg^{-1}) were administered for + 22 + days. +

+

Gait analysis

+

+ The ponies were trained on a treadmill prior to the study using a standard protocol for + treadmill habituation. Twenty-eight spherical reflective markers with a diameter of + 24mm24 mm + (topline) and + 19mm19 mm + (elsewhere) were attached with double-sided tape and second glue to anatomical landmarks (Figure + 3). Kinematic data were collected on a treadmill (Mustang, + Fahrwangen, Switzerland) at trot using six infrared optical motion capture cameras (ProReflex, + Qualisys, Gothenburg, Sweden) recording at a frame rate of + 200Hz200 Hz + for + 3030 + seconds at each session to obtain a sufficient number of strides. +

+

+ To process the data, the reconstruction of three-dimensional coordinates of each marker was + automatically calculated by Q-Track software (Qtrack, Qualisys, Gothenburg, Sweden). Each marker + was identified and labelled using an automated model (AIM model) and manual tracking. Raw data + of the designated markers were exported to Matlab (version 2018a, Niantics, California) for + further analysis using custom written scripts. For each stride, two symmetry parameters were + calculated using the vertical displacement of the head and pelvis (tubera sacrale) markers. For + each stride, the differences between the two vertical displacement minima of the head + (MinDiffhead) and pelvis (MinDiffpelvis) were calculated. Using the markers, limb-segments were + formed and angles between these limb-segments were calculated. The difference between the + maximal and minimal angle was defined as the range of motion (ROM) of a joint. For each + timepoint, the mean value of all strides for each parameter was calculated. +

+
+ +
+ Schematic picture representing location of the markers for gait analysis. +
+
+

Radiographic examination

+

+ Stifles were radiographed in 3 projections: lateromedial, craniolateral-caudomedial oblique and + caudo-cranial projection using standard machine settings before surgery (baseline), at 3 weeks + postoperatively and at 6 months, just before euthanasia. +

+

Euthanasia and sample harvest

+

+ After 6 months, animals were euthanized by induction with Midazolam (IV, + 0.06mg⋅kg−10.06mgcdot kg^{-1} + body weight) with ketamine IV, (2.2mg⋅kg−12.2 mgcdot kg^{-1} + body weight) and subsequent administration of sodium pentobarbital (IV, + 1400mg⋅kg−11400 mgcdot kg^{-1} + body weight). Next, the stifle joint was exposed, and gross assessment of the medial trochlear + ridge was performed, focusing on the degree of filling of the defect, the integration of repair + tissue with the surrounding native tissue and the surface quality of the repair tissue. + Subsequently, the entire osteochondral area containing the constructs was harvested for further + analyses with the aid of a surgical bone saw. Harvested tissues were initially kept in + sterilized PBS for micro-computed tomography (micro-CT) scanning, biomechanical analyses and for + collecting tissue from the chondral compartment of the implant for biochemical analyses. After + this, all tissues were fixed in 4% formaldehyde for subsequent histological processing. +

+

Biomechanical evaluation

+

+ The compressive properties of the chondral compartment of the defect site, the adjacent + surrounding native cartilage and the more distant surrounding native cartilage (5−10mm5 - 10 mm + from the boundary of the defect) (N=7N=7 + for cell-laden constructs and + N=7N=7 + for cell-free constructs) were evaluated with a dynamic mechanical analyzer (DMA, DMA Q800, TA + instrument) equipped with a custom-size compressing probe (diameter + =2mm= 2 mm). A ramp force of + 0.250N⋅min−10.250 Ncdot min^{-1} + was applied until reaching + 2.0N2.0 N, to limit the deformation of sample to values below + 200μm200 mu m. Compression modulus was calculated as the slope of the stress-strain curve in the range + between + 10−12%10-12 % + strain. +

+

Biochemical evaluation

+

+ Firstly, biochemical analyses were performed on supplemental pre-implantation constructs + (N=3N=3) that had been prepared in the same batch as the constructs that were later implanted. The + chondral compartments of 28-day cultured constructs were removed and freeze-dried. Next, dry + samples were digested in papain (Sigma Aldrich) at + 60°C60degree \text{C} + overnight. DNA, sulphated glycosaminoglycan (sGAG), and alkaline phosphatase (ALP) content were + quantified by performing the Quan-iT-Picogreen-dsDNA-kit assay (Molecular Probes, Invitrogen, + Carlsbad, USA), the dimethylmethylene blue assay (DMMB, Sigma-Aldrich, The Netherlands) and the + p-nitrophenyl phosphate assay (SIGMAFAST, Sigma-Aldrich), respectively. +

+

+ Secondly, tissue fractions that were collected from the chondral compartments of harvested + implants (N=6N=6 + for cell-laden constructs, + N=7N=7 + for cell-free constructs) were kept at + −80°C-80degree \text{C}, followed by lyophilization. Collagen content was quantified using an hydroxyproline assay + (L-Hydroxyproline, Merck KGaA), and the sGAG and DNA quantification was performed as described + above. +

+

Microcomputed tomography

+

+ Microcomputed tomography was employed for the quantitative analysis of the bone compartments + from the harvested osteochondral lesions (N=7N=7 + for cell-laden constructs, + N=7N=7 + for cell-free constructs). Six freshly made osteochondral grafts were scanned in a micro-CT + scanner (Quantum FX-Perkin Elmer) to quantify the initial volume of PCaP material, + pre-operatively. The post-mortem harvested tissue containing the defect area and the surrounding + native tissue were similarly scanned (voltage + =90kV= 90 kV, current + =200μA= 200 mu A, voxel size + =30μm3= 30 mu m^{3} + and total scanning time + =3minutes= 3 minutes). Subsequently, the 3D-reconstructed images were processed and analyzed using image J software + !unsupported node 'citation'! and Bone J plugin + !unsupported node 'citation'!. Two-dimensional regions of interest (ROIs) were + selected in an axial plane at the boundary between the defect and the surrounding native tissue + and interpolated to form a three-dimensional volume of interest (VOI). Thresholding was + performed to separately select areas of ceramics and newly formed bone respectively for further + calculation. Then, the percentages of mineralized newly formed bone, of non-mineralized tissue + and of remaining ceramics, including the percentage of ceramics volume loss, were quantified. +

+

Histological evaluation

+

+ Firstly, supplemental pre-implantation constructs (N=3N=3) that had been prepared in the same batch as the ones that later were implanted were fixed in + 4%4% + formaldehyde. After decalcification in + 0.5M0.5M + Ethylenediaminetetraacetic acid (EDTA) disodium salt (pH=8pH = 8) for + 11 + day, tissues were dehydrated with graded ethanol series, cleared in xylene, and embedded in + paraffin. Paraffin embedded tissues were sliced to + 5μm5 mu m + sections. Histochemical evaluation of GAG was done by safranin-O / fast green staining. Type I + collagen (primary antibody: monoclonal antibody EPR7785, + 1.083mg⋅ml−11.083 mgcdot ml^{-1}, Abcam) and type II collagen (primary antibody: monoclonal antibody II-II6B3, + 0.06mg⋅ml−10.06 mgcdot ml^{-1}, DSHB) were visualized by immunohistochemistry. +

+

+ The tissues that were harvested after + 66 + months (N=7N=7 + for cell-laden constructs, + N=7N=7 + for cell-free constructs) were kept in + 4%4% + formaldehyde and then decalcified in + 0.5M0.5M + EDTA disodium salt (pH=8pH = 8) for + 2424 + weeks. Decalcified tissues were cut into two halves before processing to enable visual + inspection of the center of the lesion. Tissues were dehydrated with graded ethanol series, + cleared in xylene and finally embedded in paraffin. Paraffin embedded tissues were sliced to + 5μm5 mu m + sections. For assessment of morphology and cell distribution, hematoxylin-eosin staining + (Mayer’s haematoxylin, Merck 109249 and eosin, Merck 115935) was performed. GAG and collagen + alignment were assessed after safranin-O / fast green and picrosirius red staining, + respectively. Types I collagen and type II collagen were visualized by immunohistochemistry, as + described above. For immunohistochemistry, all samples were treated according to previously + published protocols !unsupported node 'citation'!. Stained histological slides + were imaged using a light microscope (Olympus BX51, Olympus Nederland B.V.), equipped with a + digital camera (Olympus DP73, Olympus Nederland B.V.). To observe the picrosirius red stained + slides, a polarizer was also mounted to the light microscope. +

+

Statistical analysis

+

+ Normality of distribution of the data was assessed from skewness, kurtosis, and Q-Q plots. + Results were reported as mean + ±pm + standard deviation. Wilcoxon signed rank tests were used to analyze the biochemical, + biomechanical, and micro-CT data. Statistical significance was set at + p=0.05p = 0.05. All tests were performed using Matlab (version R2018b, The MathWorks, Inc.). +

+

+ To evaluate the gait parameters, stride-level data were analyzed with R software (version 3.6.0, + R Core Team, 2019), using package NLME (version 3.1-137) for mixed modelling. Dependent + variables were investigated for normality using normal probability plotting and examining for + skewness and kurtosis. If not normally distributed, data were transformed to permit linear mixed + modeling. The random effect was subject and timepoint was the fixed effect. Significance was set + at p < 0.05 and p-values were corrected using the false discovery rate method. Residual plots + were checked for heteroscedasticity versus the outcome, as well as for normality in Q-Q plots. +

+

Results

+

In vitro

+

+ After 4 weeks of preculture, macroscopic characterization of tissue formation and hyaline-like + extracellular matrix production were assessed both quantitatively and qualitatively within the + chondral compartment of the cell-laden osteochondral constructs. The BMP-9 stimulated ACPCs + meant to colonize the MEW scaffolds formed neo-tissue that had grown into a disc shape after 3 + weeks of culture. During the 4th week of culture, outgrowth from the MEW meshes was + observed (Figure 4A) from this construct. Cell-free constructs did + not change after immersion in growth factor-free medium for 4 weeks (Figure + 4B). Biochemical analyses of the chondral compartment of the + cell-laden constructs were performed to quantify matrix production of stimulated cells toward + chondrogenic lineage and osteogenic lineage, which revealed the presence of GAGs + (GAGs⋅cdot + DNA-1 was + 199.7±67.7μg⋅μg−1199.7 pm 67.7 mu gcdot mu g^{-1}) and ALP activity (ALP·DNA-1 was + 3702±2111U⋅μg−13702 pm 2111 Ucdotmu g^{-1}), respectively. Safranin-O staining and type II collagen immunohistochemistry were also + performed to visualize hyaline-liked matrix production from stimulated cells, which revealed + abundant deposition of GAGs and type II collagen within the constructs after 3 weeks of + in vitro culture (Figure 4C), showing that the chondral + compartments of the constructs (meant for subsequent implantation) were filled with a hyaline + cartilage-like tissue. No preferential alignment of the collagen fibers could be observed. +

+
+ +
+ Representative pictures of cell-laden and cell-free osteochondral constructs at the + time of implantation. + Cell-laden (A) and cell-free constructs (B) at the time of implantation. Positive safranin-O + staining indicating the presence of glycosaminoglycans (pink = positive), positive type II + collagen (brown = positive) and negative type I collagen (brown = positive) + immunohistochemistry were observed in the chondral compartment of the cell-laden constructs + before implantation (C). +
+
+

Evaluation during surgical implantation

+

+ Both cell-laden and cell-free constructs were press-fit implanted into the surgically created + defect sites. During this procedure, the slightly irregular outer edge of the osteal part of the + construct hampered easy sliding of the construct down into the defect and some fragmentations of + the edges of the bioceramic scaffold was observed during the procedure. This was similar for the + cell-laden and cell-free constructs, which had identical osteal parts (Figure + 5). Further, of some cell-laden constructs, the surface of the + chondral compartment was not level over the entire circumference with the surrounding native + cartilage after press-fitting into the defect site. +

+
+ +
+ Representative pictures show white fragments of broken ceramic after press-fitting + cell-laden and cell-free osteochondral constructs into the defect. + Black arrows indicate the position of some visible bioceramic fragments. White arrows + indicate protrusion of the chondral compartment +
+
+

Post-operative clinical monitoring

+

+ After surgical implantation, the animals were checked clinically for physical appearance and + vital signs on a daily basis. All ponies recovered well from anesthesia after surgery and passed + uneventfully through the rehabilitation period without any abnormalities in body temperature or + behavior, with good weight-bearing on all operated limbs and no clinical signs of lameness + during the entire period, with the exception of a single pony that developed severe lameness at + 10 weeks after surgery. This pony was treated with anti-inflammatory medication and examined + radiographically, which revealed extensive osteolysis around the created lesion. Because of + persistent discomfort, the pony was euthanized at 12 weeks after surgery. Therefore, it was + excluded from all analyses. +

+

Gait analysis

+

+ Objective gait analysis was used to check for lameness or other signs of dysfunction of the + musculoskeletal system. Objective data retrieved before implantation and at the end of the + experiment were assessed for relevant parameters, including symmetry parameters and limb + parameters. +

+

Symmetry parameters

+

+ Front and hind limb lameness were analyzed through evaluation of the symmetry parameters of the + head (MinDiff Head (Figure 6A)) and of the pelvis (MinDiff Pelvis + (Figure 6B)). These values reflect the differences in minimal + vertical displacement with a negative MinDiff indicating a left-sided asymmetry and a positive + MinDiff a right-sided asymmetry. In the treated ponies (except for the case referred to above + that was euthanized), for both the head and the pelvis, there was no clear pattern in the + direction of the asymmetries between baseline and endpoint and those differences between + baseline and endpoint were minimal and statistically not significant. Therefore, symmetry + measures could not discriminate between cell-laden and cell-free constructs. Further, there was + also no clear effect of timepoint on pelvis roll and pelvis yaw range of motion (Supplementary + Figure 12), however, pelvis pitch range of motion (ROM) (Figure + 6C) decreased for all subjects with almost 20% over time + (Supplementary Table 1). +

+

Limb parameters, effects of time

+

+ There was a significant effect of time for the height the toe was lifted from the surface during + the swing phase of the limb that decreased significantly in the cell-free treated limbs, but not + in the limbs treated with cell-laden constructs (Supplementary Table + 1). The only other significant effect of time was a decrease in the + extension of the metacarpophalangeal joint of the forelimb ipsilateral to the hind limb that had + been treated with cell-laden constructs, indicating unloading of that forelimb (Supplementary + Table 1). +

+

+ Limb parameters, differences between cell-laden and cell-free at endpoint +

+

+ There were no significant differences between any of the cell-laden and cell-free limb + parameters at the end of the experiment. Results from the linear mixed model are shown in + Supplementary Table 2. +

+
+ +
+ Gait analysis: Symmetry parameters. Symmetry data of the head (A) and pelvis (B) show + no consistent differences over time. However, pelvis pitch decreased consistently in all + individuals (C). +
+
+

Radiographic examination

+

+ Healing progression within the osteal compartment of the implanted osteochondral constructs was + followed up non-invasively through radiographic examination. On the radiographs taken at + baseline, 3, and 6 months, no obvious abnormalities in term of the architecture of the + surrounding native tissue were detected, other than the defects that had been created. This was + with the exception of the pony that developed severe lameness. In that animal, severe osteolysis + was noted at the implantation site 3 months after the implantation (Supplementary Figure + 13). +

+

+ Post-mortem macroscopic evaluation of the repair tissue +

+

+ Macroscopic characteristics, for instance, color, appearance, and filling level of the lesion, + were observed and documented before harvesting tissue sample for further analyses. After 6 + months, macroscopic evaluation revealed that the defects were filled with repair tissue that in + all cases did not fill the entire defect and remained lower than the level of the surrounding + native cartilage in both cell-laden and cell-free treatments (Figure + 7A). The color of the repair tissue was variable (from reddish, to + yellow and translucent) within the different treatments (Figure 7B). + In some cases, ceramic fragments could be observed within the repair tissue of the chondral + compartment. +

+
+ +
+ Macroscopic appearance of the repair tissue and surrounding native tissue in all + individual animals at euthanasia. + Macroscopic appearance of the defect site and surrounding femoral ridge (A). Close-ups of + macroscopic appearance at the defect site (B). +
+
+

+ Biochemical analyses of repair tissue within the chondral compartment +

+

+ The deposition of GAGs and collagen, the two main elements that compose cartilage extracellular + matrix, were quantified within the chondral compartment of the osteochondral graft 6 months + after implantation. There were no significant differences in either GAGs (cell-laden: + 30.46±15.95μg⋅μg−130.46 pm 15.95 mu gcdotmu g^{-1}, cell-free: + 24.44±15.31μg⋅g−124.44 pm 15.31 mu gcdot g^{-1}) or collagen expressed per DNA (cell-laden: + 79.66±91.21μg⋅μg−179.66 pm 91.21 mu gcdotmu g^{-1}, cell-free: + 134.21±153.73μg⋅μg−1134.21pm 153.73 mu gcdotmu g^{-1}) between the chondral compartments of the cell-laden and cell-free constructs (Figure + 8A, 8B and Supplementary Figure 14). However, + all values were substantially lower than those from native cartilage (Figure + 8, grey dotted line) that was harvested distantly from the defect + site. +

+

!unsupported node 'iframe'!

+
+ +
+ Biochemical analysis from chondral compartment at the defect site after an implantation + for 6 months. + Quantitative analysis of GAG·DNA-1 between cell-laden and cell-free treatments + (A). Quantitative analysis of collagen⋅ DNA-1 between cell-laden and cell-free + treatments (B) (x = mean). Grey dotted line indicates level in native cartilage. +
+
+

+ Biomechanical properties of the repair tissue within the chondral compartment +

+

+ Compressive strength of the chondral compartment was assessed and compared in three different + locations: the defect site, adjacent surrounding native tissue, and distant surrounding native + tissue (Figure 9A), the latter two as control measurements from + healthy cartilage tissue. There were no significant differences in the Young’s modulus of the + chondral compartment between cell-laden (0.31±0.13MPa0.31 pm 0.13 MPa) and cell-free (0.42±0.19MPa0.42 pm 0.19 MPa) constructs (Figure 9B). This was also true for two sites of the + native cartilage, one close to the border of the defect (cell-laden: + 1.75±0.80MPa1.75 pm 0.80 MPa, cell-free: + 2.22±0.48MPa2.22 pm 0.48 MPa) and one at + 5−10mm5 - 10 mm + from the defect boundary (cell-laden: + 1.86±0.78MPa1.86 pm 0.78 MPa, cell-free: + 2.19±0.77MPa2.19 pm 0.77 MPa) (Figure 9C, 9D). However, the compression modulus of the native + tissue was substantially higher (approximately + 5−65-6-fold) than inside the chondral compartment of the implant. +

+
+ +
+ Compression modulus of the chondral compartment at the defect site and surrounding + native cartilage of harvested samples 6 months after implantation. + Schematic picture demonstrating locations where mechanical properties were analyzed (A). + Compression modulus of the chondral compartment of cell-laden and cell-free constructs at 6 + months (B) and at two sites of the native cartilage, close to the border of the defect (C) + and further away (D). (x = mean). +
+
+

+ Micro-CT evaluation of repair tissue within the osteal compartment +

+

+ Bone healing and integration after was assessed through micro-CT scanning 6 months after + implantation. Micro-CT images showed significant bone loss surrounding the implant in both the + cell-laden and the cell-free groups, which could be visualized as black areas between the porous + bioceramic structure (white) and the surrounding native bone (grey). However, mineralized bone + formation could be visualized in some scaffolds from both groups with an integration to + neighboring native bone (Figure 10A). Statistically, there were no + significant differences in mineralized bone formation (cell-laden: + 6.14%±10.09%6.14% pm 10.09%, cell-free: + 4.73%±4.93%4.73% pm 4.93%) and non-mineralized tissue (cell-laden: + 81.38%±15.37%81.38% pm 15.37%, cell-free: + 74.71%±12.44%74.71% pm 12.44%). However, there was a significant difference in the amount of remaining ceramics between the + two groups (cell-laden: + 12.48%±9.75%12.48% pm 9.75%, cell-free: + 20.56%±10.54%20.56% pm 10.54%(p=0.0313p = 0.0313)) (Figure 10B). In line with this, there was a difference in the + degradation of ceramics in the cell-laden construct versus the cell-free constructs (cell-laden: + 79.02±16.18%79.02 pm 16.18 %, cell-free: + 63.20±13.90%63.20 pm 13.90 %(p=0.0313p = 0.0313)) (Figure 10C). +

+
+ +
+ Representative micro-CT images from the middle of the sagittal plane of the constructs + and quantification from 3D-reconstruction of micro-CT. + Representative micro-CT images from the middle of the sagittal plane of the constructs + (white = ceramics, grey = mineralized tissue, black = non-mineralized tissue) (A). + Quantitative analysis from micro-CT reconstruction showing percentage of mineralized bone + formation, non-mineralized tissue, and remaining ceramics (B). The volume loss of ceramics + was slightly higher in the cell-laden constructs compared to the cell-free ones (C). +
+
+

+ Histological evaluation of the osteochondral repair tissue +

+

+ Histological slides were assessed to identify the composition of the repair tissue matrix + deposited within the defect site. In the chondral compartment, the defect sites of both + cell-laden and cell-free structures were filled with fibrous repair tissue with degenerated and + necrotic superficial surface with minimal inflammatory reaction, as revealed by H&E and + safranin-O staining (Figure 11, Supplementary Figure + 15, Supplementary Figure 17, and Supplementary + Figure 18). Integration at the boundary of the defect between chondral + repair tissue and surrounding native cartilage was observed in both groups. The production of + GAGs, type II collagen, and type I collagen was very limited in the repair tissue in both groups + (Figure 11). The organization of the collagen fibrils in both groups + seemed random, without any hierarchical pattern that could be identified by polarized light + imaging of picrosirius red staining. Additionally, the special distribution of PCL-microfibers, + which had disappeared because of the xylene treatment during sample preparation, was still + traceable within the chondral compartment of both groups (1 out of 7 for cell-laden and 5 out of + 7 for cell-free structure). +

+

+ In the bone compartment, there was positive staining for type I collagen in some scaffolds from + both groups at places where there were islands of new mineralized bone formation. There were + multifocal coalescing spots of inflammatory reaction characterized by macrophages, + multinucleated giant cells, lymphocytes, eosinophils, and plasma cells (Supplementary Figure + 16, Supplementary Figure 17, Supplementary + Figure 18). +

+
+ +
+ Representative histological images from the center part of cell-laden and cell-free + structures after implantation for 6 months. + Safranin-O/fast green (red color = positive) (A, E); Collagen type II (brown color = + positive) (B, F); Picrosirius-red (C, G); collagen type I (brown color = positive) (D, H) of + cell-laden (A-D) and cell-free structures (Scale bar = 1mm). +
+
+

Discussion

+

+ This study aimed to evaluate the efficacy of an engineered osteochondral composite scaffold that + was fabricated by combining a proven osteogenic CDHA scaffold for the osteal compartment with a + novel interface for the connection between the chondral and osteal compartments. For the + chondral compartment, BMP-9 stimulated cell-laden and cell-free constructs were compared. The + cell-laden constructs contained in vitro formed tissue that was rich in GAGs and type + II collagen, obtained by seeding Articular Cartilage Progenitor Cells (ACPCs) and stimulating + them with BMP-9 for 4 weeks prior to implantation. After implantation in an equine osteochondral + defect for 6 months, there was poor chondral repair tissue in both the cell-laden and cell-free + implants. The repair tissue was akin to fibrocartilage and was characterized by the presence of + fibrous tissue with low content of GAGs and type II collagen and a degenerated surface. The CDHA + scaffold had failed to act as an osteal anchor, as evidenced by the radiographical images + showing misalignment and partial collapse of the CDHA construct, the presence of CDHA fragments + within the defect and in the surrounding tissues, and a limited volume of newly formed calcified + bone in the pores of the osteal anchor. +

+

+ In the quest for a method to achieve satisfactory and durable repair of articular cartilage, + several osteochondral grafts that incorporate cells and that were manipulated to optimize + biochemical and biomechanical properties, have been investigated in the past decades + !unsupported node 'citation'!. Articular Cartilage Progenitor Cells have become + a promising cell source due to their ability to retain their chondrogenicity after their + expansion for several passages !unsupported node 'citation'!. Recently, growth + factor BMP-9 was shown to be a potent stimulator of chondrogenic differentiation of this cell + type in vitro !unsupported node 'citation'!. This warranted further + investigations to evaluate the use of BMP-9 stimulated ACPCs for cartilage repair + in vivo. Indeed, the cell-laden chondral compartment showed a high presence of + neo-cartilage extracellular matrix production after pre-culture at the time of implantation, yet + the average GAG content decreased approximately 6.5-fold during the + in vivo implantation period. The GAG content of cell-laden constructs in fact decreased + to the level of the cell-free constructs, suggesting loss or disintegration of the + in vitro formed tissue. Which factor initiated this loss of in vitro formed + tissue remains unclear. A previous study from + !unsupported node 'citation'! demonstrated superior results in using ACPCs for + cartilage repair in an equine model, in comparison with mesenchymal stem cells. However, due to + the use of different materials and cell culture protocols, it is impossible to directly compare + those results with the ones from the current study. Several factors might have been involved in + the deterioration of the chondral compartment in this study, most prominently mechanical + stresses due to the partial failure of the osteal basis and the resulting poor osteointegration + !unsupported node 'citation'!. +

+

+ The nature of the osteal anchor is an important factor when developing tissue-engineered + osteochondral implants. Much work has been done on the development of several types of bone + grafts and many of these are routinely used in clinical settings + !unsupported node 'citation'!, so of the various elements of an osteochondral + implant, the bone part is seemingly the least difficult one. However, the relationship between + the osteal anchor and the quantity and quality of the repair tissue in the chondral compartment + has been the subject of debate !unsupported node 'citation'! and it is still + unclear what osteal anchor would form the best base for facilitating cartilage repair. The exact + same bioceramic material tested in this long-term, orthotopic equine study, had previously been + shown to successfully guide osteoregeneration in the same species, when implanted in the tuber + coxae, an anatomical locus less subject to intense mechanical loads + !unsupported node 'citation'!. Additionally, this previous study also focused + on comparing different pore architecture within the 3D printed bone scaffolds. The scaffold + architecture that led to the highest rate of new bone formation, consisting of a constant pore + size across the sample, was selected for the present study, with the goal of maximizing neo-bone + repair !unsupported node 'citation'!. However, there are two major differences + with the use of the material in the current study. First, in the previous study the material was + implanted in the tuber coxae, which is an orthotopic area but not representative of the + intra-articular environment. Second, in the former study the implant was surrounded by a + cylindrical case made of PCL that served to prevent bone ingrowth from the sides. Without such a + shell of the mechanically deformable PCL in the current study, the surgeon encountered + difficulties during the surgical placement of the implants, provoked by the non-resilience and + brittleness of the CaP-based material, combined with some deviation from an ideal cylindrical + shape of the CDHA implant. This resulted in fragments breaking off from the bioceramic osteal + anchor. In fact, although inadvertently and as a side-effect, this problem of fragment formation + was avoided in the former study when using PCL to encase. Polycaprolactone is deformable + material, and the encasing will have facilitated the sliding of the ceramic implant into the + defect. The duration of both studies was not identical (7 months in the earlier study, 6 months + in the current), which makes direct comparisons between the two impossible. However, there were + clear histological differences with many more multifocal to coalescing inflammatory reactions in + both cell-laden and cell-free implants in the current study, compared to the earlier study, in + which there were very few inflammatory cells visible. This difference is likely due to the + chronic irritation caused by fragments of material and to instability resulting from the + imperfect fit of scaffold within the defect in the current study. +

+

+ Some scaffolds from the current study collapsed and showed misalignment of the CDHA structure + within the defect, with slightly enlarged defect size after an implantation for 6 months (as + evident from the micro-CT analysis). Bone resorption around the implant was found both in the + groups with cell-laden and with cell-free chondral compartment, which infer the effect from the + osteal anchor rather than from the variable within chondral compartment. The circumstances + described above likely resulted in failure to place the implant in a real press-fit fashion and + hence, in the creation of (micro)movement, leading to increasing instability under repetitive + loading together with possible material degradation over time and ensuing osteolysis, as seen + earlier !unsupported node 'citation'!!unsupported node 'citation'!. + Additionally, the gap between the implant and surrounding native tissue due to the imperfect fit + may have allowed for the intrusion of synovial fluid. Contact of synovial fluid with subchondral + bone has been shown to induce osteolysis !unsupported node 'citation'!. In the + few scaffolds that remained in place, the volume of mineralized bone formation was also lower + than what was found in the earlier study, both in cell-laden and cell-free treatments. This is + potentially due to the higher and cyclical loads experienced in the articulating joint compared + to the tuber coxae. Overall, it was not possible to determine a single cause for the failure of + the CDHA scaffolds to act as the anchor of the engineered osteochondral implant, and it is + likely that the limited osteointegration is due to a combination of misalignment after surgery, + mechanical failure under cyclic loading, and synovial fluid infiltration. +

+

+ In earlier studies + !unsupported node 'citation'!!unsupported node 'citation'! similar observations + were made. In those studies, fibrous repair tissue was seen in the chondral compartment, + together with osteolysis and formation of a fibrous interface surrounding the osteal anchor when + tissue-engineered osteochondral grafts were implanted in a load-bearing area for a 12-month + long-term study. It was hypothesized that osteolysis and the fibrous layer surrounding the + osteal anchor led to instability that might have caused the degradation of the newly formed + cartilage-like repair tissue observed at the early of the experiment. Stability of the + osteochondral graft might be affected by multiple parameters including the alignment of an + osteal compartment within the defect and the properties of the materials being used + !unsupported node 'citation'!!unsupported node 'citation'!!unsupported node + 'citation'!!unsupported node 'citation'!, as these might affect stability of the overlying chondral compartment. In the current study, + misalignment and partial collapse of the osteal part of the construct might also be at the basis + of the protrusion of the chondral compartment of some cell-laden constructs and the inconsistent + position of the chondral graft with respect to the surrounding native tissue in both groups. + These conditions may have led to an abnormal load distribution, possibly inducing inferior + biomechanical properties !unsupported node 'citation'!. It is thus clear that + the imperfect implantation had severe repercussions and can be considered a major factor that + affected the chondral compartment and hence the outcome of the study. This effect was noticeable + to the extent that drawing any conclusions about the effect of BMP-9 seeded ACPCs, which was the + principal variable that was to be tested in the study, is not possible. Also, no conclusion + could be reached about the interface between the osteal and chondral compartments that was used + since delocalized MEW-mesh structures were observed in some scaffolds from both groups. This + might be due to misalignments of the osteal compartment as discussed above, to shear forces + during loading, or a combination of both. +

+

+ During the in vivo post-operative monitoring of the animals, the clinical signs were + very mild and far from alarming, except for the single pony that developed severe lameness. + Clinical examinations were performed routinely by experienced veterinary specialists, however, + assessment of locomotion through visual observation alone is subjective and known to have poor + repeatability, especially in mild cases. This is partly due to the inability of human visual + perception to properly distinguish, notice, and quantify differences in locomotion at high + resolution !unsupported node 'citation'!. Therefore, quantitative gait analysis + was employed as an objective and non-invasive assessment. The gait analysis data did not show + many differences with respect to baseline. This may to a certain extent have been related to + methodological factors. During the assessment, ponies were put on a treadmill and they were + imposed the same belt velocity during both measurements. Therefore, the subjects were forced to + trot at the same velocity, ensuring that stride length needed to be maintained. This might be + the reason why there were no differences between timepoints for maximal protraction and + retraction (the limb parameters). However, pelvis pitch range of motion (ROM) decreased for all + subjects with almost 20% over time. This pattern is often seen in case of dysfunction of the + back. The finding may thus be related to earlier observations that bilateral hindlimb lameness + may induce back problems in horses + !unsupported node 'citation'!!unsupported node 'citation'!!unsupported node + 'citation'!. Toe dragging of the lame limb, in which the hoof is lifted less high off the ground, is + another sign of pain !unsupported node 'citation'!. Nevertheless, the overall + impact of the bilateral lesions in the stifle joints was low, as evidenced by the fact that + there was no sign of load redistribution from the hind to the front limbs. If that had been the + case, the subjects would have compensated by displacing their center of mass more to the front, + resulting in more negative angles for forelimb fetlock extension, as fetlock hyper extension + correlates with peak ground reaction force (GRFPeak) + !unsupported node 'citation'!, where less negative angles indicate a lowered + GRFpeak. In fact, only the fetlock angles of the forelimb ipsilateral to cell-laden construct + changed, becoming less negative, hence indicating unloading rather than additional loading + (lower GRFpeak). The reason for this is not clear. +

+

+ It can be concluded that even seemingly minor modifications of a successful implant may have + grave consequences and extrapolation is dangerous in the complex in vivo situation. In + this case, the failure of the osteal compartment of the construct, the use of which seemed + well-backed by solid in vivo data, did not permit drawing conclusions about the + original hypotheses. Given the relatively frequently occurring, rather disappointing results of + in vivo orthotopic testing of promising techniques for joint repair, it may be wise to + put more emphasis on performing pilot experiments before embarking on a full-scale + in vivo study in a large animal experiment + !unsupported node 'citation'!. Functional joint repair remains a huge challenge + that has not been addressed to some satisfying extent during the last decades, despite many + promising approaches. It is likely that the quest for a real solution will go on for some time + by trial and error with more errors to come. Those errors are inevitable and need to be made but + they should take the least possible toll on experimental animals. +

+

Conclusion

+

+ This study presented the results from the evaluation of a cell-laden and cell-free versions of + an osteochondral implant for cartilage repair in a challenging in vivo large animal + model. The osteal anchor of this osteochondral implant, composed of a bioceramic material that + had previously been proven to facilitate mineralized new bone formation in the same species, + failed to perform as an effective fixation with sufficient stabilization for both cell-laden and + cell-free osteochondral implants. This insufficient fixation was evidenced by the extensive + osteolysis, the collapse and misalignment of the osteal anchor, and the limited volume of newly + formed bone. The failure of the bone anchor hindered the evaluation of the two versions of the + chondral compartment for cartilage repair. The study shows that, even after an equivalent + ceramic bone component had shown very satisfactory results in the same species, minor + differences in the implant and a change in testing condition proved to be enough to lead to + completely different results, in this case precluding drawing conclusions about the effect of + the principal variable. This outcome stresses the need of carrying out in vivo pilot + studies under exactly the same conditions before moving into a larger in vivo study. +

+

References

+

+ Abinzano, F., de Ruijter, M., Mensinga, A., Castilho, M., Khan, I., Levato, R., & Malda, J. + (2018, September). + 9-13). Combining melt electrowriting of microfiber meshes with aggregated chondroprogenitor + cells stimulated with BMP-9 to enhance cartilage tissue engineering [Conference presentation + abstract]. Annual Meeting. + https://pure.ulster.ac.uk/ws/portalfiles/portal/71294610/ESB_2018_Abstract_Proceedings_4.pdf +

+

+ Albrektsson, T., Becker, W., Coli, P., Jemt, T., Molne, J., & Sennerby, L. (2019). Bone loss + around oral and orthopedic implants: An immunologically based condition. + Clinical Implant Dentistry and Related Research, 21(4), 786–795. + https://doi.org/10/gg53t3 +

+

+ Alvarez, C. B. G., Bobbert, M. F., Lamers, L., Johnston, C., Back, W., & van Weeren, P. R. + (2008). The effect of induced hindlimb lameness on thoracolumbar kinematics during treadmill + locomotion. Equine Veterinary Journal, 40(2), 147–152. + https://doi.org/10/bsjkv7 +

+

+ Alvarez, C. B. G., Wennerstrand, J., Bobbert, M. F., Lamers, L., Johnston, C., Back, W., & + Weeren, P. R. (2007). The effect of induced forelimb lameness on thoracolumbar kinematics during + treadmill locomotion. Equine Veterinary Journal, 39(3), 197–201. + https://doi.org/10/djxztr +

+

+ Bal, B. S., Rahaman, M. N., Jayabalan, P., Kuroki, K., Cockrell, M. K., Yao, J. Q., & Cook, + J. L. (2010). In vivo outcomes of tissue-engineered osteochondral grafts. + Journal of Biomedical Materials Research Part B: Applied Biomaterials, 93(1), + 164–174. https://doi.org/10/cn94hf +

+

+ Bothe, F., Deubel, A. K., Hesse, E., Lotz, B., Groll, J., Werner, C., Richter, W., & + Hagmann, S. (2019). Treatment of focal cartilage defects in minipigs with zonal + chondrocyte/mesenchymal progenitor cell constructs. + International Journal of Molecular Sciences. + https://doi.org/10/gh63z7 +

+

+ Boushell, M. K., Hung, C. T., Hunziker, E. B., Strauss, E. J., & Lu, H. H. (2017). Current + strategies for integrative cartilage repair. Connect Tissue Research, 58(5), + 393–406. https://doi.org/10/gf5bw7 +

+

+ Bowland, P., Ingham, E., Jennings, L., & Fisher, J. (2015). Review of the biomechanics and + biotribology of osteochondral grafts used for surgical interventions in the knee. + Proceedings of the Institution of Mechanical Engineers, Part H: Journal of Engineering in + Medicine, 229(12), 879–888. https://doi.org/10/gh63z6 +

+

+ Buchner, H. H. F., Savelberg, H. H. C. M., Schamhardt, H. C., & Barneveld, A. (1995). + Bilateral lameness in horses a kinematic study. Veterinary Quarterly, 17(3), + 103–105. https://doi.org/10/chdqdt +

+

+ Burk, D. L. (2007). Intellectual property and cyberinfrastructure. First Monday. + https://doi.org/1595276491 +

+

+ Crevier-Denoix, N., Robin, D., Pourcelot, P., Falala, S., Holden, L., Estoup, P., Desquilbet, + L., Denoix, J. M., & Chateau, H. (2010). Ground reaction force and kinematic analysis of + limb loading on two different beach sand tracks in harness trotters. + Equine Veterinary Journal, 42(38), 544–551. + https://doi.org/10/dn9j57 +

+

+ de Ruijter, M., Ribeiro, A., Dokter, I., Castilho, M., & Malda, J. (2019). Simultaneous + micropatterning of fibrous meshes and bioinks for the fabrication of living tissue constructs. + Advanced Healthcare Materials, 8(7), 1800418. + https://doi.org/10/gh63z5 +

+

+ Diekman, B. O., & Guilak, F. (2013). Stem cell-based therapies for osteoarthritis: + Challenges and opportunities. Current Opinion in Rheumatology, 25(1), 119–126. + https://doi.org/10/f5k74n +

+

+ Diloksumpan, P., de Ruijter, M., Castilho, M., Gbureck, U., Vermonden, T., van Weeren, P. R., + Malda, J., & Levato, R. (2020). Combining multi-scale 3D printing technologies to engineer + reinforced hydrogel-ceramic interfaces. Biofabrication, 12(2), Article, 25014. + https://doi.org/10/gh63z4 +

+

+ Diloksumpan, P., Vindas Bolanos, R., Cokelaere, S., Pouran, B., de Grauw, J., van Rijen, M., van + Weeren, R., Levato, R., & Malda, J. (2020). Orthotopic bone regeneration within 3D printed + bioceramic scaffolds with region-dependent porosity gradients in an equine model. + Advanced Healthcare Mater, 9(10), 1901807. + https://doi.org/10/gh63z3 +

+

+ Doube, M., Klosowski, M. M., Arganda-Carreras, I., Cordelieres, F. P., Dougherty, R. P., + Jackson, J. S., Schmid, B., Hutchinson, J. R., & Shefelbine, S. J. (2010). BoneJ: Free and + extensible bone image analysis in ImageJ. Bone, 47(6), 1076–1079. + https://doi.org/10/ctk8kn +

+

+ Frisbie, D. D., McCarthy, H. E., Archer, C. W., Barrett, M. F., & McIlwraith, C. W. (2015). + Evaluation of articular cartilage progenitor cells for the repair of articular defects in an + equine model. Journal of Bone and Joint Surgery, 97(6), 484–493. + https://doi.org/10/gh63z2 +

+

+ Goodman, S. B., Pajarinen, J., Yao, Z., & Lin, T. (2019). Inflammation and bone repair: From + particle disease to tissue regeneration. Frontiers in Bioengineering and Biotechnology, + 7, 230. https://doi.org/10/gh63zz +

+

+ Gotterbarm, T., Breusch, S. J., Schneider, U., & Jung, M. (2008). The minipig model for + experimental chondral and osteochondral defect repair in tissue engineering: Retrospective + analysis of 180 defects. Laboratory Animals, 42(1), 71–82. + https://doi.org/10/dxwd9p +

+

+ Greve, L., Dyson, S., & Pfau, T. (2017). Alterations in thoracolumbosacral movement when + pain causing lameness has been improved by diagnostic analgesia. + The Veterinary Journal, 224, 55–63. + https://doi.org/10/gbrrg4 +

+

+ Heuijerjans, A., Wilson, W., Ito, K., & van Donkelaar, C. C. (2018). Osteochondral + resurfacing implantation angle is more important than implant material stiffness. + Journal of Orthopaedic Research, 36(11), 2911–2922. + https://doi.org/10/gh63zx +

+

+ Huang, B. J., Hu, J. C., & Athanasiou, K. A. (2016). Cell-based tissue engineering + strategies used in the clinical repair of articular cartilage. Biomaterials, + 98, 1–22. https://doi.org/10/f8td6q +

+

+ Johnstone, B., Stoddart, M. J., & Im, G. I. (2019). Multi-disciplinary approaches for + cell-based cartilage regeneration. Journal of Orthopaedic Research, 38(3), + 463–472. https://doi.org/10/gh63zw +

+

+ Kloppenburg, M., & Berenbaum, F. (2020). Osteoarthritis year in review 2019: Epidemiology + and therapy. Osteoarthritis and Cartilage, 28(3), 242–248. + https://doi.org/10/gh63zv +

+

+ Kold, S. E., Hickman, J., & Melsen, F. (1986). An experimental study of the healing process + of equine chondral and osteochondral defects. Equine Veterinary Journal, + 18(1), 18–24. https://doi.org/10/cchb7z +

+

+ Kwon, H., Brown, W. E., Lee, C. A., Wang, D., Paschos, N., Hu, J. C., & Athanasiou, K. A. + (2019). Surgical and tissue engineering strategies for articular cartilage and meniscus repair. + Nature Reviews Rheumatology, 15(9), 550–570. + https://doi.org/10/gg8s2h +

+

+ Lee, J. K., Responte, D. J., Cissell, D. D., Hu, J. C., Nolta, J. A., & Athanasiou, K. A. + (2014). Clinical translation of stem cells: Insight for cartilage therapies. + Critical Reviews in Biotechnology, 34(1), 89–100. + https://doi.org/10/gh63zt +

+

+ Levato, R., Webb, W. R., Otto, I. A., Mensinga, A., Zhang, Y., van Rijen, M., van Weeren, R., + Khan, I. M., & Malda, J. (2017). The bio in the ink: Cartilage regeneration with + bioprintable hydrogels and articular cartilage-derived progenitor cells. + Acta Biomaterialia, 61, 41–53. + https://doi.org/10/gh3prk +

+

+ Malda, J., Groll, J., & van Weeren, P. R. (2019). Rethinking articular cartilage + regeneration based on a 250-year-old statement. Nature Reviews Rheumatology, + 15(10), 571–572. https://doi.org/10/gh63zs +

+

+ Mancini, I. A. D., Vindas Bolanos, R. A., Brommer, H., Castilho, M., Ribeiro, A., van Loon, J. + P. A. M., Mensinga, A., van Rijen, M. H. P., Malda, J., & van Weeren, R. (2017). Fixation of + hydrogel constructs for cartilage repair in the equine model: A challenging issue. + Tissue Engineering Part C Methods, 23(11), 804–814. + https://doi.org/10/gch2gd +

+

+ Martin, I., Miot, S., Barbero, A., Jakob, M., & Wendt, D. (2007). Osteochondral tissue + engineering. Journal of Biomechanics, 40(4), 750–765. + https://doi.org/10/fb6fqk +

+

+ McCarthy, H. E., Bara, J. J., Brakspear, K., Singhrao, S. K., & Archer, C. W. (2012). The + comparison of equine articular cartilage progenitor cells and bone marrow-derived stromal cells + as potential cell sources for cartilage repair in the horse. The Veterinary Journal, + 192(3), 345–351. https://doi.org/10/ck96wv +

+

+ Morgan, B. J., Bauza-Mayol, G., Gardner, O. F. W., Zhang, Y., Levato, R., Archer, C. W., van + Weeren, R., Malda, J., Conlan, R. S., & Khan, I. M. (2020). Bone morphogenetic protein-9 is + a potent chondrogenic and morphogenic factor for articular cartilage chondroprogenitors. + Stem Cells and Development, 29(14), 882–894. + https://doi.org/10/gh63zr +

+

+ Nosewicz, T. L., Reilingh, M. L., Wolny, M., van Dijk, C. N., Duda, G. N., & Schell, H. + (2014). Influence of basal support and early loading on bone cartilage healing in press-fitted + osteochondral autografts. Knee Surgery, Sports Traumatology, Arthroscopy, + 22(6), 1445–1451. https://doi.org/10/gh63zq +

+

+ Oryan, A., Alidadi, S., Moshiri, A., & Maffulli, N. (2014). Bone regenerative medicine: + Classic options, novel strategies, and future directions. + Journal of Orthopaedic Surgery and Research, 9(1), Article, 18. + https://doi.org/10/gbftx2 +

+

+ Patel, J. M., Saleh, K. S., Burdick, J. A., & Mauck, R. L. (2019). Bioactive factors for + cartilage repair and regeneration: Improving delivery, retention, and activity. + Acta Biomateralia, 93, 222–238. + https://doi.org/10/gg8sw8 +

+

+ Schindelin, J., Arganda-Carreras, I., Frise, E., Kaynig, V., Longair, M., Pietzsch, T., + Preibisch, S., Rueden, C., Saalfeld, S., Schmid, B., Tinevez, J., White, D. J., Hartenstein, V., + Eliceiri, K., Tomancak, P., & Cardona, A. (2012). Fiji: An open-source platform for + biological-image analysis. Nature Methods, 9(7), 676–682. + https://doi.org/10/f34d7c +

+

+ Schlichting, K., Schell, H., Kleemann, R. U., Schill, A., Weiler, A., Duda, G. N., & Epari, + D. R. (2008). Influence of scaffold stiffness on subchondral bone and subsequent cartilage + regeneration in an ovine model of osteochondral defect healing. + American Journal of Sports Medicine, 36(12), 2379–2391. + https://doi.org/10/c7qbh7 +

+

+ Serra Bragança, F. M., Rhodin, M., & van Weeren, P. R. (2018). On the brink of daily + clinical application of objective gait analysis: What evidence do we have so far from studies + using an induced lameness model? Veterinary Journal, 234, 11–23. + https://doi.org/10/gdmqnn +

+

+ van Susante, J. L., Buma, P., Homminga, G. N., van den Berg, W. B., & Veth, R. P. (1998). + Chondrocyte-seeded hydroxyapatite for repair of large articular cartilage defects. A pilot study + in the goat. Biomaterials, 19(24), 2367–2374. + https://doi.org/10/b74xxn +

+

+ Vindas Bolaños, R. A., Castilho, M., de Grauw, J., Cokelaere, S., Plomp, S., Groll, J., van + Weeren, P. R., Gbureck, U., & Malda, J. (2020). Long-term in vivo performance of + low-temperature 3D-printed bioceramics in an equine model. + ACS Biomaterials Science & Engineering, 6(3), 1681–1689. + https://doi.org/10/gh63zp +

+

+ Vindas Bolaños, R. A., Cokelaere, S. M., Estrada McDermott, J. M., Benders, K. E. M., Gbureck, + U., Plomp, S. G. M., Weinans, H., Groll, J., van Weeren, P. R., & Malda, J. (2017). The use + of a cartilage decellularized matrix scaffold for the repair of osteochondral defects: The + importance of long-term studies in a large animal model. Osteoarthritis and Cartilage, + 25(3), 413–420. https://doi.org/10/f92mgk +

+

+ von Rechenberg, B., Akens, M. K., Nadler, D., Bittmann, P., Zlinszky, K., Kutter, A., Poole, A. + R., & Auer, J. A. (2003). Changes in subchondral bone in cartilage resurfacing—An + experimental study in sheep using different types of osteochondral grafts. + Osteoarthritis and Cartilage, 11(4), 265–277. + https://doi.org/10/bd6pvd +

+

+ Williams, R., Khan, I. M., Richardson, K., Nelson, L., McCarthy, H. E., Analbelsi, T., Singhrao, + S. K., Dowthwaite, G. P., Jones, R. E., Baird, D. M., Lewis, H., Roberts, S., Shaw, H. M., + Dudhia, J., Fairclough, J., Briggs, T., & Archer, C. W. (2010). Identification and clonal + characterisation of a progenitor cell sub-population in normal human articular cartilage. + PLoS One, 5(10), Article, 13246. + https://doi.org/10/bxkqkn +

+

Supplementary information

+

Supplementary Table 1.

+

+ Symmetry parameters (differences between baseline (before implantation) and endpoint (6 + months after implantation) of the study for all ponies). +

+

!unsupported node 'iframe'!

+

Note. Values are given in estimated means (CI)

+

Supplementary Table 2.

+

+ Hind limb parameters (differences between cell-free and cell-laden constructs at 6 months after + implantation). +

+

!unsupported node 'iframe'!

+

Note. Values are given in estimated means (CI)

+
+ +
+ Gait analysis: symmetry parameters. Pelvis roll and yaw joint angles (A,B) showed + no significant differences between baseline and 6 months after implantation and neither did + the kinematic hind limb parameters (C, D, E) between baseline and 6 months after induction. + Limb height of the hind limbs (F) decreased for both hindlimbs, but only significantly for + the cell free group, though there was no difference in limb height between cell-laden and + cell-free groups at 6 months after implementation. +
+
+
+ +
+ Representative radiographic images (latero-medial and craniolateral-caudomedial oblique + projections) of the stifle of ponies before implantation, 3 months after implantation + and 6 months after implantation. + Red arrows indicate the implantation sites. No radiographic abnormalities were noted in any + of the ponies (1st row), except for the pony that became severely lame at 10 + weeks. In this animal extensive osteolysis was observed (2nd row). +
+
+

!unsupported node 'iframe'!

+
+ +
Amount of GAG that was normalized with amount of DNA.
+
+

+
+ +
+ Representative Hematoxylin-Eosin (H&E) staining of 6-month harvested samples + showing a degenerated and necrotic superficial layer at the surface of the chondral + compartment featuring inflammatory cells. + Black arrow indicates area of degenerated cells, P = plasma cells +
+
+
+ +
+ Representative immunohistochemistry of collagen type I staining of 6-month harvested + samples from cell-laden and cell-free osteochondral structures +
+
+
+ +
+ Representative Hematoxylin-Eosin (H&E) staining of 6-month harvested samples from + cell-laden osteochondral structures + CR = Remaining ceramic, NB = Newly formed bone, MEW = pattern of PCL-microfibers, * = + Multifocal foci of inflammatory reaction, # = artifact from cutting, EO = Eosinophil, L = + Lymphocyte +
+
+
+ +
+ Representative Hematoxylin-Eosin (H&E) staining of 6-month harvested samples from + cell-free osteochondral structures. + CR = Remaining ceramic, NB = Newly formed bone, MEW = pattern of PCL-microfibers, * = + Multifocal foci of inflammatory reaction, # = artifact from cutting, EO = Eosinophil, L = + Lymphocyte. +
+
diff --git a/infra/Caddyfile.gateway b/infra/Caddyfile.gateway new file mode 100644 index 0000000000..c6b75a10b6 --- /dev/null +++ b/infra/Caddyfile.gateway @@ -0,0 +1,46 @@ +# this is a shared caddyfile for all preview/sandbox deployments +# they cannot all have their own caddyfile sadly + +{ + admin :2019 + on_demand_tls { + ask http://localhost:8888/allow-domain + } +} + +:8888 { + @allow expression {path} == "/allow-domain" && ({query.domain}.matches("pr-[0-9]+\\.preview\\.pubstar\\.org") || {query.domain} == "sandbox.pubstar.org") + handle @allow { + respond "OK" 200 + } + + respond "Not allowed" 403 +} + +(s3site) { + @pathWithSlash path_regexp dir (.+)/$ + handle @pathWithSlash { + redir {re.dir.1} permanent + } + + rewrite * /{args[0]}{uri} + + reverse_proxy {args[1]} { + @error status 403 404 + handle_response @error { + rewrite * {uri}/index.html + reverse_proxy {args[1]} { + @nestedError status 404 + handle_response @nestedError { + respond "Not found" 404 + } + } + } + } +} + +import /etc/caddy/sites.d/*.caddy + +:80 { + respond "OK" 200 +} diff --git a/infra/Caddyfile.preview b/infra/Caddyfile.preview deleted file mode 100644 index 722a4fe3a7..0000000000 --- a/infra/Caddyfile.preview +++ /dev/null @@ -1,81 +0,0 @@ -{ - admin :2019 - debug - on_demand_tls { - ask http://localhost:8888/allow-domain - } -} - -# this only allows preview-*.pubstar.org domains to be used for TLS certificates -# prevents abuse -:8888 { - @allow expression {path} == "/allow-domain" && {query.domain}.matches("pr-[0-9]+.pubstar.org") - handle @allow { - respond "OK" 200 - } - - respond "Not allowed" 403 -} - - -(s3site) { - # strip trailing slash from paths (except root) - @pathWithSlash path_regexp dir (.+)/$ - handle @pathWithSlash { - redir {re.dir.1} permanent - } - - # rewrite to include bucket path - rewrite * /{args[0]}{uri} - - # reverse proxy to garage S3 API - reverse_proxy {args[1]} { - # handle 403/404 by trying index.html - @error status 403 404 - handle_response @error { - rewrite * {uri}/index.html - reverse_proxy {args[1]} { - @nestedError status 404 - handle_response @nestedError { - respond "Not found" 404 - } - } - } - } -} - -:443 { - tls internal { - 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/* { - import s3site {$S3_BUCKET_NAME:assets} {$S3_ENDPOINT:http://minio:9000} - } - - handle_path /emails/* { - reverse_proxy inbucket:9000 - } - - handle { - reverse_proxy platform:3000 - } -} - -:80 { - respond "OK" 200 -} diff --git a/infra/Caddyfile.site.template b/infra/Caddyfile.site.template new file mode 100644 index 0000000000..9919c6aa6e --- /dev/null +++ b/infra/Caddyfile.site.template @@ -0,0 +1,31 @@ +${DEPLOY_HOST} { + tls { + on_demand + } + + encode gzip + + handle_path /assets* { + reverse_proxy ${STACK_NAME}_minio:9000 + } + + handle_path /assets-ui* { + reverse_proxy ${STACK_NAME}_minio:9001 + } + + handle_path /site-builder* { + reverse_proxy ${STACK_NAME}_site-builder:4000 + } + + handle_path /sites/* { + import s3site {$S3_BUCKET_NAME:assets} http://${STACK_NAME}_minio:9000 + } + + handle_path /emails/* { + reverse_proxy ${STACK_NAME}_inbucket:9000 + } + + handle { + reverse_proxy ${STACK_NAME}_platform:3000 + } +} diff --git a/infra/stack.gateway.yml b/infra/stack.gateway.yml new file mode 100644 index 0000000000..03f4df36fc --- /dev/null +++ b/infra/stack.gateway.yml @@ -0,0 +1,34 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/swarmlibs/dockerstack-schema/main/schema/dockerstack-spec.json + +services: + + proxy: + image: caddy:latest + volumes: + - ./Caddyfile.gateway:/etc/caddy/Caddyfile:ro + - /srv/caddy/sites.d:/etc/caddy/sites.d:ro + - caddy_data:/data + - caddy_config:/config + networks: [gateway_net] + ports: + - target: 80 + published: 80 + protocol: tcp + mode: host + - target: 443 + published: 443 + protocol: tcp + mode: host + deploy: + replicas: 1 + restart_policy: + condition: any + +networks: + gateway_net: + driver: overlay + attachable: true + +volumes: + caddy_data: + caddy_config: diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index ef1560e4c6..738aaf1b1d 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -2,28 +2,6 @@ services: - proxy: - image: caddy: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/knowledgefutures/platform:${IMAGE_TAG} env_file: [.env] @@ -31,14 +9,14 @@ services: HOSTNAME: "0.0.0.0" NODE_ENV: production PORT: "3000" - PUBPUB_URL: https://pr-${PR_NUMBER}.pubstar.org - PUBPUB_HOSTNAME: pr-${PR_NUMBER}.pubstar.org + PUBPUB_URL: https://${DEPLOY_HOST} + PUBPUB_HOSTNAME: ${DEPLOY_HOST} SITE_BUILDER_ENDPOINT: http://site-builder:4000 - S3_PUBLIC_ENDPOINT: https://pr-${PR_NUMBER}.pubstar.org/assets + S3_PUBLIC_ENDPOINT: https://${DEPLOY_HOST}/assets FLAGS: "uploads:off,invites:off,disabled-actions:http+email" DB_RESET: "true" DB_SEED: "true" - networks: [appnet] + networks: [appnet, gateway_net] healthcheck: test: - CMD-SHELL @@ -74,7 +52,7 @@ services: NODE_ENV: production PUBPUB_URL: http://platform:3000 PORT: "4000" - networks: [appnet] + networks: [appnet, gateway_net] deploy: replicas: 1 restart_policy: @@ -104,8 +82,8 @@ services: env_file: [.env] command: server --console-address ":9001" /data environment: - MINIO_BROWSER_REDIRECT_URL: https://pr-${PR_NUMBER}.pubstar.org/assets-ui - networks: [appnet] + MINIO_BROWSER_REDIRECT_URL: https://${DEPLOY_HOST}/assets-ui + networks: [appnet, gateway_net] deploy: replicas: 1 restart_policy: @@ -131,7 +109,7 @@ services: inbucket: image: inbucket/inbucket:latest - networks: [appnet] + networks: [appnet, gateway_net] deploy: replicas: 1 restart_policy: @@ -140,8 +118,9 @@ services: networks: appnet: driver: overlay + gateway_net: + external: true + name: gateway_gateway_net volumes: pgdata: - caddy_data: - caddy_config: From c61e3867f71fb216caafccb34be109d6679ea7b3 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 17:12:48 +0200 Subject: [PATCH 45/78] fix(skip-build): test From 66899b450c8c1dffe87245f3386363b46ddc2361 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 17:17:18 +0200 Subject: [PATCH 46/78] fix: skip skip --- .github/workflows/on_pr.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 8937b39a01..958b4712ac 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -108,9 +108,10 @@ jobs: 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' || needs.skip_build_sha.outputs.last-successful-build-sha != '') && contains(github.event.pull_request.labels.*.name, 'preview') uses: ./.github/workflows/deploy-stack.yml needs: + - skip_build_sha - build-all permissions: contents: read From fbd7c985491a521d83367537e980bbb647749466 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 17:28:04 +0200 Subject: [PATCH 47/78] fix(skip-build): debug gateway --- infra/Caddyfile | 1 + infra/Caddyfile.gateway | 1 + 2 files changed, 2 insertions(+) diff --git a/infra/Caddyfile b/infra/Caddyfile index d65c80d7de..47ffda4e3b 100644 --- a/infra/Caddyfile +++ b/infra/Caddyfile @@ -1,5 +1,6 @@ { admin :2019 + debug } {$PUBPUB_HOSTNAME} { diff --git a/infra/Caddyfile.gateway b/infra/Caddyfile.gateway index c6b75a10b6..2e0a3f2328 100644 --- a/infra/Caddyfile.gateway +++ b/infra/Caddyfile.gateway @@ -6,6 +6,7 @@ on_demand_tls { ask http://localhost:8888/allow-domain } + debug } :8888 { From 3fb1cf7d76689c1bef4ef0de59721e8eec9e3c52 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 17:32:52 +0200 Subject: [PATCH 48/78] fix(skip-build): common --- .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 958b4712ac..47596dab11 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -108,7 +108,7 @@ jobs: image-tag-override: ${{ needs.skip_build_sha.outputs.last-successful-build-sha || '' }} deploy-preview: - if: (needs.build-all.result == 'success' || needs.skip_build_sha.outputs.last-successful-build-sha != '') && contains(github.event.pull_request.labels.*.name, 'preview') + if: (needs.build-all.result == 'success' || needs.build-all.result == 'skipped') && contains(github.event.pull_request.labels.*.name, 'preview') uses: ./.github/workflows/deploy-stack.yml needs: - skip_build_sha From 9e28c53b0cb6305b35e38ea02f4c36e6ed2552b7 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 17:40:13 +0200 Subject: [PATCH 49/78] fix(skip-build): again! --- .github/workflows/on_main.yml | 4 ++++ .github/workflows/on_pr.yml | 15 +++++++++++++-- .github/workflows/on_tag.yml | 4 ++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/on_main.yml b/.github/workflows/on_main.yml index 612b5c8f81..a3094689b5 100644 --- a/.github/workflows/on_main.yml +++ b/.github/workflows/on_main.yml @@ -12,6 +12,10 @@ permissions: packages: write pull-requests: write +concurrency: + group: deploy-main + cancel-in-progress: false + jobs: ci: uses: ./.github/workflows/ci.yml diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 47596dab11..4f929340fc 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -10,6 +10,10 @@ permissions: packages: write pull-requests: write +concurrency: + group: pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + jobs: path-filter: runs-on: ubuntu-latest @@ -108,7 +112,14 @@ jobs: image-tag-override: ${{ needs.skip_build_sha.outputs.last-successful-build-sha || '' }} deploy-preview: - if: (needs.build-all.result == 'success' || needs.build-all.result == 'skipped') && contains(github.event.pull_request.labels.*.name, 'preview') + if: >- + always() && + contains(github.event.pull_request.labels.*.name, 'preview') && + github.event.action != 'closed' && + github.event.action != 'unlabeled' && + needs.skip_build_sha.result == 'success' && + (needs.build-all.result == 'success' || + (needs.build-all.result == 'skipped' && needs.skip_build_sha.outputs.last-successful-build-sha != '')) uses: ./.github/workflows/deploy-stack.yml needs: - skip_build_sha @@ -118,7 +129,7 @@ jobs: pull-requests: write with: action: deploy - image_tag: ${{ github.event.pull_request.head.sha }} + image_tag: ${{ needs.skip_build_sha.outputs.last-successful-build-sha || github.event.pull_request.head.sha }} stack_name: preview-pr-${{ github.event.pull_request.number }} hostname: pr-${{ github.event.pull_request.number }}.preview.pubstar.org env_file: .env.preview.enc diff --git a/.github/workflows/on_tag.yml b/.github/workflows/on_tag.yml index 19b4f841de..6ee5bf9c61 100644 --- a/.github/workflows/on_tag.yml +++ b/.github/workflows/on_tag.yml @@ -12,8 +12,8 @@ permissions: pull-requests: write concurrency: - group: deploy-prod-${{ github.ref_name }} - cancel-in-progress: true + group: deploy-prod + cancel-in-progress: false jobs: validate-tag: From b0d649d38e26de00e85bb9af92890a776e30a9fd Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 17:54:15 +0200 Subject: [PATCH 50/78] fix: cacheing, correct build --- .github/workflows/deploy-stack.yml | 4 +++- .github/workflows/ghcr-build-template.yml | 4 ++-- infra/Caddyfile.site.template | 4 +--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy-stack.yml b/.github/workflows/deploy-stack.yml index 6f69945edc..1ad4c3465f 100644 --- a/.github/workflows/deploy-stack.yml +++ b/.github/workflows/deploy-stack.yml @@ -119,7 +119,9 @@ jobs: cd infra umask 077 - sops -d --input-type dotenv --output-type dotenv "$ENV_FILE" > .env + # write to a per-stack env file to avoid races between concurrent deploys + sops -d --input-type dotenv --output-type dotenv "$ENV_FILE" > ".env.${STACK_NAME}" + ln -sf ".env.${STACK_NAME}" .env if ! sudo docker info --format '{{.Swarm.LocalNodeState}}' | grep -qx active; then sudo docker swarm init --advertise-addr "$(hostname -I | awk '{print $1}')" diff --git a/.github/workflows/ghcr-build-template.yml b/.github/workflows/ghcr-build-template.yml index 58ecb4a598..330dbf8bd7 100644 --- a/.github/workflows/ghcr-build-template.yml +++ b/.github/workflows/ghcr-build-template.yml @@ -80,8 +80,8 @@ jobs: 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 }} + cache-from: type=registry,ref=ghcr.io/knowledgefutures/${{ inputs.ghcr_image_name }}:buildcache + cache-to: type=registry,ref=ghcr.io/knowledgefutures/${{ inputs.ghcr_image_name }}:buildcache,mode=max build-args: | PACKAGE=${{ inputs.package }} CI=true diff --git a/infra/Caddyfile.site.template b/infra/Caddyfile.site.template index 9919c6aa6e..d5bcbce832 100644 --- a/infra/Caddyfile.site.template +++ b/infra/Caddyfile.site.template @@ -1,7 +1,5 @@ ${DEPLOY_HOST} { - tls { - on_demand - } + tls internal encode gzip From b9c00ffabfaf51f435d57b2311798dbba3d6a6e2 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 18:13:10 +0200 Subject: [PATCH 51/78] fix: update all the actions (danger) --- .github/workflows/build-docs.yml | 10 ++++----- .github/workflows/ci.yml | 10 ++++----- .github/workflows/deploy-stack.yml | 2 +- .github/workflows/e2e.yml | 6 ++--- .github/workflows/ghcr-build-template.yml | 8 +++---- .github/workflows/on_pr.yml | 6 ++--- .github/workflows/on_tag.yml | 2 +- core/.github/workflows/playwright.yml | 27 ----------------------- 8 files changed, 22 insertions(+), 49 deletions(-) delete mode 100644 core/.github/workflows/playwright.yml diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 48bc99a699..8716faa3c8 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -15,14 +15,14 @@ jobs: with: # necessary in order to show latest updates in docs fetch-depth: 0 - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 22.13.1 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 name: Install pnpm with: run_install: false @@ -34,7 +34,7 @@ jobs: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - name: Setup pnpm cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.get-store-path.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -42,7 +42,7 @@ jobs: ${{ runner.os }}-pnpm-store- # - name: Cache turbo - # uses: actions/cache@v4 + # uses: actions/cache@v5 # with: # path: .turbo # key: ${{ runner.os }}-turbo-${{ github.sha }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00b61d8d75..c7ec93f10e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,14 +28,14 @@ jobs: ENV_FILE: .env.docker-compose.dev steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 22.13.1 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 name: Install pnpm with: run_install: false @@ -47,7 +47,7 @@ jobs: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - name: Setup pnpm cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.get-store-path.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -56,7 +56,7 @@ jobs: # to cache p:build, format, lint, type-check and test-run - name: Setup turbo cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: .turbo key: ${{ runner.os }}-turbo-${{ github.sha }} diff --git a/.github/workflows/deploy-stack.yml b/.github/workflows/deploy-stack.yml index 1ad4c3465f..311f93c82e 100644 --- a/.github/workflows/deploy-stack.yml +++ b/.github/workflows/deploy-stack.yml @@ -57,7 +57,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Start SSH agent uses: webfactory/ssh-agent@v0.9.0 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d147f1eae0..b7233997cb 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -26,16 +26,16 @@ jobs: ENV_FILE: .env.docker-compose.dev steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 22.13.1 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 name: Install pnpm with: run_install: false diff --git a/.github/workflows/ghcr-build-template.yml b/.github/workflows/ghcr-build-template.yml index 330dbf8bd7..246eb9d86a 100644 --- a/.github/workflows/ghcr-build-template.yml +++ b/.github/workflows/ghcr-build-template.yml @@ -40,19 +40,19 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Compute image tags id: label @@ -77,7 +77,7 @@ jobs: echo "tags=$TAGS" >> $GITHUB_OUTPUT - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . cache-from: type=registry,ref=ghcr.io/knowledgefutures/${{ inputs.ghcr_image_name }}:buildcache diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 4f929340fc..5d69b7a53c 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -21,7 +21,7 @@ jobs: outputs: docs: ${{ steps.changes.outputs.docs }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: dorny/paths-filter@v3 @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} @@ -186,7 +186,7 @@ jobs: # if: github.event.action == 'closed' && needs.path-filter.outputs.docs == 'true' # runs-on: ubuntu-latest # steps: - # - uses: actions/checkout@v4 + # - uses: actions/checkout@v6 # - name: Close docs preview # uses: rossjrw/pr-preview-action@v1 diff --git a/.github/workflows/on_tag.yml b/.github/workflows/on_tag.yml index 6ee5bf9c61..9df13babf0 100644 --- a/.github/workflows/on_tag.yml +++ b/.github/workflows/on_tag.yml @@ -55,7 +55,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Create GitHub release env: diff --git a/core/.github/workflows/playwright.yml b/core/.github/workflows/playwright.yml deleted file mode 100644 index b050f147ee..0000000000 --- a/core/.github/workflows/playwright.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Playwright Tests -on: - push: - branches: [main, master] - pull_request: - branches: [main, master] -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 22.13.1 - - name: Install dependencies - run: npm install -g pnpm && pnpm install - - name: Install Playwright Browsers - run: pnpm exec playwright install --with-deps - - name: Run Playwright tests - run: pnpm exec playwright test - - uses: actions/upload-artifact@v4 - if: always() - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 From fca427090a2ea549d1fb88381b0ae81cd6d16751 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 18:15:28 +0200 Subject: [PATCH 52/78] fix: remove preview from domain name --- .github/workflows/deploy-stack.yml | 10 ++++------ .github/workflows/on_pr.yml | 4 ++-- infra/Caddyfile.gateway | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy-stack.yml b/.github/workflows/deploy-stack.yml index 311f93c82e..1bd921b9fc 100644 --- a/.github/workflows/deploy-stack.yml +++ b/.github/workflows/deploy-stack.yml @@ -132,12 +132,10 @@ jobs: sudo docker pull ghcr.io/knowledgefutures/platform:"$IMAGE_TAG" if [[ "$USES_GATEWAY" == "true" ]]; then - # ensure the gateway stack is running - if ! sudo docker stack ls --format '{{.Name}}' | grep -qx gateway; then - echo "deploying gateway stack..." - sudo docker stack deploy -c stack.gateway.yml --prune gateway - sleep 5 - fi + # deploy/update the gateway stack (idempotent, picks up config changes) + echo "deploying gateway stack..." + sudo docker stack deploy -c stack.gateway.yml --prune gateway + sleep 5 # write per-site caddy config from template sudo mkdir -p /srv/caddy/sites.d diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 5d69b7a53c..5f802a2a4d 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -131,7 +131,7 @@ jobs: action: deploy image_tag: ${{ needs.skip_build_sha.outputs.last-successful-build-sha || github.event.pull_request.head.sha }} stack_name: preview-pr-${{ github.event.pull_request.number }} - hostname: pr-${{ github.event.pull_request.number }}.preview.pubstar.org + hostname: pr-${{ github.event.pull_request.number }}.pubstar.org env_file: .env.preview.enc stack_file: stack.preview.yml uses_gateway: true @@ -152,7 +152,7 @@ jobs: with: action: teardown stack_name: preview-pr-${{ github.event.pull_request.number }} - hostname: pr-${{ github.event.pull_request.number }}.preview.pubstar.org + hostname: pr-${{ github.event.pull_request.number }}.pubstar.org env_file: .env.preview.enc stack_file: stack.preview.yml uses_gateway: true diff --git a/infra/Caddyfile.gateway b/infra/Caddyfile.gateway index 2e0a3f2328..df54cb2000 100644 --- a/infra/Caddyfile.gateway +++ b/infra/Caddyfile.gateway @@ -10,7 +10,7 @@ } :8888 { - @allow expression {path} == "/allow-domain" && ({query.domain}.matches("pr-[0-9]+\\.preview\\.pubstar\\.org") || {query.domain} == "sandbox.pubstar.org") + @allow expression {path} == "/allow-domain" && ({query.domain}.matches("pr-[0-9]+\\.pubstar\\.org") || {query.domain} == "sandbox.pubstar.org") handle @allow { respond "OK" 200 } From 61b3328dbe9dcc568d7dfe3d4ec2a8d25027e3f0 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 18:19:46 +0200 Subject: [PATCH 53/78] fix: add more dockerignore --- .dockerignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 42710b5f5e..5ceae9f8b1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,7 @@ **/.classpath **/.dockerignore -**/.env +**/.env* **/.git **/.project **/.settings @@ -29,5 +29,9 @@ **/values.dev.yaml **/build **/dist +**/.github +**/.turbo +**/.infra + LICENSE README.md From f9c2bd26afa8dd987a3c41230a501bd253280496 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 18:47:22 +0200 Subject: [PATCH 54/78] fix(skip-build): force update --- .github/workflows/deploy-stack.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-stack.yml b/.github/workflows/deploy-stack.yml index 1bd921b9fc..43b8136607 100644 --- a/.github/workflows/deploy-stack.yml +++ b/.github/workflows/deploy-stack.yml @@ -132,9 +132,12 @@ jobs: sudo docker pull ghcr.io/knowledgefutures/platform:"$IMAGE_TAG" if [[ "$USES_GATEWAY" == "true" ]]; then - # deploy/update the gateway stack (idempotent, picks up config changes) + # deploy/update the gateway stack echo "deploying gateway stack..." sudo docker stack deploy -c stack.gateway.yml --prune gateway + + # force-update the proxy so bind-mounted caddyfile changes get picked up + sudo docker service update --force gateway_proxy 2>/dev/null || true sleep 5 # write per-site caddy config from template From dfefe30b0bdab74592c5d3c383e572e4b1305edf Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 19:01:12 +0200 Subject: [PATCH 55/78] fix(skip-build): dnsrr --- infra/stack.preview.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index 738aaf1b1d..500abf42ca 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -30,6 +30,7 @@ services: start_period: 60s deploy: replicas: 1 + endpoint_mode: dnsrr restart_policy: condition: on-failure @@ -55,6 +56,7 @@ services: networks: [appnet, gateway_net] deploy: replicas: 1 + endpoint_mode: dnsrr restart_policy: condition: on-failure @@ -86,6 +88,7 @@ services: networks: [appnet, gateway_net] deploy: replicas: 1 + endpoint_mode: dnsrr restart_policy: condition: any @@ -112,6 +115,7 @@ services: networks: [appnet, gateway_net] deploy: replicas: 1 + endpoint_mode: dnsrr restart_policy: condition: any From aebf8c25a31c6e4f68e7f0ada499608fe0947ad5 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 19:32:48 +0200 Subject: [PATCH 56/78] fix: apply stuff --- .github/workflows/deploy-stack.yml | 29 ++--------- infra/Caddyfile.gateway | 77 +++++++++++++++++++----------- infra/Caddyfile.site.template | 29 ----------- infra/stack.gateway.yml | 1 - infra/stack.preview.yml | 4 -- 5 files changed, 53 insertions(+), 87 deletions(-) delete mode 100644 infra/Caddyfile.site.template diff --git a/.github/workflows/deploy-stack.yml b/.github/workflows/deploy-stack.yml index 43b8136607..dd334a9fd0 100644 --- a/.github/workflows/deploy-stack.yml +++ b/.github/workflows/deploy-stack.yml @@ -114,7 +114,8 @@ jobs: cd "${APP_DIR}" git fetch --prune --tags origin - git checkout --detach "${IMAGE_TAG}" 2>/dev/null || git checkout --detach "origin/${BRANCH}" + # always checkout the latest commit on the branch, even if we are skipping the build + git checkout --detach "origin/${BRANCH}" cd infra umask 077 @@ -132,32 +133,13 @@ jobs: sudo docker pull ghcr.io/knowledgefutures/platform:"$IMAGE_TAG" if [[ "$USES_GATEWAY" == "true" ]]; then - # deploy/update the gateway stack echo "deploying gateway stack..." sudo docker stack deploy -c stack.gateway.yml --prune gateway - # force-update the proxy so bind-mounted caddyfile changes get picked up - sudo docker service update --force gateway_proxy 2>/dev/null || true - sleep 5 - - # write per-site caddy config from template - sudo mkdir -p /srv/caddy/sites.d - - export DEPLOY_HOST STACK_NAME - envsubst '$DEPLOY_HOST $STACK_NAME' < Caddyfile.site.template \ - | sudo tee /srv/caddy/sites.d/${STACK_NAME}.caddy > /dev/null - - # deploy the app stack sudo env IMAGE_TAG="$IMAGE_TAG" DEPLOY_HOST="$DEPLOY_HOST" \ docker stack deploy -c "$STACK_FILE" \ --with-registry-auth --resolve-image always --prune "$STACK_NAME" - # reload caddy to pick up the new site config - gateway_container=$(sudo docker ps --filter "label=com.docker.swarm.service.name=gateway_proxy" --format '{{.ID}}' | head -1) - if [[ -n "$gateway_container" ]]; then - sudo docker exec "$gateway_container" caddy reload --config /etc/caddy/Caddyfile - fi - else # standalone mode (production) -- deploy full stack with its own caddy echo "deploying with IMAGE_TAG=$IMAGE_TAG" @@ -266,12 +248,7 @@ jobs: fi if [[ "$USES_GATEWAY" == "true" ]]; then - sudo rm -f "/srv/caddy/sites.d/${STACK_NAME}.caddy" - - gateway_container=$(sudo docker ps --filter "label=com.docker.swarm.service.name=gateway_proxy" --format '{{.ID}}' | head -1) - if [[ -n "$gateway_container" ]]; then - sudo docker exec "$gateway_container" caddy reload --config /etc/caddy/Caddyfile - fi + echo "gateway routes are dynamic, no per-site cleanup needed" fi sudo docker image prune -f diff --git a/infra/Caddyfile.gateway b/infra/Caddyfile.gateway index df54cb2000..b38afd37af 100644 --- a/infra/Caddyfile.gateway +++ b/infra/Caddyfile.gateway @@ -1,46 +1,69 @@ -# this is a shared caddyfile for all preview/sandbox deployments -# they cannot all have their own caddyfile sadly - { admin :2019 - on_demand_tls { - ask http://localhost:8888/allow-domain - } debug } -:8888 { - @allow expression {path} == "/allow-domain" && ({query.domain}.matches("pr-[0-9]+\\.pubstar\\.org") || {query.domain} == "sandbox.pubstar.org") - handle @allow { - respond "OK" 200 +*.pubstar.org { + tls internal + + encode gzip + + handle_path /assets* { + reverse_proxy { + dynamic a preview-{labels.2}_minio 9000 + } } - respond "Not allowed" 403 -} + handle_path /assets-ui* { + reverse_proxy { + dynamic a preview-{labels.2}_minio 9001 + } + } -(s3site) { - @pathWithSlash path_regexp dir (.+)/$ - handle @pathWithSlash { - redir {re.dir.1} permanent + handle_path /site-builder* { + reverse_proxy { + dynamic a preview-{labels.2}_site-builder 4000 + } } - rewrite * /{args[0]}{uri} + handle_path /sites/* { + @pathWithSlash path_regexp dir (.+)/$ + handle @pathWithSlash { + redir {re.dir.1} permanent + } + + rewrite * /assets{uri} + + reverse_proxy { + dynamic a preview-{labels.2}_minio 9000 + + @error status 403 404 + handle_response @error { + rewrite * {uri}/index.html + reverse_proxy { + dynamic a preview-{labels.2}_minio 9000 - reverse_proxy {args[1]} { - @error status 403 404 - handle_response @error { - rewrite * {uri}/index.html - reverse_proxy {args[1]} { - @nestedError status 404 - handle_response @nestedError { - respond "Not found" 404 + @nestedError status 404 + handle_response @nestedError { + respond "Not found" 404 + } } } } } -} -import /etc/caddy/sites.d/*.caddy + handle_path /emails/* { + reverse_proxy { + dynamic a preview-{labels.2}_inbucket 9000 + } + } + + handle { + reverse_proxy { + dynamic a preview-{labels.2}_platform 3000 + } + } +} :80 { respond "OK" 200 diff --git a/infra/Caddyfile.site.template b/infra/Caddyfile.site.template deleted file mode 100644 index d5bcbce832..0000000000 --- a/infra/Caddyfile.site.template +++ /dev/null @@ -1,29 +0,0 @@ -${DEPLOY_HOST} { - tls internal - - encode gzip - - handle_path /assets* { - reverse_proxy ${STACK_NAME}_minio:9000 - } - - handle_path /assets-ui* { - reverse_proxy ${STACK_NAME}_minio:9001 - } - - handle_path /site-builder* { - reverse_proxy ${STACK_NAME}_site-builder:4000 - } - - handle_path /sites/* { - import s3site {$S3_BUCKET_NAME:assets} http://${STACK_NAME}_minio:9000 - } - - handle_path /emails/* { - reverse_proxy ${STACK_NAME}_inbucket:9000 - } - - handle { - reverse_proxy ${STACK_NAME}_platform:3000 - } -} diff --git a/infra/stack.gateway.yml b/infra/stack.gateway.yml index 03f4df36fc..5f03eda5b3 100644 --- a/infra/stack.gateway.yml +++ b/infra/stack.gateway.yml @@ -6,7 +6,6 @@ services: image: caddy:latest volumes: - ./Caddyfile.gateway:/etc/caddy/Caddyfile:ro - - /srv/caddy/sites.d:/etc/caddy/sites.d:ro - caddy_data:/data - caddy_config:/config networks: [gateway_net] diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index 500abf42ca..738aaf1b1d 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -30,7 +30,6 @@ services: start_period: 60s deploy: replicas: 1 - endpoint_mode: dnsrr restart_policy: condition: on-failure @@ -56,7 +55,6 @@ services: networks: [appnet, gateway_net] deploy: replicas: 1 - endpoint_mode: dnsrr restart_policy: condition: on-failure @@ -88,7 +86,6 @@ services: networks: [appnet, gateway_net] deploy: replicas: 1 - endpoint_mode: dnsrr restart_policy: condition: any @@ -115,7 +112,6 @@ services: networks: [appnet, gateway_net] deploy: replicas: 1 - endpoint_mode: dnsrr restart_policy: condition: any From ccb8bb5696e82a24ae41d2e975b5b09e38c395dc Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 19:39:07 +0200 Subject: [PATCH 57/78] fix: aaa --- .dockerignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 5ceae9f8b1..3dd0aeaabc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,7 @@ **/.classpath **/.dockerignore -**/.env* +**/.env **/.git **/.project **/.settings From ce3a6e64341ee16d31bcb7e36f05d96c82f958fa Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 20:03:54 +0200 Subject: [PATCH 58/78] fix(skip-build): on demand --- infra/Caddyfile.gateway | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/infra/Caddyfile.gateway b/infra/Caddyfile.gateway index b38afd37af..8f4421fad1 100644 --- a/infra/Caddyfile.gateway +++ b/infra/Caddyfile.gateway @@ -4,7 +4,9 @@ } *.pubstar.org { - tls internal + tls internal { + on_demand + } encode gzip From ec63ebec49e46c621e83ca3013a2cccb6aaf7242 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 20:33:05 +0200 Subject: [PATCH 59/78] fix: tasks --- infra/Caddyfile.gateway | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/infra/Caddyfile.gateway b/infra/Caddyfile.gateway index 8f4421fad1..af9d843abe 100644 --- a/infra/Caddyfile.gateway +++ b/infra/Caddyfile.gateway @@ -12,19 +12,19 @@ handle_path /assets* { reverse_proxy { - dynamic a preview-{labels.2}_minio 9000 + dynamic a tasks.preview-{labels.2}_minio 9000 } } handle_path /assets-ui* { reverse_proxy { - dynamic a preview-{labels.2}_minio 9001 + dynamic a tasks.preview-{labels.2}_minio 9001 } } handle_path /site-builder* { reverse_proxy { - dynamic a preview-{labels.2}_site-builder 4000 + dynamic a tasks.preview-{labels.2}_site-builder 4000 } } @@ -37,13 +37,13 @@ rewrite * /assets{uri} reverse_proxy { - dynamic a preview-{labels.2}_minio 9000 + dynamic a tasks.preview-{labels.2}_minio 9000 @error status 403 404 handle_response @error { rewrite * {uri}/index.html reverse_proxy { - dynamic a preview-{labels.2}_minio 9000 + dynamic a tasks.preview-{labels.2}_minio 9000 @nestedError status 404 handle_response @nestedError { @@ -56,13 +56,13 @@ handle_path /emails/* { reverse_proxy { - dynamic a preview-{labels.2}_inbucket 9000 + dynamic a tasks.preview-{labels.2}_inbucket 9000 } } handle { reverse_proxy { - dynamic a preview-{labels.2}_platform 3000 + dynamic a tasks.preview-{labels.2}_platform 3000 } } } From 03df20540c6a56073b0fa3d16c0b419694759499 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 20:56:56 +0200 Subject: [PATCH 60/78] fix(skip-build): whole other approach --- .github/workflows/deploy-stack.yml | 13 ++++++++++- infra/Caddyfile.gateway | 33 ++++++++++------------------ infra/stack.gateway.yml | 8 ++----- infra/stack.preview.yml | 35 ++++++++++++++++++++++++------ 4 files changed, 54 insertions(+), 35 deletions(-) diff --git a/.github/workflows/deploy-stack.yml b/.github/workflows/deploy-stack.yml index dd334a9fd0..2a186858af 100644 --- a/.github/workflows/deploy-stack.yml +++ b/.github/workflows/deploy-stack.yml @@ -136,7 +136,18 @@ jobs: echo "deploying gateway stack..." sudo docker stack deploy -c stack.gateway.yml --prune gateway + # derive host ports from PR number (prefix digit + pr number) + PR_NUM="${STACK_NAME##*-}" + PLATFORM_PORT="1${PR_NUM}" + BUILDER_PORT="2${PR_NUM}" + MINIO_PORT="3${PR_NUM}" + MINIO_CONSOLE_PORT="4${PR_NUM}" + INBUCKET_PORT="5${PR_NUM}" + sudo env IMAGE_TAG="$IMAGE_TAG" DEPLOY_HOST="$DEPLOY_HOST" \ + PLATFORM_PORT="$PLATFORM_PORT" BUILDER_PORT="$BUILDER_PORT" \ + MINIO_PORT="$MINIO_PORT" MINIO_CONSOLE_PORT="$MINIO_CONSOLE_PORT" \ + INBUCKET_PORT="$INBUCKET_PORT" \ docker stack deploy -c "$STACK_FILE" \ --with-registry-auth --resolve-image always --prune "$STACK_NAME" @@ -248,7 +259,7 @@ jobs: fi if [[ "$USES_GATEWAY" == "true" ]]; then - echo "gateway routes are dynamic, no per-site cleanup needed" + echo "gateway uses dynamic map routing, no per-site cleanup needed" fi sudo docker image prune -f diff --git a/infra/Caddyfile.gateway b/infra/Caddyfile.gateway index af9d843abe..d005aa4990 100644 --- a/infra/Caddyfile.gateway +++ b/infra/Caddyfile.gateway @@ -8,24 +8,23 @@ on_demand } + map {labels.2} {platform_port} {builder_port} {minio_port} {minio_console_port} {inbucket_port} { + ~^pr-(\d+)$ "1$1" "2$1" "3$1" "4$1" "5$1" + default "" "" "" "" "" + } + encode gzip handle_path /assets* { - reverse_proxy { - dynamic a tasks.preview-{labels.2}_minio 9000 - } + reverse_proxy host.docker.internal:{minio_port} } handle_path /assets-ui* { - reverse_proxy { - dynamic a tasks.preview-{labels.2}_minio 9001 - } + reverse_proxy host.docker.internal:{minio_console_port} } handle_path /site-builder* { - reverse_proxy { - dynamic a tasks.preview-{labels.2}_site-builder 4000 - } + reverse_proxy host.docker.internal:{builder_port} } handle_path /sites/* { @@ -36,15 +35,11 @@ rewrite * /assets{uri} - reverse_proxy { - dynamic a tasks.preview-{labels.2}_minio 9000 - + reverse_proxy host.docker.internal:{minio_port} { @error status 403 404 handle_response @error { rewrite * {uri}/index.html - reverse_proxy { - dynamic a tasks.preview-{labels.2}_minio 9000 - + reverse_proxy host.docker.internal:{minio_port} { @nestedError status 404 handle_response @nestedError { respond "Not found" 404 @@ -55,15 +50,11 @@ } handle_path /emails/* { - reverse_proxy { - dynamic a tasks.preview-{labels.2}_inbucket 9000 - } + reverse_proxy host.docker.internal:{inbucket_port} } handle { - reverse_proxy { - dynamic a tasks.preview-{labels.2}_platform 3000 - } + reverse_proxy host.docker.internal:{platform_port} } } diff --git a/infra/stack.gateway.yml b/infra/stack.gateway.yml index 5f03eda5b3..53518a6045 100644 --- a/infra/stack.gateway.yml +++ b/infra/stack.gateway.yml @@ -8,7 +8,8 @@ services: - ./Caddyfile.gateway:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config - networks: [gateway_net] + extra_hosts: + - "host.docker.internal:host-gateway" ports: - target: 80 published: 80 @@ -23,11 +24,6 @@ services: restart_policy: condition: any -networks: - gateway_net: - driver: overlay - attachable: true - volumes: caddy_data: caddy_config: diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index 738aaf1b1d..ae3d1317a1 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -16,7 +16,12 @@ services: FLAGS: "uploads:off,invites:off,disabled-actions:http+email" DB_RESET: "true" DB_SEED: "true" - networks: [appnet, gateway_net] + networks: [appnet] + ports: + - target: 3000 + published: ${PLATFORM_PORT} + protocol: tcp + mode: host healthcheck: test: - CMD-SHELL @@ -52,7 +57,12 @@ services: NODE_ENV: production PUBPUB_URL: http://platform:3000 PORT: "4000" - networks: [appnet, gateway_net] + networks: [appnet] + ports: + - target: 4000 + published: ${BUILDER_PORT} + protocol: tcp + mode: host deploy: replicas: 1 restart_policy: @@ -83,7 +93,16 @@ services: command: server --console-address ":9001" /data environment: MINIO_BROWSER_REDIRECT_URL: https://${DEPLOY_HOST}/assets-ui - networks: [appnet, gateway_net] + networks: [appnet] + ports: + - target: 9000 + published: ${MINIO_PORT} + protocol: tcp + mode: host + - target: 9001 + published: ${MINIO_CONSOLE_PORT} + protocol: tcp + mode: host deploy: replicas: 1 restart_policy: @@ -109,7 +128,12 @@ services: inbucket: image: inbucket/inbucket:latest - networks: [appnet, gateway_net] + networks: [appnet] + ports: + - target: 9000 + published: ${INBUCKET_PORT} + protocol: tcp + mode: host deploy: replicas: 1 restart_policy: @@ -118,9 +142,6 @@ services: networks: appnet: driver: overlay - gateway_net: - external: true - name: gateway_gateway_net volumes: pgdata: From 376d303ddf20f6426520c33b633d3680326a444d Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 21:32:19 +0200 Subject: [PATCH 61/78] fix: give slightly different name so i can test --- .github/workflows/on_main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/on_main.yml b/.github/workflows/on_main.yml index a3094689b5..8e43c7e51b 100644 --- a/.github/workflows/on_main.yml +++ b/.github/workflows/on_main.yml @@ -1,4 +1,4 @@ -name: Promote from main +name: Promote from main (new) on: push: From c30fa084a69a394f0e948257921f105d3f4fb5ed Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Apr 2026 21:34:07 +0200 Subject: [PATCH 62/78] fix: change name --- .github/workflows/{on_main.yml => on_main_2.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{on_main.yml => on_main_2.yml} (100%) diff --git a/.github/workflows/on_main.yml b/.github/workflows/on_main_2.yml similarity index 100% rename from .github/workflows/on_main.yml rename to .github/workflows/on_main_2.yml From 0b0969f6e7815c995dff3c65e7aa45cc8f58094e Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 13 Apr 2026 16:32:15 +0200 Subject: [PATCH 63/78] feat: add prod env --- infra/.env.enc | 55 ++++++++++++++++++++++++++ infra/.env.preview.enc | 86 ++++++++++++++++++++-------------------- infra/.env.sandbox.enc | 90 +++++++++++++++++++++--------------------- infra/.sops.yaml | 1 + 4 files changed, 146 insertions(+), 86 deletions(-) create mode 100644 infra/.env.enc diff --git a/infra/.env.enc b/infra/.env.enc new file mode 100644 index 0000000000..ccc83bed24 --- /dev/null +++ b/infra/.env.enc @@ -0,0 +1,55 @@ +#ENC[AES256_GCM,data:8R4CdvbUQlCPGA8LXL+xavsG+fONmJnrk0VAJvMw8RPO,iv:XOKW14/x4f0WAlUz1Ll4yksZ4p9YWFcStSwYHxVZ76w=,tag:t22Tv8xNRHvLTLPs6vHLBg==,type:comment] +#ENC[AES256_GCM,data:RCV6DtYkYlDXTyWcOMok79XWD3tYc+C8VZEdH6jMTd5+VyXdT6NCHTVx26DoM4HWwmuZJFL/vkBSY8c=,iv:4rGsueAayH8qhXVF5tNshPlkc9fLORxvWzLJfydDE4M=,tag:yqsjQztD93Zlotyx8b9Pxw==,type:comment] +#ENC[AES256_GCM,data:DcZXFkquGDAnpVBz5aCFfd4ze79/WGu6+ncxcsR1rkun++6kW32kLpUoiupfVYuMLE6ai/wIy4QnQ4yzM7OFY5rjpA==,iv:ybEGFsQVXpfpOBkySwRKNFF5pUe2YSql2LctRetAJYk=,tag:wCdD1TRNCzVAs+Bedozf+w==,type:comment] +#ENC[AES256_GCM,data:vwpL6U/SKbgXzZ5CanbMRcBkyBauRNg+W05SFOhc2g271yoSfO3XILP/sMXULPpcUQ==,iv:8xe/zccfiagMOwUYBgnzleR8IlcOERkrZsGcmDJYo8w=,tag:8z2oaGBWB9C0R2qDk++cXw==,type:comment] +PUBPUB_URL=ENC[AES256_GCM,data:0j/39WwEaiEeqA84TXB8Z6vWYg==,iv:zLSBnMObjjz/EPGc6aPaQmTscVxwAwxP2wOaNwjJ6zs=,tag:bMeQdNbjU3F0N5u1Gk9POg==,type:str] +POSTGRES_USER=ENC[AES256_GCM,data:JCaDJs0hLfA=,iv:M3Rf9/j2G6IrDGAIeMWyC8LY30hkgb0CcaZspHTj9EM=,tag:LXZYmHqr1FA6nC/MOeb8VQ==,type:str] +POSTGRES_PASSWORD=ENC[AES256_GCM,data:cP6Aq2snXTpXrjKs9cwWBMvu22OUy1BVWiua8GFL56sRBcBNnCqIeJ0hSd6Ub3cOblcbmXp7DQDk3IssW0EtRQ==,iv:7YhY+iqhWz7FSsmRWztGykH1ni140wnYIWfWdEHjJnw=,tag:VJCbCa762HjIIJ6HhnvnYQ==,type:str] +POSTGRES_DB=ENC[AES256_GCM,data:mMQWx61NeA==,iv:UyHKhVAdVkp25Zuju0jAC5o7VzC0LkPWCHaUwThIDTk=,tag:XLagOgaHqQRjzer+MMUFUA==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:u3w2u1azHNS7HGgAd/iO1OlTBwOL7LhiJM14YXAvPiJlnpxcla5fx335ofEGI8Kwm82MuD+cW7sp6Vgju62yVqzOkAI8Qcc11JkwemtF6WWnbSSYK4bo0niCfbWdOw2HCG1vwwXA,iv:8jO4xd2kPpZSxzkZNyFm9NgIS+UqzI59FXA6ylEDWBA=,tag:DD01QIYV42pg4Jl1gqXZ8w==,type:str] +PGHOST=ENC[AES256_GCM,data:1Zs=,iv:mIZ6U9WgtB5qr6W0TAtP5Jugp0hrIRwseuECczsdxHE=,tag:YmHWjbkOcMAuc0niSbuGqQ==,type:str] +PGPORT=ENC[AES256_GCM,data:jI9xoQ==,iv:vuWJ/C4cxHCe5d1ErXRKwW3+4UEivu7Pc/ZFJH6Iwug=,tag:3gs5B+zTdK7DzFKB7iuDWg==,type:str] +PGUSER=ENC[AES256_GCM,data:Lvt7Xhd7mX4=,iv:FoxK8/06FGuU7LglhsHi5Gk/vhgoDhGf8AoaNrVfjdA=,tag:UZK92x7NYSnoSEMwciKVQQ==,type:str] +PGPASSWORD=ENC[AES256_GCM,data:Bx0PAC6Bx1aJ624SDWXG1nNpxFXHhGTCmkp8PeRmms73KhIQGYiuSimWQkhG0iBTrvmVVdSfOHsSgYzXXZOhVg==,iv:JgEc0Ah8WrFQsQb/TJsLQi7pIHy6nydVB7zFzgwcEX0=,tag:4fRKf1/dgQFC+TkspEOO2Q==,type:str] +PGDATABASE=ENC[AES256_GCM,data:Zu6f5LE7cw==,iv:AEMXV6EvZDwCpLUg0Tjgu36PG4+eV67GYPMogGWRhWQ=,tag:CCeEiEuWGC5O1oR7co6LxQ==,type:str] +VALKEY_HOST=ENC[AES256_GCM,data:z3OKSaw=,iv:NNpMxSPEUAs+zbBWM40VMBFSguEBbO7tvNqoSusDlbM=,tag:lKL1wSEal8l9VfVTIwvlwQ==,type:str] +#ENC[AES256_GCM,data:Uo6jviOfZVj5ekVkyvML4XpvffP/M7XD9fcJRA3V,iv:kjuIm6wgRv04MB3/Zc5uBACTWmsRVP4xpZ7KD1Cw01I=,tag:ewow4o5xBq3YYyGOSy3HTg==,type:comment] +#ENC[AES256_GCM,data:Z4qNl343G307ImGVqsnSLPTWRy7rzubX0jBvHjXpJf2rVw==,iv:UQ14+VCLhO/fpqL6Mvw2QAtqQ/WArEc1JhPWvAX+TuM=,tag:JT4HzuSn/ybQKDsv9JA2ow==,type:comment] +S3_BUCKET_NAME=ENC[AES256_GCM,data:0CHa4elkTI09bILpa/HUKU9KVM4U,iv:j9dd5/LU+pYss8jKoUEKWviH2WV68T4fDaJOzE+z1K8=,tag:kNu7FdOwSJpZg+hTl59TtQ==,type:str] +S3_ACCESS_KEY=ENC[AES256_GCM,data:nCg+qHHSQPCyF1sm5n10X1WoXuc=,iv:43o/RXg1uDyIHovcccjMCgkCmBg+WWtOZhBwkELv90A=,tag:OXlC8iVWe+n0Mh9ZjjUflg==,type:str] +S3_SECRET_KEY=ENC[AES256_GCM,data:lyNIMRnRk8pyT2/WiKxGykApRDq6r8BVP03vvXKmY96yi/S0Jh3wNQ==,iv:I1XitrBtevc16jsxxwqvUh2NjQTUkKhgJXuGuvFU6Og=,tag:SMbT/45tXe1Vu82UFMzq8g==,type:str] +S3_REGION=ENC[AES256_GCM,data:3h8WtozMpxEl,iv:0NN6IxXxOt9eP+V3GOlYXnyZJxRHrKDG7G70EWs2Ly4=,tag:FlJzOXyjna4p9D6wS2sxJQ==,type:str] +#ENC[AES256_GCM,data:iF3bohCYjgIt3tuVW2RTh/Q/raboMhlqeCotoTUl,iv:jadm9dLw6AG773nm2o8Mk1XFLeT5AMAlpCQhLQSfBpc=,tag:CFxhL4F5oHV5CGDf0l/g5w==,type:comment] +#ENC[AES256_GCM,data:0kOfNVoxZX/zTF1LuNnxMoZtZJ2LbB4oBhVYOnfOGErNAp/kvK6cDHcYiePo4qR+XCyWv/BnQO+/E1mY90G8Zp7ezA==,iv:EugzoXFvd4ar54QL+D6bOUB/kGlv1WqROWcOEp38GLY=,tag:JbORqHQad2WRjdz7AGBpjg==,type:comment] +SITE_BUILDER_ENDPOINT=ENC[AES256_GCM,data:WuOvCuz38194Vkh76Cvpu8xHnHjj824a,iv:i57rMi4EK3lkMUkjJH5HOC5D2lMGwv6zeahjksJZ6as=,tag:MHzrokpciI522LKR+W8fPA==,type:str] +MAILGUN_SMTP_HOST=ENC[AES256_GCM,data:Q9a+uyIpvX2k7ndPSF3O7w==,iv:JqjCJ57N8NWT1/Vg/tvE4qfyKbJhoZSNlE8hqirEVrY=,tag:1ClYH+Hm0THF27s27DhUBQ==,type:str] +MAILGUN_SMTP_PORT=ENC[AES256_GCM,data:FWIu,iv:ZGswGeogfT4Li4C1OkwU82sIknSSkBmZnXQgoTqZGl0=,tag:uiZ3kZf6aSTy+j7oPvPPtQ==,type:str] +MAILGUN_SMTP_PASSWORD=ENC[AES256_GCM,data:L3hRVfYy9UoGnXz4kRsKAYE0EbUUVG4rdAoS8w2JdF8=,iv:0FFv1ilyW9iXHtKyDnZsb/LyMFAxW0RUzzo+MSlWQ5k=,tag:fvJVwiq9yYKQBbxRkvrMLQ==,type:str] +MAILGUN_SMTP_USERNAME=ENC[AES256_GCM,data:3GVq7x2qzqQhIncDa/Xhlg==,iv:fdIL+YYBv6I+b/qEgHYq/n6A+cQSKpt0LX3NwUvbwV8=,tag:wUYMliJqrdO25PtPt5XxQw==,type:str] +GCLOUD_KEY_FILE=ENC[AES256_GCM,data:RNWFS8jcb6nlQxiigTPVahTkl6s4e6gpzy677xQi5Dv/ya36x0hUbT0ilrfHJHoAaV798gEegrPrxsHYu+Z6RNEvKtIsCsc9W3uDS2fT0hQD1Ku7Lf4oM1+ITOxhpM/s2v0CrxGOup6EdeEyW8hPwwsmkSpVsylFG+CwSUn7oBOuo6Qstd4Hn7QYnqwGmst974Dllw8Z4h7JF3IQ5brsUXZ5fBya3Xwmj5aYFNLpz/SlIzqBo14c8AC+7jOKSqUSsbnO2Gj9c74IPWmkvNt29j5ERGNUtIgNsN3tzpX1Yp6JCTcGKAQwlPcvdl15A1UZMENrqTa+JU/CWbi8jDN5JtL6OS72pw8ZIw6geSMh8zZ+bDyif3RxNgR54fOjr5TFhno2e77V1KFB03+vPZk47dFblowGINBFo/dMYCJMJZzU0+s84LgqaiexlxTmq/jdPzF6oPncIs34/sGFX5oDm/CBs8+hk9Xudlf40RKiEJJBH/SNCtsAoFTX8z7K3LIW8U7qeY/kBEg0aQ4QVN5QqWtCd+CtOIKUN/zEvdoSp0Eex6lLyclYPIqiul7Rfw8VFP91ORxN1tL+qNqKCsQ3TZuejcxMHXXJvP2+MwZSSxSmRYtSG/iWbukt7ejIFxq87bgjGTl6iNZ8DqPRzqVb5IigMaaQpt5OAQHZmVrW7XQahIvtdw8GU/+67/bMYn+VnBJbe5eJfPR8ifX7lezIT0F9UMoXdFDmspAE3+VIwNVtY/yaAAiQalXPIt8XbUG5jwfXZNDrhc57uVGrqGkU6iP8G3AYMMDJaL2EQg4iiMo7pTM/3zZc7KUZbTmi9EjGXFguavBaS1VmWUv/mZV6qh70WSRaaxfaMkKCEr9XYw/WuEZWBEzTIuR1Mdn1LlGm9PilN4dv/OyengqRkbp9KekPwneeF/Es1UHWQJssjOgmn7knvn1OehnMRS7/D+zZeEcoHDZ6Lk1xyl3JIFT0ImQ+8JaCUB3TpUHglw/ZzzDgUppUx3UIcrirDqY/868boF7bvSl7YZda7K+JPzTYaBGB5XbHMPnbtPkX0byFN7VreaBnQzRgdwCCJgsKrBFI52jF/GhOZYHFjG9y6jU081y6lphnUzmMC+Fi1TYeHbyepxeoDWRKojOHzT7urFxSOVfzataHVOgPKq8xYCmgHlh4of5O2x5/FH0b0PTcENvdTAcKSpJMoLbPGg0pA6+3+rB6mJ/D6Qy9WVp2bf/LtOeBttrMGjaS09FVw1aLrYYrBQFy8aWOtABW4Ku5BdQUZx3uD1oBZTjCuXhbGskj7ifpzTxIkd8hyEMZGzQeMBp/2VpeoRpY0qvZpEOWhMYYMPZO87t1DMdsN3HFWOuw+KHffN5/HI43xdmLxKAYVBLbRLOUtwwBDK0PhiP/z1WWuGry1aMWX0j0vzEDVoPs+ogKm4NY4vhPflXktInkoE4ecGfwetejjAZ4n7+MvZAfWzG5eh3p8wj0ci8Z9H1axICRS0JK412oIuHrR7T6n1BfGKV3RiLDAXJi1wGQwVTrceSPdvMJp8sp3LquraC1e/c4M1WjnqviM+8TPPrJhay19Gfhmegn4gbJN0cgBrd4mXSTe6SFRxJMMVmI6ThSHx6B0Xc1qEt9bqoLEDLibdfLuEG6c0RtbccHC2aOAqY4KAnybv8IkmYfHY8Xsv72DJhtx7uXM8YvGaLFH/a5JmH8RdS7ascAAV9Grldx2eI0U9oMLZ5elh55RAKQv+W8rmvvUcQtZ58rUs0qggLly0KAhjKvH1uHD4wPbRpaJXb18GT4yOZMdOzfkeOJ16OnzIvmKmS18HgEJ7TOI6TAgH/RvXdviG03Nz8e2DZgKjQWpNRItCqUeNFIrG0icwK3ZP9MMuRHMrhXR8cubE+9TCDG8D9EoMgXSROWGLp4r3fLcc+3NsW/AErYxlUkw4TsaXxDefqRmFwDWUPd+CKmwoKZXm/QYEjzKaa0Uo4s9vC+b4Aw+YNmEQCnVFmmDcQTertRKIR8UWZ/iztyezEsYnthTHGjjc2rhfa2SP7X/0wSRgHg5K8oBhxJsWqAaOupePq5o0RMBnapAY0jHqSNuBTfJ7qcmykn5on1SEQCDQ2WAD2hdDMEPHSr369g7id8MkDkw5gFMiYkLhB0ccwAXZXPbvLcS+U7x9HI4xOLk/JR7rJ6IQWHB36Ph6uEcjyS+h80qsiWggyrK91tMKjeXahOir5y335/0Cxvn0dslMyM4kDoX7BuSFCAbIo2Ev0WFc3HnUcqMF5S3FHL/mxBkxhJqFDGAdrrRUUkAb0/1LkC58Hgp6I/6nq7Ebd4efp+x6PAhLMH1OecZD4tm4Zwr8ZPPTE+m1V04Fyn7NmJHiSdDf3bmTD4A3cJNEep5qlPn/0UoskHHAPT5HdyVjjRS3Wf+IQK66kpNAd1t8z1N3i55iR0hWNiHYjfI2YCaIkvS2cX1Ym9GwJ6J5ay0MnC5bsO7KQv89sCnppL6AEr+C9xggF8Ld8LY1PfYGbXmGcxGSoHqm1Ytj2FgtVO1NYdG4q11PTbCclasgrM0fMruKiMAiPHe1akz9btcxhWzIdOIV5ya52obTKWiZOloUtXWQcK15gexqZQkzDv/+C5cWEnGIXEI3btfabu9ztm6F1g3RHlaT2KE87nLKrqE57ToWFO37O4ELW2fDG5fq2lMl1t2htHoKA7LdrmxkjtcyDWwx7gOEMdRmolSd9ju677fEZmsZkb4/fBjTSOMkOiibvXEDRj+YBfVbbQKVpleEWJD2J3yAAA2Oq3b8hx/mQy0gSFt25jsFFSiVH9dMvGA1xF5+4rbcvt1lYvpAISY9FRB7ETXXtAaguuG/Y7g15ZJVxjNbhfQNe3DFiOJHebgfQ9ILtopzG5KIOXsW8CI1ZGWVwekllavxRZYU7PG4caHXfMvXjQ70Y2WGQPgLOE5VlKsxwjgwSh48tz8zNKD43B0wa6zECwyCF9IA/w3AOpopZbYXdAotnWGWsJbPT6ml5bizuGCAhjDrPjlMrIrGa8V1NXCyzMA7k8ASzSDYLlpWkNzYJlkfgdPySJfJk5fgnCbwmffYdE2EUnLkIwYYvR1NqzaQnb3v+zyxvjnewsrkA2qJrunSptohfa4ZUqN0JcmSWg72kBA46gNzmdiJxtG6oq597UdSbltlz5C40orKxw37IL0NZe1hlBAZyEd8naAUc244UswIUHGgHcIIL9k4J1HIhtwwEsODXMkLafaFMPI2CwL4tXJIsHUmpaCxBQo+XOfEiYyYwYGZLh5hjEVbwMiY0xptg2SA0RDbMHZRwLCLpl4gPf2+B9fH5P1UGx9gxI1p2TRcbcEDe7ZTDw8eNzKUSSqdIttYHFU8shIN16FKRpVhfeosMSjpK7shWOMNDf8+52fX6J/sPOhSMtAXUm8fqTpPZzdJQsCvf8LPd6IodZ7FJp7/A2+Fr3C92KRmOxRpxcdnQNobi4LtG7vpqYuAcYOxnvxxeh92jR5uVNCKxMbRePdxiEVEpU4diNaEbq+q/FNP7pbWBtHflWPfbyv3JUUilmyCB7kwWHF7AMfyT01+DQaZWWvOM0KnMIuSGaYAMGkkXAr9pKbApDf0T0CZBLGLykq2PWP+TDvUhz13WxQ01JWELpaTh+sTJUQa5cdeO4d849Hr42NBZPIqaaTYkrsp7p1+iaE2XYsAEm/lprXOaCwO7WyaPXY3LPteU7FPhldmMeLziR5Yb7GXWml1CX86U1vOeYaSDdKdKSTVMpcpILu5qNyeyPe7IDGybm+I49wEZ6YTq5bF6bZ/CM9QoLZWdMeCM/3X18Oc+bzx9IKScikegpPqaopO4XSFBdNjciELRtTlbksyoa7+pC99jiDyMoETYAaNDxETZff+YHL0P+whX5EWIMxd6JkV17AoeAdYHxNBuNp5oa1i4DWodvYeCA3/98gVXGmtbnWsUHkgJywHLyg1sviE15mR2ySSDL24L2BjS1zgCSydqtQ1fVmbJ8fEvvuknrQ5BUl9RQP01yJrW6PiGXDJn8cBCCJnc/aW2HZONmsDWWAtFez04Fy9LXEeVMHCjSsh/gn+ctelBCgB9wA6lOc0RLDvzoUNFLNZQymQDe6/NDLoQk9BNIsFZ4B3bFifTBJ0jRY8vLe5hZLaFOnB4DO1I/Wby06A==,iv:G52ZyrWQzpMc+02er75fiA51YpsUIUIVP4ZZE6yCEjc=,tag:YPnTjfj78G2f1tI8471Mxw==,type:str] +OTEL_SERVICE_NAME=ENC[AES256_GCM,data:viAcAhInjQ==,iv:MJCmAvrNOGICRgD3Im7JeeMkJtZ1FMuGEVeG8lukfeU=,tag:O5X1MEUSTlIbuufpKZ/7LQ==,type:str] +HONEYCOMB_API_KEY=ENC[AES256_GCM,data:YCjppJ17Um/KYi5PFetVd/FtgVStrbPefzMk5JGy9EhpbYB6HXiZoMYzLKpGJdCnVNZwvR+ixxyDwz+H7k6E2Q==,iv:dTBXIOGfV2U6Cdp5WaT1ZBSp2cyvmxi7kvCxwXhLkK8=,tag:j3vQWzapd/B6HHALEcOX+g==,type:str] +API_KEY=ENC[AES256_GCM,data:xunOhBY0pBmBvkxQQdmo2Hd0UBWx1t1bIWL8DW9BP+8=,iv:Ko3x9MRhXroo4eTmBjIt/JTAbIBTxBjuu7ar9E/srE4=,tag:kW2OWSgW5PfZdp0cj0CM3g==,type:str] +DATACITE_API_URL=ENC[AES256_GCM,data:Rv1aOIalrpFtQmh+T793PGfFF9A8RweL,iv:PX1KXOqlllLjI+OZ/j0SaB85umjZCjDJAjmF+a4uzLI=,tag:18/1LdxC4vhpOkbaid0/sQ==,type:str] +DATACITE_REPOSITORY_ID=ENC[AES256_GCM,data:3V0DsaDWfuy9sRU=,iv:DktONx13OVNz99TIOavFay8roF+JlPEkjd/Itn70qQk=,tag:+0NlLI/2VNfPCH4sSMN/vg==,type:str] +DATACITE_PASSWORD=ENC[AES256_GCM,data:ot24zRAxs/pERb+q6yWW,iv:8Y6iMLloxrV1kN5IPZnfqgh3GbOAwZkEGG1ZeeHbHlU=,tag:VNtM8zwa1LNGl8pVPJ/aLg==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDcWxFN05jU0JBSDlrUWlC\nUkJxTFQ4dzRxWlRUWWE0SnRqVFdpQXg4UUVjCmduZytRaFFIYjR4WHdUQXNJUlRm\nSjJMVEl0L3FIcFZPaUI0QXk5MVFXRWsKLS0tIEtBL1AyQzB6RUdIa0hYbDMvSjVj\nYTRVczFIQUxKUWFmcE1FR0VkVjYybjAKeudXXAzu7OaYb6Q4bztrrGJcbxeFZLyR\nOjymgbMNN3DW8JPCCt2ene73kYvIyhZGk2Piv0I/ues7GktjIkaUvQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtb3Y0U1A2UUVOQkxidlNa\nN2dRVHpDUlBQcHVjcmtWUmlGNTkrQXRxRTFzCkFUbGpSQlJXdzNva0hTNEJmeFc5\nbnVQRzhjVU5uWURKSW5xZHo1UTN4aFUKLS0tIC91L3ptcTBCZGxCYmJONUNhVUxP\nSlkzc2pZV2xUSVFhaHdWOTY4Qk8vVEUKHW9evIXwKM7HUjIKRKy3ZR/lrg94szea\nclWHyT7y6vB++YOGj2VBtNP5jNXF5Wo24XDGeTWrfnfjCxvmM6uQ3w==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_recipient=age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKTXprbE9BdW5qOUxXTWxy\nMVdiY3JqcDJEcFpBSHNCV0g5Smo5Y1A4bTNFCkZ2VmZ5QlJwZUoxSS9jSjN3R3pz\nWjRXMkRDaGt0RmN3MnAzMWJaeDRveDAKLS0tIENpQWZxSDM4SEtrdlcrYWdIN3A2\neHBHem5UQ0FCN1lKbm55S0VqS01uV0EK1K7hTehy6Tfk1/uRjJwcHee63j0GeKD0\nr+F1m5AeOhlB1r4Vjyuk1F3Gen6EmHnwphBfPteObIeK70zP4gX2Pg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_recipient=age1slx6e48k7fre0ddyu7dtm2wwcqaywn5ke2mkngym3rpazxwvvuyq9qjknk +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxMkZkYTJFMnJtSG5obHpu\nTHBrV1lEL3pRTXpQcEd2SHF6d2RGa1JkdTA4CnhmTGJkb3lQd3U5akR5bTBSZFNa\nWkNaaFlJY0UrcjZZRlkvYnQ3RGdPUE0KLS0tICtxMUlJV3dXdE9wM1poNy9rVXJv\nUU9xK3dBQURGdEJiM1ltSHc5RUg2a2cKdeOVy9KVpLh1FfnKcNl7pYo0Xw0KG4cw\nW4Z9QRv9wTV7RWlEWsmxP3zyVn6apeC4JyoaTTozToqIw0Vacx90GA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOeFdTckFEbSs0QnlMN1ZV\nd1pUTFR5azhvelh4SGNBNDVvZFhoUzRuTlI4CmswRmJ6V3NiNDkxc1NRR3BON1dn\nL01qU0xtT1dSSmZzdnRMQURGU3N2bTAKLS0tIEpYc2NRSEdJQ1Q2dmpCQWRMeVgv\nZVptYlRCZmxsVmpmK0xWY2g3cEtFVnMKPOwfPurkRvhJpZEPLfyb7Yhu80iAHla5\ndxKD5vfN0H9HCtN06Fd3OIVLQBKJMXxHEzly/KEzM8LFcAAR6cs2qw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_recipient=age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2N1BxRURkSGhVbFhPUlox\ndzF6RXdmVm5DR2xhbVJhcmh4UEdDbXp5bEI4Cno5a1pMaC8vT2pCSkZoSlpxMUpJ\nRGNXYkNDV2VHMnJham44QzVMR1BxeDQKLS0tIE9kRHl3RkRORGdXZ2JSWmcza0h2\naW8xZEV1MkNaV3Zndmx2R3duNXNaZWsKGVraFkZ/+vRQScbrPJZZLN2erLJi1Lwv\n3ZFFIFSI5DkA3RHil5hoMpUDN5L02SAkizjE3igMBdVnrwKUfc2+Dw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy +sops_age__list_6__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3OGtSVjZMWEw4VDZiRXdt\nV1pQNksxWUN2MnR4N0dwWjY0S2NYbHVzNXpVCjRYc0FEZ1BtdWVSRkc1L3hxTjdW\nbmhMY1VFdmdqODV6ZmZqT2xhZmVJRnMKLS0tIHhpZzVkMHFxcU9QZkcxZG45Yldj\nL3R0SFhhSGxKUG5HTWVwNmFlR01OZHMKbI3B4FR21sxMJdt+Ma72B5k4GOPZnJlY\n4xJkO7i6N/hUm7T39GxP9PTa9wjgR51V4zRqlxheqWXh25rRMWeq/w==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_6__map_recipient=age15fsv503cl9ajfg9fphw45sfq0ytlptnr8wtlt48asytz4ev6rfks2pwt5s +sops_age__list_7__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkcEFuWkpJOUh5YUt6ZmpF\nOHFaTmxvTmMvclJWQ0NqM1UwZTNwYzl1T0RRCkN4dkxYdXBiSHhrR1R0ay9NWTZz\nUWVrY1Uwbkw3NUhCaE5yMDNHNGJySU0KLS0tIG5sZGtZbUZHcmR5TUk2d21TeUx6\nN25UYUQwQ00yNU1jd05UWVkxaW5PWmsKJcfcc7Y3VpSZZyqmSQZ/cqUPX377nsyu\nase6/yzKJxVia+0BaIRheJXdGWznWAXzKhgrHqe43V3xWq0CKcmfmg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_7__map_recipient=age14dssfufn8n0n7lcvyxxp4rq7zuf5pv0ek4zl9h7atsy85ceynfvqa0qz59 +sops_lastmodified=2026-04-13T14:31:45Z +sops_mac=ENC[AES256_GCM,data:qz90dbHKIYH6ak/BCu4r1egmr6SOgd9SlqNOseMGRRspTsVDYvXEtQsyQxYJu6kJbsp76vF+tafqV0bB9+SRF6n/LnoaXisDXGaWAS1qY7uX9DlBEwvVSmbLcxb6cQ47vhY+H80OwVvj/aEGb38C86wAk5IX7w/FIcPwX38iz2I=,iv:NYr8Hma43NUdLrqDPiszBxtKjo1SNi5KwPt/ZVY0leI=,tag:TD+pHm6daRRMGwczixcyqA==,type:str] +sops_unencrypted_suffix=_unencrypted +sops_version=3.12.1 diff --git a/infra/.env.preview.enc b/infra/.env.preview.enc index 2a40937253..c2d3f930aa 100644 --- a/infra/.env.preview.enc +++ b/infra/.env.preview.enc @@ -1,51 +1,53 @@ -#ENC[AES256_GCM,data:01FuyTOAi8hcwTlE3h5bzuxLnxIcAUUOaBWOgewecUak,iv:wlTDb983ry3QmHPTNO831TCRc/y1rNfVBdEBltsjQHk=,tag:Jx4ihkq9zJe5DrhKh3VzUg==,type:comment] -#ENC[AES256_GCM,data:F8lt5UbzAe/bl9JjbNo9PMP/XbpxNDqubPdlBIy5G7FlBXHKAs4kMOUrcG749TP7QfdjRbUVxGWTeRE=,iv:VCsmfGyoATmwB7ZA7q+cE/5hAdvhctqUR3V36uyvugc=,tag:+iAPVAkvIv5mBoKmyAcRXg==,type:comment] -#ENC[AES256_GCM,data:SfJkrX4rrOWnflZeLZB+OTo5ZEjmoa+hGALSh6OXyWhnMwYuWgj4imsw/Pt06BnD6mULcUX6w/WSbxiChpK6Rz2Uow==,iv:fxo360dsbqLLuyQRoxnbgLsP0XFurYGwCV7Q2mule/o=,tag:96vqOoeBrInNq0iAvHygFA==,type:comment] -#ENC[AES256_GCM,data:S1LlGLvEHPWvhx4deVBIM4LlBQCRLoyZBpwhLqqabmUHJmg5ZU4u7h15VZdxulQUCA==,iv:HB1P2F3SbjlG6WfU/M03X/Ix0iZQhvZlTCmQG+9Dm+k=,tag:fYGwJb5upU4BbHTFn5sXDA==,type:comment] -#ENC[AES256_GCM,data:18ixZWR90g7X5C3/wu/lG8mCRS5fvfe5PrCThAwBqCqSpYmnCmw3kX9NtKHVTwt1Po0mkw==,iv:hxsNw3R9ionzxa70IjaXLyk++KfW6lCEfCI8yot3Ecg=,tag:61sgT8b7SWWlW7u/hAbbUQ==,type:comment] -POSTGRES_USER=ENC[AES256_GCM,data:NykRLilL23I=,iv:uSUgclbI+u5HlqSazcXTdQMF3+6MPnF5gBqhgFrdxQ8=,tag:XFNiQkZRzfClqyzaPDqGGg==,type:str] -POSTGRES_PASSWORD=ENC[AES256_GCM,data:cjdQF1aMDBA=,iv:tkZMn6+06SKK95jTNmuTsdrWcuB3vQfTkOYaIKTwpCc=,tag:V09wYVU5J+mtkCrmQ9+2Mw==,type:str] -POSTGRES_DB=ENC[AES256_GCM,data:euB33ubvYg==,iv:eq7IpQpUOqO4RGLmR+i93W1Th9ldQxO+s6UOKMrP+do=,tag:K//krGc7ryYjXqdJoAZu/w==,type:str] -DATABASE_URL=ENC[AES256_GCM,data:0lZeeWD8t16+wVoVx3GG5avuecZvJtEjfRq3r3DldeNQq66C6umx/s84LcdoVA==,iv:pBG3RlFFjyUSgCEVVMesLd21GM/ChoPw6Kw5ZY9ErII=,tag:aHv0zEgRyjUO/HyjkrIShg==,type:str] -PGHOST=ENC[AES256_GCM,data:ti8=,iv:Y+g2zJxw6AZnm3JXLDuUJFtOkPUMiyTFCsm+0GoA1VI=,tag:2jp/X+VV7SNVbMUmC1e7xA==,type:str] -PGPORT=ENC[AES256_GCM,data:2RhQ/g==,iv:8rikqp+jWc6t8cJLLuLTrGhetFI7wu+LLUxI2QMCMAI=,tag:HtqOSp2JKUAZkHq6m6KDoQ==,type:str] -PGUSER=ENC[AES256_GCM,data:rjuJ2J89/Dw=,iv:BAK/cCaQYwyJiMV+pAVTRToBF5s12/A8WXTxT9RRjN4=,tag:n4hBU76ZReuJdNpCs+cowA==,type:str] -PGPASSWORD=ENC[AES256_GCM,data:EQHAp8G72pU=,iv:xKbh12AcnOBUcw+swC3McPYb23nyL12RV9iDt8qKBj0=,tag:IahOy866FQBSqaIvFCbq3w==,type:str] -PGDATABASE=ENC[AES256_GCM,data:nYwXkfTO+w==,iv:qq86WLG0XxFiwiQuhGiQXduAkqUjSzEgLPxJTnc02V0=,tag:VVf9bzQtqR5O56X+yMtyeQ==,type:str] -VALKEY_HOST=ENC[AES256_GCM,data:OlI8Ois=,iv:apN+8DyufGyGHh/+fGJG/4Zt1SSLHr8h9IJzWAVcc5g=,tag:mJoNwGnuv/yGygA4SE81Gw==,type:str] -MINIO_ROOT_USER=ENC[AES256_GCM,data:R1xiVwU4mHU8le4zaA==,iv:hQE2tkfqR0x2WisPx+g89YtPC1+YX4LkHyZ608OxzI8=,tag:fYJvFtC/vwW4hfFj2k32Iw==,type:str] -MINIO_ROOT_PASSWORD=ENC[AES256_GCM,data:JJoqNeEK/bEFlsP4xQ==,iv:pdBqmcJ+Pxhv3ZrIs5DTQSwHmsfjqhlbpCkO1Nv/OZk=,tag:sWZ0GxJfxQHYE51SFtghxA==,type:str] -S3_BUCKET_NAME=ENC[AES256_GCM,data:33QLz1weIvl27fkuz0M=,iv:4QWmPSPE2YpYP6dGc3hNzHq4EW2vRRANG0Sn/1OSQLE=,tag:0Tqwb5DvhwprvXeNw4YNCw==,type:str] -S3_ACCESS_KEY=ENC[AES256_GCM,data:GtYFUF+JbS57Nmw=,iv:AxwCMu9e4yJ/tiemM+Wyk6QqYdzQ3oZAjicDMKVEXWA=,tag:hXplo3UDLXy0suWJ2KGM+w==,type:str] -S3_SECRET_KEY=ENC[AES256_GCM,data:qV3HZp7Qem7u2+Q=,iv:Dwat6yRxAFMu2C1Lhl1J6K+iZ2X6/n9jmdjlz2N7AK4=,tag:YreDcISrsSTMpUPcGsHvaA==,type:str] -S3_REGION=ENC[AES256_GCM,data:liRqnsqwJ0Ct,iv:XUfhFifl/MZWf9Yy/02Bhjvlp4jBuQEML29dg+18SvY=,tag:ICTYnLtmRbVVjyYAK3nTnw==,type:str] -S3_ENDPOINT=ENC[AES256_GCM,data:jzzGDkgL33OjHDCn/JrCeac=,iv:4xyA52QskQeXZLaMWqAF8h+JYiUKNdoaBqoWknxLe0U=,tag:pVdb89AY/ZEirwQTpOwvLQ==,type:str] -#ENC[AES256_GCM,data:ZvethfQn3G93hzb9HylzPPV+SNHU1dLdUWddhZKfKYDXR48m4/pxI0Bfem2xEuG6N5vI756KFzhnjwDqEyk/zzKO1g==,iv:gqU2h8tctMH8UEixoGeze/u1osGk2HjPIn6KJkA7oss=,tag:bba0OSt+ukemN2PAA+m1mA==,type:comment] -SITE_BUILDER_ENDPOINT=ENC[AES256_GCM,data:P3Aohsj9hrSbbEB4cIZH5e4Dwic4i131,iv:NRSCdyFm+LcaoSEoin27cFkIQEYVHUZcnyKtHChlDn8=,tag:5jQP0xeUwsS8YU3fi3oNRw==,type:str] -SITE_BUILDER_API_KEY=ENC[AES256_GCM,data:h/Tt,iv:OHa3LPP4HAC+/6ynmQyqu/4kIOW5jTBhPLmmjBAXerA=,tag:UAUvhlNI9t/gMcG0bZAfzA==,type:str] -MAILGUN_SMTP_HOST=ENC[AES256_GCM,data:il5J,iv:2qhdXyt8M9Waj2Blp3/bZ6Lbd1CSnL44ni0w9BKMk2M=,tag:6L7LrjsV5dzd4SJ2CAOwbA==,type:str] -MAILGUN_SMTP_PORT=ENC[AES256_GCM,data:Cq0A,iv:HyZccNohY3LOBL83imGrKJRXaI5HNnOAbfkf7jsgUUU=,tag:onTdFiHQVfmMq38ffw7QLA==,type:str] -MAILGUN_SMTP_PASSWORD=ENC[AES256_GCM,data:Ux3L,iv:b72bNCwUdbI/ncfm0mozrkgnF7vPsBTFFWydptlh10w=,tag:HzbugC9DzQfnym0y6O91fg==,type:str] -MAILGUN_SMTP_USERNAME=ENC[AES256_GCM,data:u/w/,iv:+8s/rhemnPmmFFKg8HiB5KFT/kHfVwXrh6F7NKVBdwA=,tag:EZ9veppRkG4miHNCDKzETA==,type:str] -GCLOUD_KEY_FILE=ENC[AES256_GCM,data:1dSh,iv:rr9/qGYRH+BGmI+h12CmcG8qccbXhji7RPXMg3O2oTk=,tag:XeeEFc4MvUq80c9CWCL8pg==,type:str] -OTEL_SERVICE_NAME=ENC[AES256_GCM,data:geghunAlbZHbEr97CPbirtSo,iv:9DDQOV0iuBVAlLrkEMoPFAFAqxZQx+3wdea8QkzhbxM=,tag:WbpHuAka03EJLZSf82pjGg==,type:str] -HONEYCOMB_API_KEY=ENC[AES256_GCM,data:L2rw,iv:7ZXT6tLTPX8K8cdFFmr8XB9aM5PiXoLabzk+dXDAerM=,tag:RRicQ25cXLFSfXdHO0iXmw==,type:str] -API_KEY=ENC[AES256_GCM,data:qcnV,iv:8aVJLcGajFM/1Wclg6UiitCRRl94es2VBZycM0/yvII=,tag:VZVCW6CPEJeZV0T5LSfP+w==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1cXRndVVCQmF5OUg3RUtW\nS0RMUGdhcFRUR0poUis1cm5nclRZbDZsVGxrCk1EVG9kRHRPRzl3ci9KcVBzQ09G\nZmFvTG5CbXA1bk5ZWHdEZGRMWk9pR0kKLS0tIFo4dytQQzlQK3lVdTdxVlZBQ2M3\nMDZYSFJZTUF4MmNtOEtQSGF4SDduM1EKevzGXoKoCU0hEWKcecBnZ6+9B3kY35c8\na9gU3yrDoMP2fA7+6Iu5+cgJt2Ise+ddGSY52kg7XobY4DCg/Ufo/A==\n-----END AGE ENCRYPTED FILE-----\n +#ENC[AES256_GCM,data:Tmf1gdMIJLhR4lJ6BTT/O1eST8ezW6fH0bW8K50Jg0p5,iv:g3+uMFAaOEMQLmdB4zLwAoUWBJ+Q8gNsH0bqRvZPeb8=,tag:3HthTO2wGScje59GrMopCQ==,type:comment] +#ENC[AES256_GCM,data:JMCIHlRqS87GfZBs1qUrUaqwQI1ZGgKjllUAss8ylOY3AlRI3SgpyGda9GYo+LkAgd0QEnl5X4VX3uA=,iv:61dMSmH6VJMPAhEg7qGo9tjKEXwIwmnanSbNBKeaGds=,tag:w0X/RibOr0+ZJPVfcR25aQ==,type:comment] +#ENC[AES256_GCM,data:Vig5D+w4xAGfbynSGto/7vHBirx4N51ai3Ta5+UD2aTTHxgQ9CsWQsi3gjeyHPEpf4PQpGExISj981/PlhHKEX9Pkg==,iv:Jhm7E7A23kwkt03wopJO7NmlPXvIy1siQyjag8jUt4w=,tag:d460pfWrLeNEVRN11/3tfQ==,type:comment] +#ENC[AES256_GCM,data:tC4aiZrc50HCArjFTFgHv7+2AUC3/Aa2XAcIVeFsgx2xA8H6NoJuwPZCwd1o0HTTow==,iv:X45ag/rMmx4eBYRFSQuuPv66WULvq/kEAoqc0TLXHps=,tag:lCJ1st+2l4sKJtFtI5cJKQ==,type:comment] +#ENC[AES256_GCM,data:465TdUZCJf4xiATorcnOq7rrI3vnH8VXudDzsislTAtez8w0NnRtv1txlWTKa6knn4X02g==,iv:mwXHg8M6c1AU4av8hG1RpwXKMDDtT58hxeucmKiHy/0=,tag:cACoYb5j+J5hFhoRurfMhg==,type:comment] +POSTGRES_USER=ENC[AES256_GCM,data:sOTJhEwMr90=,iv:xXsLG3MizwwFuH1a2uC7WTlE/wBDUdj9+SAHTIZeh60=,tag:H2ZLi7u0SB09ATt6PhQrWw==,type:str] +POSTGRES_PASSWORD=ENC[AES256_GCM,data:JqTM0syVNz4=,iv:RIkSsWz2I6AldE65JM/txMaI9cYbjEUczVQe0MzvSkw=,tag:UPa+CE0FonmM+UDKKHviJA==,type:str] +POSTGRES_DB=ENC[AES256_GCM,data:qCND4tBPDg==,iv:aTL6gJJO1yA0JqcsbJLQtaQbVmVK/wX3wAc/XfgIk3s=,tag:AHpctuVy+L+y4vDO3EyZ0A==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:a/4X0qK41RW9JhlH3SsRsGZwPTPM8CgQx0czwfv9fSLGeSlFIa5Gd6naihWflQ==,iv:3b3MxxLsb2J1uVEIrqL5Hl430r/Rr2brK1C6Zi3/jQA=,tag:o0EKKFzTK0/csCwBg8otBw==,type:str] +PGHOST=ENC[AES256_GCM,data:S6A=,iv:qd0U00+HQfX8PGGifhGmJ5F282fK8+0x1ejqBYKJjR8=,tag:kVKk+MPHOxsdVkzQU7VKzA==,type:str] +PGPORT=ENC[AES256_GCM,data:oxUQ8Q==,iv:NqiCUtE5uE6GJ7aNtBDKZMGHGFfzLCaWfOh/Zf/S34I=,tag:0viecm7Xd9dHW6tGWJhuiA==,type:str] +PGUSER=ENC[AES256_GCM,data:IM1eXdo6KGA=,iv:ftKqX4wwsgWFPkW+bjaHzrcrmGbHgu++epUlpJVZkuI=,tag:r4cs2H/kBQSixSV+Cj1xWw==,type:str] +PGPASSWORD=ENC[AES256_GCM,data:m64uP4NF0F0=,iv:KYsJxHUrtePgVCMB1f17b9L4Ktva0ylOZy5hr0uzLtI=,tag:q7tqp7i/hw3MCIpuB8e/Qw==,type:str] +PGDATABASE=ENC[AES256_GCM,data:q1LnMUHmqA==,iv:U3u49slkA/io5SWadfeSBQrMMwo71WHYy64hBJdsWpA=,tag:54gUNHrJsROV08UYwYtOtw==,type:str] +VALKEY_HOST=ENC[AES256_GCM,data:LZ3lBUY=,iv:zQfWbER+x77lJvbBsbKx7o5/6VrgHPVx4X6vdyHdeVc=,tag:Uv1598lu9nieMKP2yKD5Mw==,type:str] +MINIO_ROOT_USER=ENC[AES256_GCM,data:A/EdIDbN2Yl7rB5KMg==,iv:l/XhiJHIc1Ponph7JTXZ1u1QQg4Il9KbsaRceoh+BDY=,tag:9ZX4TP1LjG3F/JS4NIcC4A==,type:str] +MINIO_ROOT_PASSWORD=ENC[AES256_GCM,data:xf27kjJQn7GaEfMzFw==,iv:Y7p2F7uE6rp8ntK7jF5q0pnHuZ7q0WNShEujvc73Ehc=,tag:6+s8VkcsCdQIY915VuDOCA==,type:str] +S3_BUCKET_NAME=ENC[AES256_GCM,data:LDUNR+q4X3uqXrCOlyU=,iv:de6A0xUtIu/+UTNQPExeg4ZoZI2c3MuyJi8HmW3Gsxs=,tag:mskab+X9jSeJfUPrYV/Q+A==,type:str] +S3_ACCESS_KEY=ENC[AES256_GCM,data:emKlJ8RJtSB+eKE=,iv:e+6gGFc9k9Ecnwu3Vc6Y8wiTejDI8VFqJTRpRa5Hyv0=,tag:L16xW/SzmQg7PjNkgxTusA==,type:str] +S3_SECRET_KEY=ENC[AES256_GCM,data:Fdm55u0duuHA8kU=,iv:MupG5XUiJi9aZTzIFrV4D0nt7sT7S4c5sLmx250G7iM=,tag:mdEXqwlOJ961A4wQ4n5yyA==,type:str] +S3_REGION=ENC[AES256_GCM,data:9AZAnE4TiOPH,iv:yRJK0mRh55+1I9nzSG/PIlYPYCzAoYKORewaUbkpElY=,tag:LHZVB/hh7TsVkE3LOexSAw==,type:str] +S3_ENDPOINT=ENC[AES256_GCM,data:x9KzNHCBo9m8Tqj1zdD+0MA=,iv:mISODtpy/HTycLXz1urt1T1miC/L75Q4OQXikkGpbjY=,tag:PK7iwt7OL7Thykntp+/AXg==,type:str] +#ENC[AES256_GCM,data:dMHOgIaJx8zyqL0t5g8AtEMSUgFZOH3+FJO0ukISqYwbCI7U+hTdJ2/xgFOKJOXDiTY3R2aFp69qRetqsCnkOKNreA==,iv:eNbvnndut14mh1GvWnUF/7ddHPRFHujj8WP7Q/bti7Q=,tag:1zSJg2PeZnZ120HZV5eFow==,type:comment] +SITE_BUILDER_ENDPOINT=ENC[AES256_GCM,data:KGN90wx1oIJnUiHVYfEF97QQvlPsNBn9,iv:1rDSTi9y+G+9vWk8UXQoMLVomrC7MsUkeGoiXvG3Cz4=,tag:RtWbFcIM84JRCIz6F89DEg==,type:str] +SITE_BUILDER_API_KEY=ENC[AES256_GCM,data:ZAMo,iv:lf/zIwV2hWnoidVtYbnvT/K/26gY8vtdf3byje/py8k=,tag:wr23vowjxNPa6yaP3CddOw==,type:str] +MAILGUN_SMTP_HOST=ENC[AES256_GCM,data:9TYf,iv:9bdOxzunyUddm1al5F5oLoyvssqzufVbO0Ab/rotGNU=,tag:Eh1d9HYHBBlZqbt4A4ahNg==,type:str] +MAILGUN_SMTP_PORT=ENC[AES256_GCM,data:g4Ia,iv:GU+kFitTxbrMCv7mOIFaRlssK3HItaNqqOYYkAiAs6s=,tag:1cvohPZLegEnB06nkat8Vg==,type:str] +MAILGUN_SMTP_PASSWORD=ENC[AES256_GCM,data:3G7+,iv:1jHrT502+KqI7bufvFIAZJpOkOY8ccZof2rCqCLE5ow=,tag:s4yyPpAE/hmVCn2QwJcMZg==,type:str] +MAILGUN_SMTP_USERNAME=ENC[AES256_GCM,data:kzMw,iv:GmUf3mPlDLlUSG47nae52OiZl3tXd7lUv4dsx11PurM=,tag:n22KXQ9LDjIW4LXMbU1Q+A==,type:str] +GCLOUD_KEY_FILE=ENC[AES256_GCM,data:HQ/1,iv:xrl9zcX6vMn8aZSiUeFljNdyy7MD4JZ+GUU4+zwdON8=,tag:FMvdqdKc6piETXCmTHiE/A==,type:str] +OTEL_SERVICE_NAME=ENC[AES256_GCM,data:xGDuXvq/chJXLJ49+NjjP8re,iv:UZ2qzdLAZhSXlcW/H+PMxSIAetTjAs8PnDY7A6N3o7o=,tag:mAdC4uxXkW3EDOy5dGxhOw==,type:str] +HONEYCOMB_API_KEY=ENC[AES256_GCM,data:0BE7,iv:K55o2HnMWx++XZ6LhLD41VnSF6wjPcFYfzpQWzLr5o8=,tag:OblGXqR3nkv2r+BiPI2ZVA==,type:str] +API_KEY=ENC[AES256_GCM,data:Y/8e,iv:3H1vIn/to+5vdcGUieGoNz4/SgNfIfVGrgm36bxUfYU=,tag:iqO99H5KHUz2qUyeisAZBQ==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4OEZ5M0F5Y0N1dXZYZnJk\naEN4dU10SWlRd21XVlVqVk52WUlOT1BhampBCkNiRm1oNjhaMWRoMVhrY0ZiT1FQ\nVnQwemc4WkpJSmJxRFFhQWJKQjRmTTAKLS0tIFNYeFc3UE1waWpHZzhjN2RlUjc0\nSzJhaDJRRTdXK3hGTEl0aUE3c1NybHcKo6kOkStvwnx7rd0bTP7RXpR+vkxVZwO4\nxoOg5D+yqYInjbSMQ6QXwp9YXq65esIUQWosL4RQcb28G9Bpy94ASw==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKYThXaFhnN0tVUU9sVjJ6\nRFY1Mk9rVDRFcjZ5eTZjbTNuNWpROFpXdXlRCkRNUkhhYVRjZjhRSEIxU1NJdlpD\nQVR5MDRMc2pZMnovU1NiOTdDLzBOSXMKLS0tIDhQUU1ReDlpWThPU1V2QSszd3ZT\nVHFGYW9YOTNmTmxOQlU5OGM2clIxdW8KEHDxeC7oWHY04chaVsUzutowlbQAlR14\n/sgSspLISHdXNw/7vksI62GB5OmbCF9/316LY01yClFpKvbGcZe3fA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuais4V2psME5SZTM5aGcx\nTGZic3Q4SlF4bEVxczV3dWpuZnZWZE9QVzJVClRvL0Y5QWVOa1M4dk9uMEEyUEZG\na1BWN2h5bjR1Tlh3SEdSODJzQVVSZm8KLS0tIDVpWVhVOUVBbkVmMXViUWhsUjJE\nN3MzbnlvbjU2T2ExQW5hT255QXJGVEkKCN+5m5iKtJ3kTapjyrT6hYvhWefzgX50\no7AZTgwnTHRKc6R3leYBxOd3Bj0orMQIW5I8kAlcBhCamEO9N2kJ0w==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj -sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTdFMrWVlYWnQyS0FaR21t\na3Nsb1FSVVRxMDN5b0F6TDBoMHNDNW0yc2lFCmpKRE0yWEhPM1BWbXBJT0U1SHNl\nNzJKSnJRREZsajBCVjFreDNWcE5XNkEKLS0tIE9LMUxOdUlNQzdoNnFKRW82ZHA5\nZ3JjdHFaVG94TlRZVUpFK1BTRXVHeXMKbUrmQBwDMgY3c3OlIeN7UHQZHkHTBzBP\n16trH9mVU3vsbYCxanrY+hy6E90LeaU5tnntHXb0HMqGDl6vgCNHYQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnSjBPSTUxd2ZWbXBnNUI4\nVUN2U2QzNFN5bVMwMURwc0JmeFA3L0dBVlM4ClJMZk5NNzhUSmZhNkVxcmpMYngy\nVzgwN3ZwVGUrbkx1cDhUMUQzaXlmbEUKLS0tIHNwRzZBa3kzaU0rUGZYK05vTUV3\nWWtua0x3VjRrUU9ZbituaUhIWGdrQmsK1nthXNZi7AcO131jznkmUi7PwouOU4MQ\nrqUa/hhuRna+qf/aSO84QRT1LhNwqjthpIr9KZc0Yp/CYGbl7vN00A==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1slx6e48k7fre0ddyu7dtm2wwcqaywn5ke2mkngym3rpazxwvvuyq9qjknk -sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5bUFyTGwwL0ZwaVArSnZ1\ndUJkQnV2NVhTQnQyK2JGSUc3aHAyZ3N5UVNVCkF5WnFPU0RxMUFqOUtQeitQbzdD\nNHRNaGZBNUtTMlJFbzcvRGo3YklmTFUKLS0tIFlQZHdTSVd1V0JERDZEcjR3Q0RZ\nZlc4c0VLalFQK2krQjcxT0ZHb21PcEkK0tfMklh/5YsvkJ4P/iIT1TjWEcmtYJob\n7kOqk4B10aA1v4NmeqGBpf+stqXebUblpQmEXq5gmMQFlawFWHFPIQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRajRHcnR2ZnBQNG5YZ3g4\ncFJaNjVGRk52NzhhZzBXTUNmaFhiZXdrL2djCkp5SlNhbnJrV0tUdEJtbEN1QUo3\nM3g2MlVSdVhvMklGTTlZcXdjQk5ZWU0KLS0tIEV2cEVoQWd0dGV6bjdmRTlaZitx\nd04yeWk5R1NYQ0JaellJR3ZQSC9jazgKxkFPQu1+/4BRbUtQ9Ouk9QJKp1JGXhyb\n0UZnAU0C1BXzxMFMrIzaFYd8kzoPhdRcx5u3Js+8kpxorh8Xjr0Bkw==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h -sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0Y0RNMGh4eTdNWWNobWlY\ncGsxTDVrZ2xmOGhnRjlob01sQ21mSmJvdWdVCmcwaVJBNnczQ2VXZmM3ZXlGbUVH\ncUtBMnRYZHk3M0xZcVZyQ29jMTVxSTAKLS0tIHdVNGNZWkJxaGZnNDBKMTFFSDJl\nbnVYZ0d1L05qbFhZWXoxazFvQ0tRYkkKA6qOZA6z/d01xmV6zomUA8JUrTUywKro\nUz0bkgsHnvElWv13CBaYEDMHZmkhWq+shnr+tYUXrXx4RLbq3a70EA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQd21OenNjOVBRUnVwNCta\nSlZtNGpGbWtyNk90SjFiTEc0dUMybjByU0UwCll5bHdHY3BEUm1jVmkzYm5lMzFJ\neWVERFg1N1FqVFFLMS9wR3JDb1g4MkkKLS0tIDhIeVRjN3BVNFBlcFhndDVEL1oy\nTDdHMnR0cVBySDN0SStlRlkxeEhNQXcKwJp57r6hio95y+fFapvS2Wl/7/yr4WaW\nt5rlTU6EWw2Bu5OFMI71hmiyDPPbYbQlbh7GZFs4+KRDv9yR1aiPOw==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx -sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBScWxBREtCWWZ3TXYrR3Qw\nbFA4Q05VVktiVUZ1RjZqUi9pRllwRDU3Sm1FCmJhbUZQTFZZL2kyN0tNUlQwWVlD\ndDBtekZndlgzUDlxN01MWE13ZVJaTGsKLS0tIFRpRlN4a3lOSWJuU2p3UWRsb2ti\ncnoweWV3ZlBhOEx5cXZzNTBKbDN3eTgKgV/v4wrEnBXdDe0wiT9iX/abq1RxJ6Kv\nh6USU/cUJqElUU5Nu8rYqlVyrJw9zF5hq+L0XomOGWyYdfwWW9qXaQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBacnNtU0UvY1p0a1VHWjVz\nRWZzemc1dDdJRWs0MjZTVWhEUTNmK3NDQWhnCkdveE1yTXFOL2NIMmFjMXlpOVVT\nOE9Qc21nSTl1MzJ3cHZNTTUwK2FRTVUKLS0tIFAyd3BJQndMeWxrSEQ2U1lIYVgw\naFBIaDF6cUNLR2dJYTdSYzltUVp1OGMKOOj0Y/vulG6/GDdqm0QfmlrhAsoP8wgO\nvayThyjBXYhAG+66RnfyqqtgkI9jN8sMpBrRKhBFghwV8iCAuYMW/g==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_5__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -sops_age__list_6__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzTS9mK0ljd1NRbUJtU2J2\nY1NHbWZzem9NZ3R6VnQvMEJnSm0zdG4xaUVZCi9zL3FNRytMUmd2eVU5ekxYZVpz\ndXVwd3Nrd25kamRMZW1wbHM0RUo2N1EKLS0tIHNSUGVESHBjRmQ0eVY4WndsRWlQ\ncWNqUUF5cUtDUFhTVFBIdTRUNjcxWDAKWtdnGqv+POmO3gyPfn4gxEF6bndbVMPN\nkr2jYlNlk+yLxin5t2eZXnwEdHKD6/JPrzobaV722Y3gcy4SMBrx3g==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_6__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4S3dtR3BjK1Rqb01NbzZM\nemlNYjlEbmsyeFR3YkNNd3ZielJlb2Ftc1NvCmdvOGtnVWU1T0xrSGp6OURYTlhG\nYjRNVHJQTkFvZVFYSVhOQnF4RklqbTgKLS0tIDJ3d0NjMU5wWERKRU5vMm42QXNB\nZ3liUkl2bUlFbThWWVBWaEpVVXNuWFUKunCGKcseSLVufzTaB/e5BS5VOCWvVWof\nlcjbWJJSMYl5sFakAm6fYVI7nwDXupfpRpPpmaCMj6TNhzosGYvdcg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_6__map_recipient=age15fsv503cl9ajfg9fphw45sfq0ytlptnr8wtlt48asytz4ev6rfks2pwt5s -sops_lastmodified=2026-04-06T10:33:35Z -sops_mac=ENC[AES256_GCM,data:FhzbTx/c9Cikj+q3nFeunA5flcDL/IDuLzbTNqFiMloqUl2nLgfqP/MnPzti3/IS45EgIQlK8Tc3prSN5B6NDKH0UN0M1xvbe9p2eeoDQJk8I/OqpqrhqkfnClbk82DIQvjps4TqhJZidu7LHV0g+lF33Uwl0F3a1rMwkRL6umo=,iv:SkKnb7RqNRQGix2rKrv0oALUDOc0leChmb+abgcB4UY=,tag:HlDtB4atTYGXEt+0OCnSMA==,type:str] +sops_age__list_7__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJTlk3cDZJY1NMTk11Mytn\nWmttVGw3bE9TVnFOeUVnRzJCM2Z6cEU1eVhNClc0ekxTVS81d0s0VXB6U3VwMm05\nb05teE5lcEduak9kc2FRS0Q3Z1VSaTQKLS0tIFRhbnJ1Zzc3NkZHSWNhalJ4UHFC\nZDhZUWxNcWhvdWNicTd6d1dEWGVaREUKxO3w6806edbl1Rhvio3bpv+wcL+sCcCb\nxbV/IgLmbzmqA01HW1p93OfDtZtEy8jezZCpKPHoAgRiu+rEs3cT5g==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_7__map_recipient=age14dssfufn8n0n7lcvyxxp4rq7zuf5pv0ek4zl9h7atsy85ceynfvqa0qz59 +sops_lastmodified=2026-04-13T14:32:27Z +sops_mac=ENC[AES256_GCM,data:WCLvmygTPyk3i+DpenmlsQozigDD0G24+zwlvsyV/CJ92GnihXji5lGPaY23Z1K+2pPKdyPhft0+z8z8nbhHX+OZtDGQnkOjszEebt9f0bNmaAwbUDXm4XJCgQkUpPxN5PqVAFBWXzVh0CnDNEfTFSVOwAJau5o1PAfBtuwpJ6c=,iv:0vxrtsNONxHh5OAjMIY2rfgpDjnGcHIRvZFrlduLffQ=,tag:V0VZ6hLbw/HtcvIQkUlFrg==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.12.1 diff --git a/infra/.env.sandbox.enc b/infra/.env.sandbox.enc index 71249e26d9..88b370812c 100644 --- a/infra/.env.sandbox.enc +++ b/infra/.env.sandbox.enc @@ -1,53 +1,55 @@ -#ENC[AES256_GCM,data:EUk6PIzXsseMFS4wuOnFQj91hGFwV1hzfi3w2DQtroWV,iv:Gr9+RpcxLEZ0/s37D5tEdejqHCKFe2PBonqh7J4NKUQ=,tag:ELZGUZkCUoirFfUC+SWTmg==,type:comment] -#ENC[AES256_GCM,data:zD2eJw0olC7BZUYmFfI+ui2rJOHotGFvfXsJtVV6Fs9eddGy2puY27q1MqfFJZ1HS8qd1YpWTqprPb0=,iv:WM22N8dLhJSE/zNA+ASjwENeSuZ9Cfx+FuF7T2tM9Yw=,tag:CBzIFOJIyHUJprY0+fAvJw==,type:comment] -#ENC[AES256_GCM,data:GgsiNd0FA23Ml4AOVn0gpzzjrFimTi6QEBIDJoWjR8lFjNSEQIbBUzdwSBUJtRW9/kpE9nciaRioyRhuzbf5riEy4g==,iv:v/rX0FvZBCVQjH6KJvOsRvBOzzapSpJ1S5xtD5BeuJ0=,tag:va2QU1onE+vV7leJjjuajA==,type:comment] -PUBPUB_HOSTNAME=ENC[AES256_GCM,data:0rThpmSzlemAo9bEedewZKEhZw==,iv:mHZIqB+V3LY7qkxq8aSmd+J8OLTpDJQAIK/r7Sr9zJU=,tag:RPtpmXfP0dzNn2h4WDREKw==,type:str] -PUBPUB_URL=ENC[AES256_GCM,data:hZlG9x0gFG0f0FzC+SPCWnV1s/Tulm18OhOY,iv:7UM8dBIV3yaGOakYSkm49dpEEnoWYojpoNppLHfPGFY=,tag:1tu98rRDaRdiD/aHBVThNw==,type:str] -POSTGRES_USER=ENC[AES256_GCM,data:uZ5vn1rC3WA=,iv:TkJZImdU6aafkJ0Ye6cdaT3/Z+w4/7QemlAS6guKCus=,tag:UjqhwiubHrWGKKZqwLvu8A==,type:str] -POSTGRES_PASSWORD=ENC[AES256_GCM,data:hCmhLFmyazk=,iv:7Cyq18Hb4iK5PQfBqb4xwJ8vL2POEJyElYUa5hx93gU=,tag:pH0L4Z1ijcO9kPMQIdKRMw==,type:str] -POSTGRES_DB=ENC[AES256_GCM,data:ISSMJidN,iv:0xkYpUb1dg9Bp13XCdIPIJwelfZGd07hNDY7pIBKJjg=,tag:wtZVSRcSuW/oSGqqHWX+4g==,type:str] -DATABASE_URL=ENC[AES256_GCM,data:3LAclVCQWrG+7CdFXPnj47QVzEvXAyUbQDesSeCl4jZ4TG6dl99x56fJTBTF+jYOszdkjnps37yQ2du6U8zFoVHUdbgh3bEJBw==,iv:sgxvIo8HqTZDrFQGN0TawKvcwnbRCV1mKf6fAwbxepU=,tag:zKeU/TY3o8vo7kWA7x05jQ==,type:str] -PGHOST=ENC[AES256_GCM,data:Wu8=,iv:bf6higOwpwLgZ2gSHU3vtsBWIknD8IqSRE+FJymZNvY=,tag:xWgR/KULnc5q+ht5r/q6sg==,type:str] -PGPORT=ENC[AES256_GCM,data:wUDbkA==,iv:vu5OWULm4wUsdvla8ng9MYYuavj28z31xRMH0FkpB5o=,tag:iie9hQz66wEC1ButsZNC0w==,type:str] -PGUSER=ENC[AES256_GCM,data:w2SIl1KPFkKE8XWEZEPApg==,iv:qcPcosmKNdNBap2ZvXkODc1+AL2LRCpuyoySXvZx97I=,tag:9l9dP2RqRXe323VIuwHbNQ==,type:str] -PGPASSWORD=ENC[AES256_GCM,data:tskXQm/SiPq4v/mtnzfvZZjqhqU=,iv:kMCrdDbO3IIYrr6Fq3wlBDnFAKeFDX0XXA5CMFfpksc=,tag:acZGDyb30y7ZShxmtIOEYw==,type:str] -PGDATABASE=ENC[AES256_GCM,data:k7pH4TISVtrgCQZBNKQ=,iv:FT/tVKCe8Uzj6fKt3tjGEQ5sm/MJZTb2cfCcbhH8yzI=,tag:XERwNJlVJya6OJyGtws6IQ==,type:str] -VALKEY_HOST=ENC[AES256_GCM,data:eOykTCQ=,iv:bEYSu68W3pHnDPJBG4qeBcnr30gWHgKRTS0QiLH6Jjk=,tag:fXp6f3nNtKnXACcRbzax5w==,type:str] -MINIO_ROOT_USER=ENC[AES256_GCM,data:B9teVQllUozOjWAclQ==,iv:WlwU2agbABSFfZ0Ai8MAf3nLmDm2f0ngXjyYV2ctZVg=,tag:pfJesr2Wc+1JkT6cS4Qdig==,type:str] -MINIO_ROOT_PASSWORD=ENC[AES256_GCM,data:m5IhnP4bmLwvhRH1MQ==,iv:YwrFzZy4V9DTgIHB6kjuaHkf9bYR3qlWOg387lioC2E=,tag:j94mHdsseLMCz2qWX1XN0w==,type:str] -ASSETS_BUCKET_NAME=ENC[AES256_GCM,data:J7+yZke6,iv:nmGO4MnTHkF02hT+Swus0AdzMUnLQAqnlVQbmaV8Ibc=,tag:+lygXbeROCrFv7WY0FniOQ==,type:str] -ASSETS_UPLOAD_KEY=ENC[AES256_GCM,data:2MvQ0tTCJKlELzw=,iv:mVOi3FAknw098g2AY+h43mbtZ9YT3PNt6fkOmzBTaKo=,tag:cDIcHbJvB8h6P5ZXULv1pA==,type:str] -ASSETS_UPLOAD_SECRET_KEY=ENC[AES256_GCM,data:fR04nwkgZ8jFC0s=,iv:lJcMu0xS5AsKN9X+LZ70RELGF4u1eI0uKTbpyQcoTQQ=,tag:SSaKB4Sn620gwXm9ilRYkQ==,type:str] -ASSETS_REGION=ENC[AES256_GCM,data:AHtDF/GoJc/C,iv:F7XuISy3MXp77A9xAaMryKs4mQZPrb7Oebl9bBVAfJQ=,tag:kK4IHFGO7tCsV+16Xg4Dng==,type:str] -ASSETS_STORAGE_ENDPOINT=ENC[AES256_GCM,data:NKCglvQc9KIoARg9kNhx8qM=,iv:d4RdlJQURPKGeNDlXDONDkTW0vA1yYCvWQZlRLQh2EI=,tag:AdpBoDVZ2/IznMTlqPIxrw==,type:str] -ASSETS_PUBLIC_ENDPOINT=ENC[AES256_GCM,data:PVQHF4MFGv02V5c5l2X/UYsix+q5E4Phim3j7qhuXkv+Nw==,iv:+wE5+eDgFT8CPqDrzXuR8Jc8sm7Et1KIGXMuMBkXK/U=,tag:hSEpd5211TbOINqpT9lpiw==,type:str] -S3_ENDPOINT=ENC[AES256_GCM,data:GE+jlsFsEhlT/NrhRASHOUo=,iv:UgnJ9aS4Et+otQCIyAQN0A6FasbHLBVxeYuG+/BW7Vc=,tag:KDtGbGOWtME8RYGMYEeY9Q==,type:str] -S3_REGION=ENC[AES256_GCM,data:bfsZhVI6SUQt,iv:eqbKTZ/JEi8CViY4nbOLIp2txcdUxXk+BFfPQt0oYXE=,tag:XpbvEPh1dPTC3dfn7HZxuA==,type:str] -SITE_BUILDER_ENDPOINT=ENC[AES256_GCM,data:WaJmPKLU0xgz2filfafc7YfCXKNpQLrt,iv:qbnYSvd+j/vJge3jdckmjhg6GKc2+QTK6EBAE34HA4g=,tag:tfbitWsNF2k6hSfLoawytQ==,type:str] -SITE_BUILDER_API_KEY=ENC[AES256_GCM,data:nUfu,iv:YkcgqQqaw3EhllxJrODxKcxK6XLRamKl6R409oP8wLs=,tag:kAei09a2qd9iAKi9AXjbXQ==,type:str] -MAILGUN_SMTP_HOST=ENC[AES256_GCM,data:jTdA,iv:ulzrjrcYakwL0CVZLmyhXkHXpq+xqXbQiQAKQL11P6s=,tag:ObCFVmmbd3VN620xGQKvxw==,type:str] -MAILGUN_SMTP_PORT=ENC[AES256_GCM,data:+PaC,iv:4yAKdeIlgnF6xHNOT0Z6CR7Nu3rG6waJCvkgfhNOFL8=,tag:jEiAaXp0kUn3s+cP8bjKyg==,type:str] -MAILGUN_SMTP_PASSWORD=ENC[AES256_GCM,data:Fkgy,iv:tDcWCgTpMQvAhMXzu/HokUCYNlzfiEc+FjGcI3VkaZQ=,tag:/a9h4511XDbmk16s+DmcCA==,type:str] -MAILGUN_SMTP_USERNAME=ENC[AES256_GCM,data:OKMx,iv:38RLXgWXNzA/XgJ22Qil4ZBo2Obo2uKHA3SYtWfOCC8=,tag:2LhnzeXIkabpXBwWXNTe9w==,type:str] -GCLOUD_KEY_FILE=ENC[AES256_GCM,data:eXcO,iv:jOd0V1n+g/CV04x92oLLD7BHj0FOaTKjTqeOv4GdT+M=,tag:M1pg4Sf/VhDIw/8dQRWtPA==,type:str] -OTEL_SERVICE_NAME=ENC[AES256_GCM,data:/osaHe00IZD9Uz/gDyca4Sg=,iv:cGZ4j0VC4v5Th8Qn9r4hT3vH6Ne6SE4EA/yLarIWHNI=,tag:an3A9I/ze6AYxwLVEJiMCA==,type:str] -HONEYCOMB_API_KEY=ENC[AES256_GCM,data:47eY,iv:tMJArSXqZiDqWiotXFmFyW0WqR+3kvkxuCk2lp2IhyU=,tag:3qWyEBnHXFlm8qfniQJ93g==,type:str] -API_KEY=ENC[AES256_GCM,data:r9MF,iv:7JBUNs84+/+l9vWP/eMY51YcvxeiCAWYfBRfKZQUSpc=,tag:i2eKY5CYm2dDJmZFV5S9Ew==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXUmtQMy9ZREpPa0NLRGsv\naU1VMW83cUJlWjFTMjJreW42UnltWUI0L2h3CmJxdzRXa2FPaERHR0xyUHY5SDFv\nb282VktXSWlrdk1NN1hSbzgzSDNhYUEKLS0tIHg0MHR5WHhJcDFna0JDbUR3c0I5\ncVJqaGw1UE9VclhOSDNaeUR0UzZVeEkKmrjBiCf23E1uxNtCXr8zpVvP2gMAdUFC\nV7fk+zpa3443cc/2AvSAiWxyXHblykN+cxc+suNUDNv1QtQlqG7RIg==\n-----END AGE ENCRYPTED FILE-----\n +#ENC[AES256_GCM,data:jBfsGj0lELXA+9bOa1c+baucIqv/vk61JiHTI6pzIXPI,iv:EMbgYPTznNSNrXxiotkSquzG0BSu2OKMP1GsIA1F4zE=,tag:QNXLyFBU03BxlRrYGJ7ulg==,type:comment] +#ENC[AES256_GCM,data:nGC//rBOZ2jFyAL+6WVggvPsNjNQFJvWMXk8J1RhMklyOZ87w6ARRQF8xeFsG//A0L3fs5Yl36EgXOk=,iv:3s3xYuOvtH7izw3tEG0OfXcnM23NDxsgTGSvL1TYngo=,tag:rz/9wrr3zjYXNRGVy83gPA==,type:comment] +#ENC[AES256_GCM,data:vtK18WGlINUoV9db6JE4jLrml+OY1aNIB+HnmfIyw4Okhhq0ssX0MiArZJYK6nsHCCZF6Oq6R2lSug8fM5whzHhTfQ==,iv:jkmfJMOFoRt5qdim1iqhRx0BJTpET7948pigLfQ8TTA=,tag:rwl8fVR5yAlIxfwJJrPnHg==,type:comment] +PUBPUB_HOSTNAME=ENC[AES256_GCM,data:4+onYA5m+li/w4hgjvEFuvv7mQ==,iv:LpNlUY9eAbmxAXC39KURh/8JPU3f22qcPjyLdCeepvE=,tag:wIxAeb5zBtcWsznLu8b6Tw==,type:str] +PUBPUB_URL=ENC[AES256_GCM,data:A+ETgDe3dKGbiZhzZx8xInakUzMsVPiVW/w+,iv:KUnoOExAhuBN9n9qp+FmDTgI3OJkAY9jJCGUhaqAHUk=,tag:aHPRu/bxGSZ6S0HEJP+0zw==,type:str] +POSTGRES_USER=ENC[AES256_GCM,data:fKf9SvRalyA=,iv:7BlAXYMc/sgyVGM+Q2/RcReWgtR44OX9SOM4hhxWK9M=,tag:qDzVxnVu1+qkTyTMDA2AvA==,type:str] +POSTGRES_PASSWORD=ENC[AES256_GCM,data:uFpP+8ZK7dQ=,iv:+gWxhFVelmoYyFA5VlgyVs6K2ZLKKN86kt6RVhk1WX0=,tag:eb6tcb4mbn73sjgJCj7HKQ==,type:str] +POSTGRES_DB=ENC[AES256_GCM,data:lBg/ArB2,iv:05tf+p3Auw4uF/egEkNOHjSQywY3MU0gTofCMNcabY4=,tag:tqTtFUYAH6dEVZq9vfy19A==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:0kjz4bx91yGHIcsnQQQ80o82Lk58l/hj2M8BcK64r8mbNRJrfz82ruGwXe0qvYUi0nLy0KSNMIg3DdPBGI+wfYg9XP4V6W+1EQ==,iv:qypCSI6KWoEoHGWkBXGBnrCcaUFvxQyLakTAFKSvKZw=,tag:uyA1EnbSHDVk5kyim8W03g==,type:str] +PGHOST=ENC[AES256_GCM,data:eiE=,iv:PvgvxfTdux4PqO2vkVHclv6SzXsn3fXFuBZVxv1erJ4=,tag:F3jbsWKn9yn0FLF9LuVSRQ==,type:str] +PGPORT=ENC[AES256_GCM,data:HxUoYg==,iv:169zP133qf1VYTgDtZUy7Nar5ZmyItarnnRclXYCA6M=,tag:viiX72zPhFl1cauz5LAW1g==,type:str] +PGUSER=ENC[AES256_GCM,data:mzP+0pNjyYgEBb4QovkvTg==,iv:5bP5BJTLRomPDLditZdpUmV2oIDHwvKC+1VG8Rr2MZg=,tag:8N/zolPfBMl3vb7AqMCuqA==,type:str] +PGPASSWORD=ENC[AES256_GCM,data:AggsmIoWpoNcYvUaEAI19NUoR7Q=,iv:cgpa+YPhiqWTFdamly86cYDZwPdtCegCGBdgKkunpfw=,tag:VuT1LWMC3b6TF6PW3eg1EQ==,type:str] +PGDATABASE=ENC[AES256_GCM,data:IXRXa0dGj3WkKtpoits=,iv:Cum3Za1lzltMA09wRY/tLCqHzkmhFB7oLQDh97yD2v8=,tag:jPbv4yNqMJRgBZKrW5ch9Q==,type:str] +VALKEY_HOST=ENC[AES256_GCM,data:qh5ksYk=,iv:it16z4hlmi3DTZW6EQOEcSRGFnpQ5PDhomneXcMG3Gs=,tag:TKSXCIKy4EdfLJbCnw5Wtg==,type:str] +MINIO_ROOT_USER=ENC[AES256_GCM,data:YspKA8Qhw3V/NFZFbA==,iv:3fxzt0hG+iuJITiK6rD79w3smxbDxywYwN26IQpIwUM=,tag:Xl0w6j0m+MmqaWKi0VvNww==,type:str] +MINIO_ROOT_PASSWORD=ENC[AES256_GCM,data:Jrvkuwh5NjSTL2WCVw==,iv:Pf5mUuwnbwwkPqzllwvTGykMHwXBVPyJ8NVI6ERxs2U=,tag:qdpdyGL4KgkkV+9v0YS47w==,type:str] +ASSETS_BUCKET_NAME=ENC[AES256_GCM,data:n0U/aYZ5,iv:b2LjDM45jnjXkEPwJ/RDyJqQkGJ5KvFD7if5MBF8Cdg=,tag:+SUUSe9M0fWlcXw9kEGZNA==,type:str] +ASSETS_UPLOAD_KEY=ENC[AES256_GCM,data:mZD9n56wtSXZaQs=,iv:mYQQuk58xMlraigwUWrf+1wt/S+cUZ7jMdhfYDEedpc=,tag:Eq0XXTxekzg/ioGdi+8oZA==,type:str] +ASSETS_UPLOAD_SECRET_KEY=ENC[AES256_GCM,data:uPF46yiEEcFQuh8=,iv:CjH2+eupf4jwxQ23QS9XYnhIv78pmQQ6N6w1E5JN/nc=,tag:zQjA7KtgwQ+6Vp4O3LLzzA==,type:str] +ASSETS_REGION=ENC[AES256_GCM,data:Nuj+Aao6z8Vr,iv:p4iQePHZsoR952Jbg/voHQbl6c3cIBhs3LMymX0auWo=,tag:0R7OtuKA900dFuP0ehbKjA==,type:str] +ASSETS_STORAGE_ENDPOINT=ENC[AES256_GCM,data:sOgNc94JqJUEoh3wvelMdXw=,iv:OVlUCsS5KdOxfQsZnvPZMdB0qIaMLamkn6hrSH6/0bE=,tag:x6Ph3624JjnKoODM9ZZKAw==,type:str] +ASSETS_PUBLIC_ENDPOINT=ENC[AES256_GCM,data:E9shxLdr4BYEVOHUvx7AWKgCh5ApVNrjFWIBidKxkHoZKQ==,iv:7e6nW/l8eNFNciT6xhj4fappCWDW8HAonLViBmcVAD4=,tag:s4zvj3cCAyTgfvHT8DVwPA==,type:str] +S3_ENDPOINT=ENC[AES256_GCM,data:XaJhGFvSNHsWnPaQNawdljs=,iv:I8IEr00Xzo7NL+U0QEKMG8hyeG/Uv5rgK/BV8x+wacw=,tag:XJg/snjcBiaQWSwSyDHA6A==,type:str] +S3_REGION=ENC[AES256_GCM,data:i1ioAs9Zf7W1,iv:iBUFL5iB/xpEZjmno45RbSHOpgvPBLEZk2Wj9wzJIj4=,tag:NZahlJP3P7b0XQS9eacCSQ==,type:str] +SITE_BUILDER_ENDPOINT=ENC[AES256_GCM,data:ykZ22VnUbvvnWcARbw5e5GXMM9NTuiJb,iv:1xvAlIPRMDDCMifr8zc4baSUYyNBVPpVMnaOcENPJd0=,tag:evgWhFZVvDJWQ3aJy1cfOQ==,type:str] +SITE_BUILDER_API_KEY=ENC[AES256_GCM,data:q8pI,iv:seYXfOxntVyrw8RQfTtYtsGhWShusPyjO2L1rJGRZb0=,tag:pc24Cp9LlrcTaVIOnHNFKw==,type:str] +MAILGUN_SMTP_HOST=ENC[AES256_GCM,data:6U/g,iv:q/nEH53LCjp4vP26Ow4vTiHMlZryeVbiFMS4cCpW90I=,tag:z3s7CxHOLQbmOURQTY0Lug==,type:str] +MAILGUN_SMTP_PORT=ENC[AES256_GCM,data:WGTr,iv:DjIQ4OKBKhOKMbWOuJ6KRwaLww+hceRME2t1VsY8Nc0=,tag:Yo4l1QoR3kCwvW8JlrAKIw==,type:str] +MAILGUN_SMTP_PASSWORD=ENC[AES256_GCM,data:w5bE,iv:4unkjSu+Lw+rjG0pywisubCl+LDhz87rRTVNGiQhhcs=,tag:JDrnj+M3LBH8bghHKJVKvw==,type:str] +MAILGUN_SMTP_USERNAME=ENC[AES256_GCM,data:67lp,iv:+fe8Fhz9CJmtYrgwceNp5bAgF+LmDDhspylfSe79cL0=,tag:hC2ugXr9VJxbqc0ZfN1T7A==,type:str] +GCLOUD_KEY_FILE=ENC[AES256_GCM,data:Xfby,iv:Vk76o/rl73hNVW5E57RxAp2iff+hmDJJiEQ3ITQHNJA=,tag:M1nVkFSFSnqhFRFiUSM5Fg==,type:str] +OTEL_SERVICE_NAME=ENC[AES256_GCM,data:rI0rM8obYtmeD2nNJ6miYLc=,iv:kuEytDV2tnG63x3UTwgQszlkEoeGUU1RRN3i68/BMzM=,tag:FwZgxLwU+HAFFqF1BW9jLQ==,type:str] +HONEYCOMB_API_KEY=ENC[AES256_GCM,data:/anK,iv:LtvlyuLD0ob4jTqP9uujmeb9W7zyR7I6faJDYuu7PX4=,tag:iO0O7OAW7JEvqVyAddy20A==,type:str] +API_KEY=ENC[AES256_GCM,data:Exq7,iv:bcex5MFI63QGzq8LtT5EK/aFbOYB0NQfDnMCStsQBvY=,tag:OfBC6qO96vSPA5j337ucUA==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFZ0RHeHhzZklVTXV0Tkkz\nNHVTL3lKWkVZdGJuNzE0a05DOU9lRVR5NEFVCml4dGFMWVlFOTVzWHBvZ1lxOFQr\nMEdsVmNKRVE1dXJNa2tqMktEUDhNQWcKLS0tIENoTkN0VUZtVnh0NnUvMnc4bWV6\nVW1aRFdNaTdJS0drT0llcXZTY3N0SUUKEGUKXBiRB5Ns/wsh5QszpyIVHuZJ+ber\n5czeZDWDDl9Vx5WYOt0M/4TC6rJQ6hkfj+Ps+eKycitnVTOnOnpASA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnME9uSXF3TU90VFFEb0Vr\nTHlFT1JCZGxUMVBLNDJ0bkdMTmU0YktNY0NRCjIvakQrRjc4OVB3WDdwWEt0Sjd0\nWUxvT001SnVsZGhrWjV1MzJjOGNnS3MKLS0tIC8xK01yYnRiMzIyTDd1dVpGaUNh\nU1lmZnREUktRRkpQOHlpcVBOQlVlVVEKQW699/iBFbah1OOVbcClLoyWadsDGYaf\nMz+5EvDFwO7LPxld3uTkXNh0tNuXmi6T85c09FM7BPqarNMsBbqVJA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvUms2VWdqQTZJaU1TS0Zp\najd1YlpIUEhIRkVjR201R25heTJkc0E3WVdNCnVsUUJCNjA2OTdXaEpXSEs0UnBB\nWGNqdGJuQ3RyS0J4cEVvRTlHbUJGdjgKLS0tIHYvU0NFZWZtd1lxRGlxdnVlK3o4\naXJ6TTBnUUVneGltdnpwQWtKdk94cG8Kln39W0fSwaiv3hG8df3oQ1Lc+W1N7BN+\nkbHBe4nZm7d0/vzMogNwQYfHzgRZbsLqaizqsyysgUDYwGAbVYrhJw==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj -sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByY3F3ZFU5KzBVYklIbmR1\nQjloZFFnZEMvekxrTUpOb3FMUnpFekZXODNZCmhRNjZiNGZjVHVlTTFPSDJnNGFG\nS3NyWFF0UFBxL0ZGYUhxeGpGVldBVEEKLS0tIGVwclczRERPTENWMGl2VmtTaTBQ\nTWVIanRTMURaWmFJSWZwTkZSRUlRbEkKqMEEWUGjLYbDwd3q8isW/mg5c+MNPcod\naSh7XqVcOyDBUBr1yuQ1DY2itMY+x502HNzUl84i67mep0k619jgTA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvT2ZobHYxMFhsSE95clVP\nSzNYQklqNEhFeEE0MWNpa1dyZXBCN1B6RkdrClVWS1VkbXBuRkdXWS9kUUFIWDRs\nZTlFNkhXaWc5MHJNckxzVEx3ejkvblEKLS0tIHhqaytBbVRPdjZkWVdVZWtvbEho\nWGpna1BSck1CeFFrOWYyNC9JNzFiYk0KgQpy/ZypRK4wbttPicRsxbAs/woK6Kq4\ntzAsBHjn/0WL0aqgFe9sJkiGp+o6z9FKeZ6+3VkgJSJlozQGYwgIog==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1slx6e48k7fre0ddyu7dtm2wwcqaywn5ke2mkngym3rpazxwvvuyq9qjknk -sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtU05jcmxMeUJWZnpISkd6\nTFZFNkxPMjBXOVNSSmxwWUNZczdtZXYxWTFVCitCclkyV2hsUnJzcGhvbXluc3N2\nOGVsUldjaEpxMVFMRVZzNm9mV0N6SjQKLS0tIHBEOFhlbDNFTThOZW1HZU0veldX\nMmZiNkZBZzA4QVlyRTJRTzBSVXJPSUkKEGiTA2DpTTOUI/dxrQSYoQhiS6DM4Q/t\nOMAA3OU05t2n0SeWUoJ5yFJ2PcFWTkXnUG173e/ip5zpdL/el7dDRw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIS1c3bEp0R1BLMlB1L204\nM09HeURSbVVWNmpaNVgvcDBxWDhyRGpsc0YwCll1UmRvdHpYSGVDSHBpSUpEN3VU\nbUlQR0w0YkZxNCt0Mk4zaG5ibjRwenMKLS0tIE96cXQrZGhCMzEyOVBLZjMwT2xW\nd2RKMWJqWURPbmwwUnpYY0x6RDVNZ0UKSb18GsEhmsHM5OTPnd41C4MIBWO9X7BC\nOWMxa/OBFcnmGxD9+/ULklCaIIAklLfYhuZ6VjmjJtPHLcUFrZ+sBw==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h -sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1NzZpYk9RMithS0xieWZ1\nVGo0VURsOWQ3VTJjK1V3amtVR3dCQ2twZjF3Cm5IbkliWmp5dXpFSUl1YURJelZD\nb0ZDaExNc3NNQUFpK2p0UUZCdHhMNjgKLS0tIHRRck13c2crMXF1TnFha2NpMHBD\nVlovMGxyMmx4TWVYdUF3NHhQRTVNa3cKE/patQW9NnZ6LnbGT21gs6F9ggCCbveI\nff0kfaLVFaG073PGSRlS/6pI6kY10E8VWWUTjsA1tBZM3vLsZ0fq7A==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMYzdVekdobTdkUmQ5R090\nZE5mUVliRTFZOEJUZFg0MU1YTUkxNXpPbkRRCkNUeXM4QWtudG5XK0VEMDBxZzAz\nUlU2aXp4UUF0Y3J0QTJYUVplclg0ZVkKLS0tIHJsMXBGTFZmdUpFakFLOVZxblA0\nSFkzL2hERmkxTStYZjRsY1oxcjBuc1UKrnliWi/B3ilvTBdZC7kBUSUxbwmeZfin\nGSwhZT/5dEaE7yekUu/66GqfClsV2NZ7t8g16PnNAavUzBYQcfL2oA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx -sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYajZNZkxacjY5L2Q4KzBu\nS25iZ082a1FSSVpwODR5S2I1U09CTE5wWEVFCjAxOEZ4STAzcDZILzBxeWVYK090\nNTBQUlpSMmlhSDhRNkUxaWFLb1p3SlkKLS0tIE1MYXVuaEhDd1ROdGdlbUZKckZv\nRWFLRWNGUWZ2TVg1Qi9hZCttSlc4TlUKR5vNkVuEqf4sBxCPFdse4XInlB8xqA0z\n3szMwE8r8fIzKg8j8chh3yb3H2aHel2WJanF7+omB8H19Fvq1+3NcQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHbU15MytNY05OcjJZanhl\nZk9kNjNOeE1Za2JyNzJCclIyMWNHRGVkczBrClkxVERHVit6Smhvdnh0RHFQQ2JE\nNWF1YmZzSGljbUtBbExkV1JSWGdoMTQKLS0tIDZORGlLZzF0Yk1MQllyUmRvQ2Rk\nT0E2K3UvdSsvYVFuQysrdGFMU3lLZGcKkG1ghacfvcuFiYuR9Ir1YsdJE/Ufq/OW\nHlYgXa20CP5VDRN3n2qqUpqCOOm7e7HGB3w4oCYRFQezDxgi+kpl6Q==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_5__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -sops_age__list_6__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSWTRCcGowV1c2blhrZjRP\nczBBaldMemM0SGI4dzRiVWdMbEkxM0VXRlhzCkNpaVFqSnZJbU5RWkIweGo0WU9L\neW5WTzN1T1djR1Z5WlVDdmZteVJ0SmcKLS0tIHZ4WUxhdzJXQzFYTG5ET1dCUi9m\nOW5IbWpESUd6K1ZZNHh1M20rNWxTTEUKTjqTQD+SCcfLdj8J9PFMgNfUkqScy3DN\ncSYOwbpH2ObVhvvmgLqjR+RQdLkpb2itf4cufKspSi5d6uA2AQfwtw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_6__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2cnNuTGRmMDZVWDR6TTNu\nZm1wbmlvZng3OEVQcSsrVEMwOHJnU3dOc2lZCmQ0dVZSc09LdGVNOXNxdS9Uazkv\nYTNORkt4YkJMNVlrQVpzQ1lrYXZnclEKLS0tIE9oMjZ5NCtxL3JVbHhLSi9XdWZG\nMzJudm01SU5jVlZtMHpKb0srY25hYTgKqmAMHejcPI8uJQ8JzeCKfiioSJJ1cwFq\nDZC0scbcazUJDiJw3ZFluYTAr7Q9eqFvcQmEHQSYFiLg8NcMIo38og==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_6__map_recipient=age15fsv503cl9ajfg9fphw45sfq0ytlptnr8wtlt48asytz4ev6rfks2pwt5s -sops_lastmodified=2026-04-06T10:33:40Z -sops_mac=ENC[AES256_GCM,data:oZ7HHxAKttSyIEhkQ1pWPUwKD95EXNYUXKaO90T1ye4AWRpFPcBIfOkCk0eyOJyD1iPpj7MgzTcNcyCC14Q4G2ZnNQE/Gy4ma4UQSCcGqG85AqYWJ81Blj4thzWxZLpE1ZDqGNJilI0zQeMu2fH39rrFrBqt5b81X6TKYMCjvEc=,iv:sBkqMnLIEHmg8Bxv/wO109qKItTO+zFfGvHAhwwLoqI=,tag:5t82yBl7V6ITFVoh3imWoA==,type:str] +sops_age__list_7__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxVjNqMmE0SGJ2UlZEN2Fl\nRWVCZlhmV1JSZTN1T0dFT016bEJBNzVTOEZzCkdsc3E1SG9VWEJCQUNLZzVsVkJi\nU25mRlZkcTN2OU1uQzVLN3M3d1FqT1EKLS0tIDVCeHJ3QW41RW1NdUdUc0J2ZmR0\nNzZVdG9tWURGRmZ6RUc0UDFJWXV2SUkK2kQ+pTHulS1oFtYhTOjxtYoTNgLxzdEF\nfhKRnVOiTxzxelKp+qi28k0ELL4FjW1SzaqlO5GXfcpYVfFnP+rymg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_7__map_recipient=age14dssfufn8n0n7lcvyxxp4rq7zuf5pv0ek4zl9h7atsy85ceynfvqa0qz59 +sops_lastmodified=2026-04-13T14:32:24Z +sops_mac=ENC[AES256_GCM,data:eY7D1NUVIqFAUs5zN+FiR4CpWN0+pC/VHH4dDF9GoVo2rzSujLWAdkp1lgUMx8tjg7fk/u8vUnlkUUgMrQ153RZvp62PyM2qFTBLCpuvDhzgt55wGkaDEnYSq2gbxMorK+J0r2VKHWfI1FvqmrvZicptjce+0ZTkwAINgvM5sPI=,iv:aFTeReA4AYm0Oq+vOvFHrfz1zvMsDbwqu9+Gw3iIxNg=,tag:73Ks+cbvGkaySZuRMa9teg==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.12.1 diff --git a/infra/.sops.yaml b/infra/.sops.yaml index 40eddb2aa2..155163bdb1 100644 --- a/infra/.sops.yaml +++ b/infra/.sops.yaml @@ -15,3 +15,4 @@ creation_rules: - age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx - age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy - age15fsv503cl9ajfg9fphw45sfq0ytlptnr8wtlt48asytz4ev6rfks2pwt5s + - age14dssfufn8n0n7lcvyxxp4rq7zuf5pv0ek4zl9h7atsy85ceynfvqa0qz59 From dcc65ebb34bb1e7fcd95f474ef1c4889f5808bae Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Mon, 13 Apr 2026 16:58:53 +0200 Subject: [PATCH 64/78] fix: remove minio from prod --- infra/stack.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/infra/stack.yml b/infra/stack.yml index faf65a671a..8b4634c7a1 100644 --- a/infra/stack.yml +++ b/infra/stack.yml @@ -28,7 +28,6 @@ services: HOSTNAME: '0.0.0.0' NODE_ENV: production PORT: '3000' - PUBPUB_URL: ${PUBPUB_URL} SITE_BUILDER_ENDPOINT: http://site-builder:4000 networks: [appnet] healthcheck: @@ -120,14 +119,6 @@ services: restart_policy: condition: any - inbucket: - image: inbucket/inbucket:latest - networks: [appnet] - deploy: - replicas: 1 - restart_policy: - condition: any - networks: appnet: driver: overlay @@ -137,6 +128,5 @@ networks: volumes: pgdata: - minio_data: caddy_data: caddy_config: From 1688d346cf4b05e953a86dd26475044cf66ae6ce Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 14 Apr 2026 13:20:18 +0200 Subject: [PATCH 65/78] fix: remove mention of custom caddy --- Dockerfile.caddy | 9 - infra/Caddyfile | 33 +- pnpm-lock.yaml | 1808 +--------------------------------- self-host/caddy/Caddyfile | 32 +- self-host/docker-compose.yml | 14 +- 5 files changed, 48 insertions(+), 1848 deletions(-) delete mode 100644 Dockerfile.caddy diff --git a/Dockerfile.caddy b/Dockerfile.caddy deleted file mode 100644 index 388120b5ce..0000000000 --- a/Dockerfile.caddy +++ /dev/null @@ -1,9 +0,0 @@ -FROM caddy:builder-alpine AS builder - -RUN xcaddy build --with github.com/sagikazarmark/caddy-fs-s3 - -FROM caddy:alpine - -COPY --from=builder /usr/bin/caddy /usr/bin/caddy - -CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"] \ No newline at end of file diff --git a/infra/Caddyfile b/infra/Caddyfile index 47ffda4e3b..79ee27011f 100644 --- a/infra/Caddyfile +++ b/infra/Caddyfile @@ -6,27 +6,28 @@ {$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 {$S3_BUCKET_NAME:assets} - region {$S3_REGION:us-east-1} - endpoint {$S3_ENDPOINT:http://minio:9000} - access_key {$S3_ACCESS_KEY} - secret_key {$S3_SECRET_KEY} + @pathWithSlash path_regexp dir (.+)/$ + handle @pathWithSlash { + redir {re.dir.1} permanent + } + + rewrite * /assets{uri} + + reverse_proxy host.docker.internal:{minio_port} { + @error status 403 404 + handle_response @error { + rewrite * {uri}/index.html + reverse_proxy host.docker.internal:{minio_port} { + @nestedError status 404 + handle_response @nestedError { + respond "Not found" 404 + } + } } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9dbac2a503..deba9ad6e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1590,112 +1590,6 @@ importers: specifier: 'catalog:' version: 5.9.2 - site-builder: - dependencies: - '@astrojs/react': - specifier: ^4.2.4 - version: 4.3.0(@types/node@22.17.2)(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) - '@aws-sdk/client-s3': - specifier: ^3.525.0 - version: 3.864.0 - '@aws-sdk/lib-storage': - specifier: ^3.787.0 - version: 3.864.0(@aws-sdk/client-s3@3.864.0) - '@hono/node-server': - specifier: ^1.14.1 - version: 1.19.1(hono@4.9.7) - '@hono/zod-validator': - specifier: ^0.4.3 - version: 0.4.3(hono@4.9.7)(zod@3.25.76) - '@pubpub/tailwind': - specifier: workspace:* - version: link:../config/tailwind - '@t3-oss/env-core': - specifier: ^0.12.0 - version: 0.12.0(typescript@5.9.2)(zod@3.25.76) - '@tailwindcss/typography': - specifier: ^0.5.16 - version: 0.5.16(tailwindcss@4.1.17) - '@tailwindcss/vite': - specifier: ^4.1.17 - version: 4.1.17(vite@6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) - '@ts-rest/core': - specifier: 'catalog:' - version: 3.51.0(@types/node@22.17.2)(zod@3.25.76) - '@ts-rest/serverless': - specifier: ^3.52.1 - version: 3.52.1(@ts-rest/core@3.51.0(@types/node@22.17.2)(zod@3.25.76))(@types/aws-lambda@8.10.145)(zod@3.25.76) - archiver: - specifier: ^6.0.2 - version: 6.0.2 - astro: - specifier: ^5.7.3 - version: 5.13.5(@types/node@22.17.2)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.46.4)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) - astro-pdf: - specifier: ^1.6.0 - version: 1.7.2(astro@5.13.5(@types/node@22.17.2)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.46.4)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1))(typescript@5.9.2) - contracts: - specifier: 'workspace:' - version: link:../packages/contracts - dotenv: - specifier: ^16.4.5 - version: 16.6.1 - hono: - specifier: ^4.9.7 - version: 4.9.7 - mime-types: - specifier: ^2.1.35 - version: 2.1.35 - react: - specifier: catalog:react19 - version: 19.2.3 - react-dom: - specifier: catalog:react19 - version: 19.2.3(react@19.2.3) - tailwindcss: - specifier: 'catalog:' - version: 4.1.17 - tailwindcss-animate: - specifier: ^1.0.6 - version: 1.0.7(tailwindcss@4.1.17) - tsconfig: - specifier: workspace:* - version: link:../config/tsconfig - tsx: - specifier: ^4.19.3 - version: 4.20.5 - ui: - specifier: 'workspace:' - version: link:../packages/ui - utils: - specifier: 'workspace:' - version: link:../packages/utils - zod: - specifier: 'catalog:' - version: 3.25.76 - devDependencies: - '@astrojs/check': - specifier: ^0.9.4 - version: 0.9.4(prettier-plugin-astro@0.14.1)(prettier@3.6.2)(typescript@5.9.2) - '@types/archiver': - specifier: ^6.0.2 - version: 6.0.3 - '@types/mime-types': - specifier: ^2.1.4 - version: 2.1.4 - '@types/node': - specifier: '22' - version: 22.17.2 - '@types/react': - specifier: catalog:react19 - version: 19.1.10 - '@types/react-dom': - specifier: catalog:react19 - version: 19.1.7(@types/react@19.1.10) - dotenv-cli: - specifier: ^5.0.1 - version: 5.1.0 - site-builder-2: dependencies: '@aws-sdk/client-s3': @@ -1801,53 +1695,6 @@ packages: '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} - '@astrojs/check@0.9.4': - resolution: {integrity: sha512-IOheHwCtpUfvogHHsvu0AbeRZEnjJg3MopdLddkJE70mULItS/Vh37BHcI00mcOJcH1vhD3odbpvWokpxam7xA==} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - - '@astrojs/compiler@2.12.2': - resolution: {integrity: sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw==} - - '@astrojs/internal-helpers@0.7.2': - resolution: {integrity: sha512-KCkCqR3Goym79soqEtbtLzJfqhTWMyVaizUi35FLzgGSzBotSw8DB1qwsu7U96ihOJgYhDk2nVPz+3LnXPeX6g==} - - '@astrojs/language-server@2.15.4': - resolution: {integrity: sha512-JivzASqTPR2bao9BWsSc/woPHH7OGSGc9aMxXL4U6egVTqBycB3ZHdBJPuOCVtcGLrzdWTosAqVPz1BVoxE0+A==} - hasBin: true - peerDependencies: - prettier: ^3.0.0 - prettier-plugin-astro: '>=0.11.0' - peerDependenciesMeta: - prettier: - optional: true - prettier-plugin-astro: - optional: true - - '@astrojs/markdown-remark@6.3.6': - resolution: {integrity: sha512-bwylYktCTsLMVoCOEHbn2GSUA3c5KT/qilekBKA3CBng0bo1TYjNZPr761vxumRk9kJGqTOtU+fgCAp5Vwokug==} - - '@astrojs/prism@3.3.0': - resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==} - engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} - - '@astrojs/react@4.3.0': - resolution: {integrity: sha512-N02aj52Iezn69qHyx5+XvPqgsPMEnel9mI5JMbGiRMTzzLMuNaxRVoQTaq2024Dpr7BLsxCjqMkNvelqMDhaHA==} - engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} - peerDependencies: - '@types/react': ^17.0.50 || ^18.0.21 || ^19.0.0 - '@types/react-dom': ^17.0.17 || ^18.0.6 || ^19.0.0 - react: ^17.0.2 || ^18.0.0 || ^19.0.0 - react-dom: ^17.0.2 || ^18.0.0 || ^19.0.0 - - '@astrojs/telemetry@3.3.0': - resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==} - engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} - - '@astrojs/yaml2ts@0.2.2': - resolution: {integrity: sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ==} - '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -2739,9 +2586,6 @@ packages: '@bundled-es-modules/tough-cookie@0.1.6': resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} - '@capsizecss/unpack@2.4.0': - resolution: {integrity: sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q==} - '@chevrotain/cst-dts-gen@11.0.3': resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} @@ -2910,27 +2754,6 @@ packages: '@electric-sql/pglite@0.3.7': resolution: {integrity: sha512-5c3mybVrhxu5s47zFZtIGdG8YHkKCBENOmqxnNBjY53ZoDhADY/c5UqBDl159b7qtkzNPtbbb893wL9zi1kAuw==} - '@emmetio/abbreviation@2.3.3': - resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} - - '@emmetio/css-abbreviation@2.1.8': - resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==} - - '@emmetio/css-parser@0.4.0': - resolution: {integrity: sha512-z7wkxRSZgrQHXVzObGkXG+Vmj3uRlpM11oCZ9pbaz0nFejvCDmAiNDpY75+wgXOcffKpj4rzGtwGaZxfJKsJxw==} - - '@emmetio/html-matcher@1.3.0': - resolution: {integrity: sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==} - - '@emmetio/scanner@1.0.4': - resolution: {integrity: sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==} - - '@emmetio/stream-reader-utils@0.1.0': - resolution: {integrity: sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==} - - '@emmetio/stream-reader@2.2.0': - resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} - '@emnapi/core@0.45.0': resolution: {integrity: sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==} @@ -3399,65 +3222,33 @@ packages: peerDependencies: react: ^16.13 || ^17 || ^18 || ^19 - '@img/sharp-darwin-arm64@0.33.5': - resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - '@img/sharp-darwin-arm64@0.34.3': resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.33.5': - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - '@img/sharp-darwin-x64@0.34.3': resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.0.4': - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} - cpu: [arm64] - os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.0': resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.0.4': - resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} - cpu: [x64] - os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.0': resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.0.4': - resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} - cpu: [arm64] - os: [linux] - '@img/sharp-libvips-linux-arm64@1.2.0': resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.0.5': - resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} - cpu: [arm] - os: [linux] - '@img/sharp-libvips-linux-arm@1.2.0': resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} cpu: [arm] @@ -3468,64 +3259,32 @@ packages: cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.0.4': - resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} - cpu: [s390x] - os: [linux] - '@img/sharp-libvips-linux-s390x@1.2.0': resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.0.4': - resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} - cpu: [x64] - os: [linux] - '@img/sharp-libvips-linux-x64@1.2.0': resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': - resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} - cpu: [arm64] - os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.0.4': - resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} - cpu: [x64] - os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.2.0': resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.33.5': - resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - '@img/sharp-linux-arm64@0.34.3': resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.33.5': - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - '@img/sharp-linux-arm@0.34.3': resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3538,59 +3297,30 @@ packages: cpu: [ppc64] os: [linux] - '@img/sharp-linux-s390x@0.33.5': - resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - '@img/sharp-linux-s390x@0.34.3': resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.33.5': - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - '@img/sharp-linux-x64@0.34.3': resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.33.5': - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.3': resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.33.5': - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - '@img/sharp-linuxmusl-x64@0.34.3': resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.33.5': - resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - '@img/sharp-wasm32@0.34.3': resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3602,24 +3332,12 @@ packages: cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.33.5': - resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - '@img/sharp-win32-ia32@0.34.3': resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.33.5': - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - '@img/sharp-win32-x64@0.34.3': resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -5263,16 +4981,6 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - '@puppeteer/browsers@2.10.8': - resolution: {integrity: sha512-f02QYEnBDE0p8cteNoPYHHjbDuwyfbe4cCIVlNi8/MRicIxFW4w4CfgU0LNgWEID6s06P+hRJ1qjpBLMhPRCiQ==} - engines: {node: '>=18'} - hasBin: true - - '@puppeteer/browsers@2.6.1': - resolution: {integrity: sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==} - engines: {node: '>=18'} - hasBin: true - '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -7024,11 +6732,6 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' - '@tailwindcss/vite@4.1.17': - resolution: {integrity: sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==} - peerDependencies: - vite: ^5.2.0 || ^6 || ^7 - '@tanstack/query-core@5.85.5': resolution: {integrity: sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==} @@ -7377,9 +7080,6 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/fontkit@2.0.8': - resolution: {integrity: sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew==} - '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -7570,9 +7270,6 @@ packages: '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - '@types/yauzl@2.10.3': - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260119.1': resolution: {integrity: sha512-siuRD9Shh5gVrgYG5HEWxFxG/dkZa4ndupGWKMfM4DwMG7zLeFayi6sB9yiwpD0d203ts01D7uTnTCALdiWXmQ==} cpu: [arm64] @@ -7906,32 +7603,6 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@volar/kit@2.4.23': - resolution: {integrity: sha512-YuUIzo9zwC2IkN7FStIcVl1YS9w5vkSFEZfPvnu0IbIMaR9WHhc9ZxvlT+91vrcSoRY469H2jwbrGqpG7m1KaQ==} - peerDependencies: - typescript: '*' - - '@volar/language-core@2.4.23': - resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} - - '@volar/language-server@2.4.23': - resolution: {integrity: sha512-k0iO+tybMGMMyrNdWOxgFkP0XJTdbH0w+WZlM54RzJU3WZSjHEupwL30klpM7ep4FO6qyQa03h+VcGHD4Q8gEg==} - - '@volar/language-service@2.4.23': - resolution: {integrity: sha512-h5mU9DZ/6u3LCB9xomJtorNG6awBNnk9VuCioGsp6UtFiM8amvS5FcsaC3dabdL9zO0z+Gq9vIEMb/5u9K6jGQ==} - - '@volar/source-map@2.4.23': - resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} - - '@volar/typescript@2.4.23': - resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} - - '@vscode/emmet-helper@2.11.0': - resolution: {integrity: sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==} - - '@vscode/l10n@0.0.18': - resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} - '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -8180,17 +7851,6 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true - astro-pdf@1.7.2: - resolution: {integrity: sha512-uhoqXm58jcHwCRbmkEgtXnISygEjJHOrp77lFFkgW68R9RPt0SlxFZxB9mA2gpJ3RsitrEL3/iinJuB7ZJ9m1A==} - engines: {node: '>=18.0.0'} - peerDependencies: - astro: ^4.4.4 || ^5.0.0 - - astro@5.13.5: - resolution: {integrity: sha512-XmBzkl13XU97+n/QiOM5uXQdAVe0yKt5gO+Wlgc8dHRwHR499qhMQ5sMFckLJweUINLzcNGjP3F5nG4wV8a2XA==} - engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} - hasBin: true - async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} @@ -8223,10 +7883,6 @@ packages: axios@1.11.0: resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - b4a@1.6.7: resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} @@ -8258,36 +7914,6 @@ packages: bare-events@2.6.1: resolution: {integrity: sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==} - bare-fs@4.2.2: - resolution: {integrity: sha512-5vn+bdnlCYMwETIm1FqQXDP6TYPbxr2uJd88ve40kr4oPbiTZJVrTNzqA3/4sfWZeWKuQR/RkboBt7qEEDtfMA==} - engines: {bare: '>=1.16.0'} - peerDependencies: - bare-buffer: '*' - peerDependenciesMeta: - bare-buffer: - optional: true - - bare-os@3.6.2: - resolution: {integrity: sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==} - engines: {bare: '>=1.14.0'} - - bare-path@3.0.0: - resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} - - bare-stream@2.7.0: - resolution: {integrity: sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==} - peerDependencies: - bare-buffer: '*' - bare-events: '*' - peerDependenciesMeta: - bare-buffer: - optional: true - bare-events: - optional: true - - base-64@1.0.0: - resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} - base16@1.0.0: resolution: {integrity: sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==} @@ -8321,9 +7947,6 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - blob-to-buffer@1.2.9: - resolution: {integrity: sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA==} - bowser@2.12.0: resolution: {integrity: sha512-HcOcTudTeEWgbHh0Y1Tyb6fdeR71m4b/QACf0D4KswGTsNeIJQmg38mRENZPAYPZvGFN3fk3604XbQEPdxXdKg==} @@ -8331,10 +7954,6 @@ packages: resolution: {integrity: sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==} engines: {node: '>=14.16'} - boxen@8.0.1: - resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} - engines: {node: '>=18'} - brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -8345,9 +7964,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - brotli@1.3.3: - resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} - browserslist@4.25.3: resolution: {integrity: sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -8406,10 +8022,6 @@ packages: resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} engines: {node: '>=14.16'} - camelcase@8.0.0: - resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} - engines: {node: '>=16'} - caniuse-lite@1.0.30001735: resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==} @@ -8519,11 +8131,6 @@ packages: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} - chromium-bidi@0.11.0: - resolution: {integrity: sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==} - peerDependencies: - devtools-protocol: '*' - ci-info@3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} @@ -8532,10 +8139,6 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} - ci-info@4.3.0: - resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} - engines: {node: '>=8'} - cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} @@ -8607,10 +8210,6 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - clone@2.1.2: - resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} - engines: {node: '>=0.8'} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -8694,9 +8293,6 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - common-ancestor-path@1.0.1: - resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} - commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -8750,17 +8346,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-es@1.2.2: - resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} - cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - cookie@1.0.2: - resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} - engines: {node: '>=18'} - core-js-compat@3.45.0: resolution: {integrity: sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==} @@ -8793,15 +8382,6 @@ packages: typescript: optional: true - cosmiconfig@9.0.0: - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -8824,9 +8404,6 @@ packages: cropperjs@1.6.2: resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==} - cross-fetch@3.2.0: - resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} - cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -8835,17 +8412,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - crossws@0.3.5: - resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} - crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} - css-tree@3.1.0: - resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -9137,9 +8707,6 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - degenerator@5.0.1: resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} engines: {node: '>= 14'} @@ -9167,9 +8734,6 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - destr@2.0.5: - resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} - detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -9181,22 +8745,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - deterministic-object-hash@2.0.2: - resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} - engines: {node: '>=18'} - - devalue@5.3.2: - resolution: {integrity: sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==} - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - devtools-protocol@0.0.1367902: - resolution: {integrity: sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==} - - dfa@1.2.0: - resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} - diacritics@1.3.0: resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==} @@ -9207,17 +8758,10 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} - diff@5.2.0: - resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} - engines: {node: '>=0.3.1'} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -9271,10 +8815,6 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - dset@3.1.4: - resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} - engines: {node: '>=4'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -9288,12 +8828,6 @@ packages: electron-to-chromium@1.5.207: resolution: {integrity: sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw==} - emmet@2.4.11: - resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} - - emoji-regex@10.5.0: - resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -9514,11 +9048,6 @@ packages: engines: {node: '>=16.0.0'} hasBin: true - extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - fast-copy@3.0.2: resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} @@ -9567,9 +9096,6 @@ packages: fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -9610,10 +9136,6 @@ packages: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} - flattie@1.1.1: - resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} - engines: {node: '>=8'} - follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -9623,12 +9145,6 @@ packages: debug: optional: true - fontace@0.3.0: - resolution: {integrity: sha512-czoqATrcnxgWb/nAkfyIrRp6Q8biYj7nGnL6zfhTcX+JKKpWHFBnb8uNMw/kZr7u++3Y3wYSYoZgHkCcsuBpBg==} - - fontkit@2.0.4: - resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -9722,10 +9238,6 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.3.1: - resolution: {integrity: sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==} - engines: {node: '>=18'} - get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -9742,10 +9254,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -9874,9 +9382,6 @@ packages: resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} engines: {node: '>=14.0.0'} - h3@1.15.4: - resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} - hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} @@ -10021,9 +9526,6 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - html-escaper@3.0.3: - resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} - html-to-text@9.0.5: resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} engines: {node: '>=14'} @@ -10044,9 +9546,6 @@ packages: htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - http-cache-semantics@4.2.0: - resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - http-proxy-agent@7.0.0: resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} engines: {node: '>= 14'} @@ -10118,9 +9617,6 @@ packages: import-in-the-middle@1.14.2: resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==} - import-meta-resolve@4.2.0: - resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} - indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -10181,9 +9677,6 @@ packages: resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} engines: {node: '>= 12'} - iron-webcrypto@1.2.1: - resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} - is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -10582,12 +10075,6 @@ packages: resolution: {integrity: sha512-OCzaRMK8HobtX8fp37uIVmL8CY1IGc/a6gLsDqz3quExFR09/U78HUzWYr7T31UEB6+Eu0/8dkVD5fFDOl9a8w==} engines: {node: '>= 8'} - jsonc-parser@2.3.1: - resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} - - jsonc-parser@3.3.1: - resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - jsondiffpatch@0.4.1: resolution: {integrity: sha512-t0etAxTUk1w5MYdNOkZBZ8rvYYN5iL+2dHCCx/DpkFm/bW28M6y5nUS83D4XdZiHy35Fpaw6LBb+F88fHZnVCw==} engines: {node: '>=8.17.0'} @@ -11088,9 +10575,6 @@ packages: peerDependencies: react: ^18.0 || ^19.0 - mdast-util-definitions@6.0.0: - resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} - mdast-util-directive@3.1.0: resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} @@ -11148,9 +10632,6 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - mdn-data@2.12.2: - resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} - mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -11422,9 +10903,6 @@ packages: mudder@2.1.1: resolution: {integrity: sha512-0/F//kjoRlefsazFcGxa7FAuwRNDoX3ALal7W9uOZgE9QKxKatFM1NKu3tkmxMAFvUXoIHN2b/PlIt5B+hJirQ==} - muggle-string@0.4.1: - resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -11456,10 +10934,6 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - neotraverse@0.6.18: - resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} - engines: {node: '>= 10'} - netmask@2.0.2: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} @@ -11521,9 +10995,6 @@ packages: no-case@2.3.2: resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} - node-fetch-native@1.6.7: - resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - node-fetch@2.6.11: resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==} engines: {node: 4.x || >=6.0.0} @@ -11551,9 +11022,6 @@ packages: encoding: optional: true - node-mock-http@1.0.2: - resolution: {integrity: sha512-zWaamgDUdo9SSLw47we78+zYw/bDr5gH8pH7oRRs8V3KmBtu8GLgGIbV2p/gRPd3LWpEOpjQj7X1FOU3VFMJ8g==} - node-plop@0.26.3: resolution: {integrity: sha512-Cov028YhBZ5aB7MdMWJEmwyBig43aGL5WT4vdoB28Oitau1zZAcHUn8Sgfk9HM33TqhtLJ9PlM/O0Mv+QpV/4Q==} engines: {node: '>=8.9.4'} @@ -11657,12 +11125,6 @@ packages: resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} engines: {node: '>=0.10.0'} - ofetch@1.4.1: - resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} - - ohash@2.0.11: - resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} - on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -11748,10 +11210,6 @@ packages: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-limit@6.2.0: - resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} - engines: {node: '>=18'} - p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -11818,9 +11276,6 @@ packages: resolution: {integrity: sha512-8KPLGT5g9s+olKMRTU9LFekLizkVIu9tes90O1/aigJ0T5LmyPqTzGJrETnSw3meSYg58YH7JTzhTTW/3z6VAw==} hasBin: true - pako@0.2.9: - resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} - param-case@2.1.1: resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} @@ -11920,9 +11375,6 @@ packages: peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} - pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - pg-cloudflare@1.2.7: resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} @@ -12089,15 +11541,6 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-astro@0.14.1: - resolution: {integrity: sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==} - engines: {node: ^14.15.0 || >=16.0.0} - - prettier@2.8.7: - resolution: {integrity: sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==} - engines: {node: '>=10.13.0'} - hasBin: true - prettier@3.6.2: resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} engines: {node: '>=14'} @@ -12245,16 +11688,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - puppeteer-core@23.11.1: - resolution: {integrity: sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==} - engines: {node: '>=18'} - - puppeteer@23.11.1: - resolution: {integrity: sha512-53uIX3KR5en8l7Vd8n5DUv90Ae9QDQsyIthaUFVzwV6yU750RjqRznEtNMBT20VthqAdemnJN+hxVdmMHKt7Zw==} - engines: {node: '>=18'} - deprecated: < 24.15.0 is no longer supported - hasBin: true - qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} @@ -12275,9 +11708,6 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - radix3@1.1.2: - resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} - ramda@0.30.1: resolution: {integrity: sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==} @@ -12637,12 +12067,6 @@ packages: resolution: {integrity: sha512-yPpxc4ZR2makceA9hy/jHNqc7QVkd4Je/N0WRHm6bs3PtivPuPynxE5ejU/mp5EhnCv8+uZL7vhz8rkluSlx+Q==} engines: {node: '>=8'} - request-light@0.5.8: - resolution: {integrity: sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==} - - request-light@0.7.0: - resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -12690,9 +12114,6 @@ packages: resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - restructure@3.0.2: - resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} - retext-latin@4.0.0: resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} @@ -12768,9 +12189,6 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - s.color@0.0.15: - resolution: {integrity: sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==} - safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -12796,9 +12214,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sass-formatter@0.7.9: - resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==} - saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -12882,10 +12297,6 @@ packages: shallow-equal@3.1.0: resolution: {integrity: sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==} - sharp@0.33.5: - resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - sharp@0.34.3: resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -12964,10 +12375,6 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - smol-toml@1.4.2: - resolution: {integrity: sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==} - engines: {node: '>= 18'} - snake-case@2.1.0: resolution: {integrity: sha512-FMR5YoPFwOLuh4rRz92dywJjyKYZNLpMn1R5ujVpIYkbA9p01fq8RMg0FkO4M+Yobt4MjHeLTJVm5xFFBHSV2Q==} @@ -13102,10 +12509,6 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} - string.prototype.trim@1.2.10: resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} engines: {node: '>= 0.4'} @@ -13214,9 +12617,6 @@ packages: stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} - suf-log@2.5.3: - resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==} - supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -13271,9 +12671,6 @@ packages: resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} engines: {node: '>=6'} - tar-fs@3.1.0: - resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==} - tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -13351,9 +12748,6 @@ packages: resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} engines: {node: '>=8'} - tiny-inflate@1.0.3: - resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} - tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -13608,9 +13002,6 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typed-query-selector@2.12.0: - resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==} - typedarray.prototype.slice@1.0.5: resolution: {integrity: sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==} engines: {node: '>= 0.4'} @@ -13618,12 +13009,6 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typesafe-path@0.2.2: - resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} - - typescript-auto-import-cache@0.3.6: - resolution: {integrity: sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==} - typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} @@ -13640,19 +13025,10 @@ packages: engines: {node: '>=0.8.0'} hasBin: true - ultrahtml@1.6.0: - resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} - unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - unbzip2-stream@1.4.3: - resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} - - uncrypto@0.1.3: - resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -13671,16 +13047,10 @@ packages: resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} engines: {node: '>=4'} - unicode-properties@1.4.1: - resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} - unicode-property-aliases-ecmascript@2.1.0: resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} - unicode-trie@2.0.0: - resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} - unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} @@ -13688,9 +13058,6 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - unifont@0.5.2: - resolution: {integrity: sha512-LzR4WUqzH9ILFvjLAUU7dK3Lnou/qd5kD+IakBtBK4S15/+x2y9VX+DcWQv6s551R6W+vzwgVS6tFg3XggGBgg==} - unique-string@2.0.0: resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} engines: {node: '>=8'} @@ -13755,68 +13122,6 @@ packages: resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} engines: {node: '>=14.0.0'} - unstorage@1.17.0: - resolution: {integrity: sha512-l9Z7lBiwtNp8ZmcoZ/dmPkFXFdtEdZtTZafCSnEIj3YvtkXeGAtL2rN8MQFy/0cs4eOLpuRJMp9ivdug7TCvww==} - peerDependencies: - '@azure/app-configuration': ^1.8.0 - '@azure/cosmos': ^4.2.0 - '@azure/data-tables': ^13.3.0 - '@azure/identity': ^4.6.0 - '@azure/keyvault-secrets': ^4.9.0 - '@azure/storage-blob': ^12.26.0 - '@capacitor/preferences': ^6.0.3 || ^7.0.0 - '@deno/kv': '>=0.9.0' - '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 - '@planetscale/database': ^1.19.0 - '@upstash/redis': ^1.34.3 - '@vercel/blob': '>=0.27.1' - '@vercel/functions': ^2.2.12 - '@vercel/kv': ^1.0.1 - aws4fetch: ^1.0.20 - db0: '>=0.2.1' - idb-keyval: ^6.2.1 - ioredis: ^5.4.2 - uploadthing: ^7.4.4 - peerDependenciesMeta: - '@azure/app-configuration': - optional: true - '@azure/cosmos': - optional: true - '@azure/data-tables': - optional: true - '@azure/identity': - optional: true - '@azure/keyvault-secrets': - optional: true - '@azure/storage-blob': - optional: true - '@capacitor/preferences': - optional: true - '@deno/kv': - optional: true - '@netlify/blobs': - optional: true - '@planetscale/database': - optional: true - '@upstash/redis': - optional: true - '@vercel/blob': - optional: true - '@vercel/functions': - optional: true - '@vercel/kv': - optional: true - aws4fetch: - optional: true - db0: - optional: true - idb-keyval: - optional: true - ioredis: - optional: true - uploadthing: - optional: true - update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true @@ -13995,14 +13300,6 @@ packages: yaml: optional: true - vitefu@1.1.1: - resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 - peerDependenciesMeta: - vite: - optional: true - vitest@3.2.4: resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -14031,115 +13328,26 @@ packages: jsdom: optional: true - volar-service-css@0.0.62: - resolution: {integrity: sha512-JwNyKsH3F8PuzZYuqPf+2e+4CTU8YoyUHEHVnoXNlrLe7wy9U3biomZ56llN69Ris7TTy/+DEX41yVxQpM4qvg==} - peerDependencies: - '@volar/language-service': ~2.4.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - volar-service-emmet@0.0.62: - resolution: {integrity: sha512-U4dxWDBWz7Pi4plpbXf4J4Z/ss6kBO3TYrACxWNsE29abu75QzVS0paxDDhI6bhqpbDFXlpsDhZ9aXVFpnfGRQ==} - peerDependencies: - '@volar/language-service': ~2.4.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - volar-service-html@0.0.62: - resolution: {integrity: sha512-Zw01aJsZRh4GTGUjveyfEzEqpULQUdQH79KNEiKVYHZyuGtdBRYCHlrus1sueSNMxwwkuF5WnOHfvBzafs8yyQ==} - peerDependencies: - '@volar/language-service': ~2.4.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - volar-service-prettier@0.0.62: - resolution: {integrity: sha512-h2yk1RqRTE+vkYZaI9KYuwpDfOQRrTEMvoHol0yW4GFKc75wWQRrb5n/5abDrzMPrkQbSip8JH2AXbvrRtYh4w==} - peerDependencies: - '@volar/language-service': ~2.4.0 - prettier: ^2.2 || ^3.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - prettier: - optional: true - - volar-service-typescript-twoslash-queries@0.0.62: - resolution: {integrity: sha512-KxFt4zydyJYYI0kFAcWPTh4u0Ha36TASPZkAnNY784GtgajerUqM80nX/W1d0wVhmcOFfAxkVsf/Ed+tiYU7ng==} - peerDependencies: - '@volar/language-service': ~2.4.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - volar-service-typescript@0.0.62: - resolution: {integrity: sha512-p7MPi71q7KOsH0eAbZwPBiKPp9B2+qrdHAd6VY5oTo9BUXatsOAdakTm9Yf0DUj6uWBAaOT01BSeVOPwucMV1g==} - peerDependencies: - '@volar/language-service': ~2.4.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - volar-service-yaml@0.0.62: - resolution: {integrity: sha512-k7gvv7sk3wa+nGll3MaSKyjwQsJjIGCHFjVkl3wjaSP2nouKyn9aokGmqjrl39mi88Oy49giog2GkZH526wjig==} - peerDependencies: - '@volar/language-service': ~2.4.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - vscode-css-languageservice@6.3.7: - resolution: {integrity: sha512-5TmXHKllPzfkPhW4UE9sODV3E0bIOJPOk+EERKllf2SmAczjfTmYeq5txco+N3jpF8KIZ6loj/JptpHBQuVQRA==} - - vscode-html-languageservice@5.5.1: - resolution: {integrity: sha512-/ZdEtsZ3OiFSyL00kmmu7crFV9KwWR+MgpzjsxO60DQH7sIfHZM892C/E4iDd11EKocr+NYuvOA4Y7uc3QzLEA==} - - vscode-json-languageservice@4.1.8: - resolution: {integrity: sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==} - engines: {npm: '>=7.0.0'} - - vscode-jsonrpc@6.0.0: - resolution: {integrity: sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==} - engines: {node: '>=8.0.0 || >=10.0.0'} - vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} - vscode-languageserver-protocol@3.16.0: - resolution: {integrity: sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==} - vscode-languageserver-protocol@3.17.5: resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} vscode-languageserver-textdocument@1.0.12: resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} - vscode-languageserver-types@3.16.0: - resolution: {integrity: sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==} - vscode-languageserver-types@3.17.5: resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} - vscode-languageserver@7.0.0: - resolution: {integrity: sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==} - hasBin: true - vscode-languageserver@9.0.1: resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} hasBin: true - vscode-nls@5.2.0: - resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} - vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} @@ -14211,10 +13419,6 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-pm-runs@1.1.0: - resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} - engines: {node: '>=4'} - which-typed-array@1.1.19: resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} @@ -14236,10 +13440,6 @@ packages: resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} engines: {node: '>=12'} - widest-line@5.0.0: - resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} - engines: {node: '>=18'} - wildcard@1.1.2: resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==} @@ -14262,10 +13462,6 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} - wrap-ansi@9.0.0: - resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} - engines: {node: '>=18'} - wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -14304,9 +13500,6 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - xxhash-wasm@1.1.0: - resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -14321,18 +13514,10 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} - yaml-language-server@1.15.0: - resolution: {integrity: sha512-N47AqBDCMQmh6mBLmI6oqxryHRzi33aPFPsJhYy3VTUGCdLHYjGh4FZzpUjRlphaADBBkDmnkM/++KNIOHi5Rw==} - hasBin: true - yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.2.2: - resolution: {integrity: sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==} - engines: {node: '>= 14'} - yaml@2.3.1: resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} engines: {node: '>= 14'} @@ -14350,9 +13535,6 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - yjs@13.6.19: resolution: {integrity: sha512-GNKw4mEUn5yWU2QPHRx8jppxmCm9KzbBhB4qJLUJFiiYD0g/tDVgXQ7aPkyh01YO28kbs2J/BEbWBagjuWyejw==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -14369,18 +13551,10 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} - yocto-spinner@0.2.3: - resolution: {integrity: sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ==} - engines: {node: '>=18.19'} - yoctocolors-cjs@2.1.2: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} - yoctocolors@2.1.2: - resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} - engines: {node: '>=18'} - zip-stream@4.1.1: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} @@ -14389,26 +13563,12 @@ packages: resolution: {integrity: sha512-LfOdrUvPB8ZoXtvOBz6DlNClfvi//b5d56mSWyJi7XbH/HfhOHfUhOqxhT/rUiR7yiktlunqRo+jY6y/cWC/5g==} engines: {node: '>= 12.0.0'} - zod-to-json-schema@3.24.6: - resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} - peerDependencies: - zod: ^3.24.1 - - zod-to-ts@1.2.0: - resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==} - peerDependencies: - typescript: ^4.9.4 || ^5.0.2 - zod: ^3 - zod-validation-error@5.0.0: resolution: {integrity: sha512-hmk+pkyKq7Q71PiWVSDUc3VfpzpvcRHZ3QPw9yEMVvmtCekaMeOHnbr3WbxfrgEnQTv6haGP4cmv0Ojmihzsxw==} engines: {node: '>=18.0.0'} peerDependencies: zod: ^3.25.0 || ^4.0.0 - zod@3.23.8: - resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} - zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -14485,116 +13645,6 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 - '@astrojs/check@0.9.4(prettier-plugin-astro@0.14.1)(prettier@3.6.2)(typescript@5.9.2)': - dependencies: - '@astrojs/language-server': 2.15.4(prettier-plugin-astro@0.14.1)(prettier@3.6.2)(typescript@5.9.2) - chokidar: 4.0.3 - kleur: 4.1.5 - typescript: 5.9.2 - yargs: 17.7.2 - transitivePeerDependencies: - - prettier - - prettier-plugin-astro - - '@astrojs/compiler@2.12.2': {} - - '@astrojs/internal-helpers@0.7.2': {} - - '@astrojs/language-server@2.15.4(prettier-plugin-astro@0.14.1)(prettier@3.6.2)(typescript@5.9.2)': - dependencies: - '@astrojs/compiler': 2.12.2 - '@astrojs/yaml2ts': 0.2.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@volar/kit': 2.4.23(typescript@5.9.2) - '@volar/language-core': 2.4.23 - '@volar/language-server': 2.4.23 - '@volar/language-service': 2.4.23 - fast-glob: 3.3.3 - muggle-string: 0.4.1 - volar-service-css: 0.0.62(@volar/language-service@2.4.23) - volar-service-emmet: 0.0.62(@volar/language-service@2.4.23) - volar-service-html: 0.0.62(@volar/language-service@2.4.23) - volar-service-prettier: 0.0.62(@volar/language-service@2.4.23)(prettier@3.6.2) - volar-service-typescript: 0.0.62(@volar/language-service@2.4.23) - volar-service-typescript-twoslash-queries: 0.0.62(@volar/language-service@2.4.23) - volar-service-yaml: 0.0.62(@volar/language-service@2.4.23) - vscode-html-languageservice: 5.5.1 - vscode-uri: 3.1.0 - optionalDependencies: - prettier: 3.6.2 - prettier-plugin-astro: 0.14.1 - transitivePeerDependencies: - - typescript - - '@astrojs/markdown-remark@6.3.6': - dependencies: - '@astrojs/internal-helpers': 0.7.2 - '@astrojs/prism': 3.3.0 - github-slugger: 2.0.0 - hast-util-from-html: 2.0.3 - hast-util-to-text: 4.0.2 - import-meta-resolve: 4.2.0 - js-yaml: 4.1.0 - mdast-util-definitions: 6.0.0 - rehype-raw: 7.0.0 - rehype-stringify: 10.0.1 - remark-gfm: 4.0.1 - remark-parse: 11.0.0 - remark-rehype: 11.1.2 - remark-smartypants: 3.0.2 - shiki: 3.11.0 - smol-toml: 1.4.2 - unified: 11.0.5 - unist-util-remove-position: 5.0.0 - unist-util-visit: 5.0.0 - unist-util-visit-parents: 6.0.1 - vfile: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@astrojs/prism@3.3.0': - dependencies: - prismjs: 1.30.0 - - '@astrojs/react@4.3.0(@types/node@22.17.2)(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)': - dependencies: - '@types/react': 19.1.10 - '@types/react-dom': 19.1.7(@types/react@19.1.10) - '@vitejs/plugin-react': 4.7.0(vite@6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - ultrahtml: 1.6.0 - vite: 6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - '@astrojs/telemetry@3.3.0': - dependencies: - ci-info: 4.3.0 - debug: 4.4.1 - dlv: 1.1.3 - dset: 3.1.4 - is-docker: 3.0.0 - is-wsl: 3.1.0 - which-pm-runs: 1.1.0 - transitivePeerDependencies: - - supports-color - - '@astrojs/yaml2ts@0.2.2': - dependencies: - yaml: 2.8.1 - '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -16047,14 +15097,6 @@ snapshots: '@types/tough-cookie': 4.0.5 tough-cookie: 4.1.4 - '@capsizecss/unpack@2.4.0': - dependencies: - blob-to-buffer: 1.2.9 - cross-fetch: 3.2.0 - fontkit: 2.0.4 - transitivePeerDependencies: - - encoding - '@chevrotain/cst-dts-gen@11.0.3': dependencies: '@chevrotain/gast': 11.0.3 @@ -16316,29 +15358,6 @@ snapshots: '@electric-sql/pglite@0.3.7': {} - '@emmetio/abbreviation@2.3.3': - dependencies: - '@emmetio/scanner': 1.0.4 - - '@emmetio/css-abbreviation@2.1.8': - dependencies: - '@emmetio/scanner': 1.0.4 - - '@emmetio/css-parser@0.4.0': - dependencies: - '@emmetio/stream-reader': 2.2.0 - '@emmetio/stream-reader-utils': 0.1.0 - - '@emmetio/html-matcher@1.3.0': - dependencies: - '@emmetio/scanner': 1.0.4 - - '@emmetio/scanner@1.0.4': {} - - '@emmetio/stream-reader-utils@0.1.0': {} - - '@emmetio/stream-reader@2.2.0': {} - '@emnapi/core@0.45.0': dependencies: tslib: 2.8.1 @@ -16742,92 +15761,48 @@ snapshots: dependencies: react: 19.2.3 - '@img/sharp-darwin-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.4 - optional: true - '@img/sharp-darwin-arm64@0.34.3': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.2.0 optional: true - '@img/sharp-darwin-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.4 - optional: true - '@img/sharp-darwin-x64@0.34.3': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.2.0 optional: true - '@img/sharp-libvips-darwin-arm64@1.0.4': - optional: true - '@img/sharp-libvips-darwin-arm64@1.2.0': optional: true - '@img/sharp-libvips-darwin-x64@1.0.4': - optional: true - '@img/sharp-libvips-darwin-x64@1.2.0': optional: true - '@img/sharp-libvips-linux-arm64@1.0.4': - optional: true - '@img/sharp-libvips-linux-arm64@1.2.0': optional: true - '@img/sharp-libvips-linux-arm@1.0.5': - optional: true - - '@img/sharp-libvips-linux-arm@1.2.0': + '@img/sharp-libvips-linux-arm@1.2.0': optional: true '@img/sharp-libvips-linux-ppc64@1.2.0': optional: true - '@img/sharp-libvips-linux-s390x@1.0.4': - optional: true - '@img/sharp-libvips-linux-s390x@1.2.0': optional: true - '@img/sharp-libvips-linux-x64@1.0.4': - optional: true - '@img/sharp-libvips-linux-x64@1.2.0': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': - optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.0.4': - optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.0': optional: true - '@img/sharp-linux-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.4 - optional: true - '@img/sharp-linux-arm64@0.34.3': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.2.0 optional: true - '@img/sharp-linux-arm@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.5 - optional: true - '@img/sharp-linux-arm@0.34.3': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.2.0 @@ -16838,51 +15813,26 @@ snapshots: '@img/sharp-libvips-linux-ppc64': 1.2.0 optional: true - '@img/sharp-linux-s390x@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.4 - optional: true - '@img/sharp-linux-s390x@0.34.3': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.2.0 optional: true - '@img/sharp-linux-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.4 - optional: true - '@img/sharp-linux-x64@0.34.3': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.2.0 optional: true - '@img/sharp-linuxmusl-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - optional: true - '@img/sharp-linuxmusl-arm64@0.34.3': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 optional: true - '@img/sharp-linuxmusl-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - optional: true - '@img/sharp-linuxmusl-x64@0.34.3': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.2.0 optional: true - '@img/sharp-wasm32@0.33.5': - dependencies: - '@emnapi/runtime': 1.4.5 - optional: true - '@img/sharp-wasm32@0.34.3': dependencies: '@emnapi/runtime': 1.4.5 @@ -16891,15 +15841,9 @@ snapshots: '@img/sharp-win32-arm64@0.34.3': optional: true - '@img/sharp-win32-ia32@0.33.5': - optional: true - '@img/sharp-win32-ia32@0.34.3': optional: true - '@img/sharp-win32-x64@0.33.5': - optional: true - '@img/sharp-win32-x64@0.34.3': optional: true @@ -19008,33 +17952,6 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@puppeteer/browsers@2.10.8': - dependencies: - debug: 4.4.1 - extract-zip: 2.0.1 - progress: 2.0.3 - proxy-agent: 6.5.0 - semver: 7.7.2 - tar-fs: 3.1.0 - yargs: 17.7.2 - transitivePeerDependencies: - - bare-buffer - - supports-color - - '@puppeteer/browsers@2.6.1': - dependencies: - debug: 4.4.1 - extract-zip: 2.0.1 - progress: 2.0.3 - proxy-agent: 6.5.0 - semver: 7.7.2 - tar-fs: 3.1.0 - unbzip2-stream: 1.4.3 - yargs: 17.7.2 - transitivePeerDependencies: - - bare-buffer - - supports-color - '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -21019,13 +19936,6 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.1.17 - '@tailwindcss/vite@4.1.17(vite@6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': - dependencies: - '@tailwindcss/node': 4.1.17 - '@tailwindcss/oxide': 4.1.17 - tailwindcss: 4.1.17 - vite: 6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) - '@tanstack/query-core@5.85.5': {} '@tanstack/query-devtools@5.84.0': {} @@ -21421,10 +20331,6 @@ snapshots: '@types/estree@1.0.8': {} - '@types/fontkit@2.0.8': - dependencies: - '@types/node': 22.17.2 - '@types/geojson@7946.0.16': {} '@types/glob@7.2.0': @@ -21629,11 +20535,6 @@ snapshots: '@types/uuid@9.0.8': {} - '@types/yauzl@2.10.3': - dependencies: - '@types/node': 22.17.2 - optional: true - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260119.1': optional: true @@ -21959,18 +20860,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': - dependencies: - '@babel/core': 7.28.3 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.3) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.3) - '@rolldown/pluginutils': 1.0.0-beta.27 - '@types/babel__core': 7.20.5 - react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) - transitivePeerDependencies: - - supports-color - '@vitest/browser@3.0.5(@types/node@20.19.11)(playwright@1.53.0)(typescript@5.9.2)(vite@6.3.5(@types/node@20.19.11)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 @@ -22087,56 +20976,6 @@ snapshots: loupe: 3.2.0 tinyrainbow: 2.0.0 - '@volar/kit@2.4.23(typescript@5.9.2)': - dependencies: - '@volar/language-service': 2.4.23 - '@volar/typescript': 2.4.23 - typesafe-path: 0.2.2 - typescript: 5.9.2 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - - '@volar/language-core@2.4.23': - dependencies: - '@volar/source-map': 2.4.23 - - '@volar/language-server@2.4.23': - dependencies: - '@volar/language-core': 2.4.23 - '@volar/language-service': 2.4.23 - '@volar/typescript': 2.4.23 - path-browserify: 1.0.1 - request-light: 0.7.0 - vscode-languageserver: 9.0.1 - vscode-languageserver-protocol: 3.17.5 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - - '@volar/language-service@2.4.23': - dependencies: - '@volar/language-core': 2.4.23 - vscode-languageserver-protocol: 3.17.5 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - - '@volar/source-map@2.4.23': {} - - '@volar/typescript@2.4.23': - dependencies: - '@volar/language-core': 2.4.23 - path-browserify: 1.0.1 - vscode-uri: 3.1.0 - - '@vscode/emmet-helper@2.11.0': - dependencies: - emmet: 2.4.11 - jsonc-parser: 2.3.1 - vscode-languageserver-textdocument: 1.0.12 - vscode-languageserver-types: 3.17.5 - vscode-uri: 3.1.0 - - '@vscode/l10n@0.0.18': {} - '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -22434,122 +21273,6 @@ snapshots: astring@1.9.0: {} - astro-pdf@1.7.2(astro@5.13.5(@types/node@22.17.2)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.46.4)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1))(typescript@5.9.2): - dependencies: - '@puppeteer/browsers': 2.10.8 - astro: 5.13.5(@types/node@22.17.2)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.46.4)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) - kleur: 4.1.5 - p-map: 7.0.3 - puppeteer: 23.11.1(typescript@5.9.2) - transitivePeerDependencies: - - bare-buffer - - bufferutil - - supports-color - - typescript - - utf-8-validate - - astro@5.13.5(@types/node@22.17.2)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.46.4)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1): - dependencies: - '@astrojs/compiler': 2.12.2 - '@astrojs/internal-helpers': 0.7.2 - '@astrojs/markdown-remark': 6.3.6 - '@astrojs/telemetry': 3.3.0 - '@capsizecss/unpack': 2.4.0 - '@oslojs/encoding': 1.1.0 - '@rollup/pluginutils': 5.2.0(rollup@4.46.4) - acorn: 8.15.0 - aria-query: 5.3.2 - axobject-query: 4.1.0 - boxen: 8.0.1 - ci-info: 4.3.0 - clsx: 2.1.1 - common-ancestor-path: 1.0.1 - cookie: 1.0.2 - cssesc: 3.0.0 - debug: 4.4.1 - deterministic-object-hash: 2.0.2 - devalue: 5.3.2 - diff: 5.2.0 - dlv: 1.1.3 - dset: 3.1.4 - es-module-lexer: 1.7.0 - esbuild: 0.25.9 - estree-walker: 3.0.3 - flattie: 1.1.1 - fontace: 0.3.0 - github-slugger: 2.0.0 - html-escaper: 3.0.3 - http-cache-semantics: 4.2.0 - import-meta-resolve: 4.2.0 - js-yaml: 4.1.0 - kleur: 4.1.5 - magic-string: 0.30.17 - magicast: 0.3.5 - mrmime: 2.0.1 - neotraverse: 0.6.18 - p-limit: 6.2.0 - p-queue: 8.1.0 - package-manager-detector: 1.3.0 - picomatch: 4.0.3 - prompts: 2.4.2 - rehype: 13.0.2 - semver: 7.7.2 - shiki: 3.11.0 - smol-toml: 1.4.2 - tinyexec: 0.3.2 - tinyglobby: 0.2.14 - tsconfck: 3.1.6(typescript@5.9.2) - ultrahtml: 1.6.0 - unifont: 0.5.2 - unist-util-visit: 5.0.0 - unstorage: 1.17.0(ioredis@5.7.0) - vfile: 6.0.3 - vite: 6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) - vitefu: 1.1.1(vite@6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) - xxhash-wasm: 1.1.0 - yargs-parser: 21.1.1 - yocto-spinner: 0.2.3 - zod: 3.25.76 - zod-to-json-schema: 3.24.6(zod@3.25.76) - zod-to-ts: 1.2.0(typescript@5.9.2)(zod@3.25.76) - optionalDependencies: - sharp: 0.33.5 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@types/node' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - db0 - - encoding - - idb-keyval - - ioredis - - jiti - - less - - lightningcss - - rollup - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - typescript - - uploadthing - - yaml - async-function@1.0.0: {} async@3.2.6: {} @@ -22582,8 +21305,6 @@ snapshots: transitivePeerDependencies: - debug - axobject-query@4.1.0: {} - b4a@1.6.7: {} babel-plugin-macros@3.1.0: @@ -22623,30 +21344,6 @@ snapshots: bare-events@2.6.1: optional: true - bare-fs@4.2.2: - dependencies: - bare-events: 2.6.1 - bare-path: 3.0.0 - bare-stream: 2.7.0(bare-events@2.6.1) - optional: true - - bare-os@3.6.2: - optional: true - - bare-path@3.0.0: - dependencies: - bare-os: 3.6.2 - optional: true - - bare-stream@2.7.0(bare-events@2.6.1): - dependencies: - streamx: 2.22.1 - optionalDependencies: - bare-events: 2.6.1 - optional: true - - base-64@1.0.0: {} - base16@1.0.0: {} base64-js@1.5.1: {} @@ -22674,8 +21371,6 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - blob-to-buffer@1.2.9: {} - bowser@2.12.0: {} boxen@7.0.0: @@ -22689,17 +21384,6 @@ snapshots: widest-line: 4.0.1 wrap-ansi: 8.1.0 - boxen@8.0.1: - dependencies: - ansi-align: 3.0.1 - camelcase: 8.0.0 - chalk: 5.6.0 - cli-boxes: 3.0.0 - string-width: 7.2.0 - type-fest: 4.41.0 - widest-line: 5.0.0 - wrap-ansi: 9.0.0 - brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -22713,10 +21397,6 @@ snapshots: dependencies: fill-range: 7.1.1 - brotli@1.3.3: - dependencies: - base64-js: 1.5.1 - browserslist@4.25.3: dependencies: caniuse-lite: 1.0.30001735 @@ -22777,8 +21457,6 @@ snapshots: camelcase@7.0.1: {} - camelcase@8.0.0: {} - caniuse-lite@1.0.30001735: {} case-anything@2.1.13: {} @@ -22906,18 +21584,10 @@ snapshots: chrome-trace-event@1.0.4: {} - chromium-bidi@0.11.0(devtools-protocol@0.0.1367902): - dependencies: - devtools-protocol: 0.0.1367902 - mitt: 3.0.1 - zod: 3.23.8 - ci-info@3.8.0: {} ci-info@3.9.0: {} - ci-info@4.3.0: {} - cjs-module-lexer@1.4.3: {} class-variance-authority@0.7.1: @@ -22982,8 +21652,6 @@ snapshots: clone@1.0.4: {} - clone@2.1.2: {} - clsx@2.1.1: {} cluster-key-slot@1.1.2: {} @@ -23056,8 +21724,6 @@ snapshots: commander@8.3.0: {} - common-ancestor-path@1.0.1: {} - commondir@1.0.1: {} compress-commons@4.1.2: @@ -23126,12 +21792,8 @@ snapshots: convert-source-map@2.0.0: {} - cookie-es@1.2.2: {} - cookie@0.7.2: {} - cookie@1.0.2: {} - core-js-compat@3.45.0: dependencies: browserslist: 4.25.3 @@ -23170,15 +21832,6 @@ snapshots: optionalDependencies: typescript: 5.9.2 - cosmiconfig@9.0.0(typescript@5.9.2): - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - parse-json: 5.2.0 - optionalDependencies: - typescript: 5.9.2 - crc-32@1.2.2: {} crc32-stream@4.0.3: @@ -23197,12 +21850,6 @@ snapshots: cropperjs@1.6.2: {} - cross-fetch@3.2.0: - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -23215,17 +21862,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - crossws@0.3.5: - dependencies: - uncrypto: 0.1.3 - crypto-random-string@2.0.0: {} - css-tree@3.1.0: - dependencies: - mdn-data: 2.12.2 - source-map-js: 1.2.1 - css.escape@1.5.1: {} cssesc@3.0.0: {} @@ -23512,8 +22150,6 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - defu@6.1.4: {} - degenerator@5.0.1: dependencies: ast-types: 0.13.4 @@ -23552,42 +22188,26 @@ snapshots: dequal@2.0.3: {} - destr@2.0.5: {} - detect-indent@6.1.0: {} detect-libc@2.0.4: {} detect-node-es@1.1.0: {} - deterministic-object-hash@2.0.2: - dependencies: - base-64: 1.0.0 - - devalue@5.3.2: {} - devlop@1.1.0: dependencies: dequal: 2.0.3 - devtools-protocol@0.0.1367902: {} - - dfa@1.2.0: {} - diacritics@1.3.0: {} diff-match-patch@1.0.5: {} diff@4.0.2: {} - diff@5.2.0: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 - dlv@1.1.3: {} - doctrine@3.0.0: dependencies: esutils: 2.0.3 @@ -23644,8 +22264,6 @@ snapshots: dotenv@16.6.1: {} - dset@3.1.4: {} - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -23660,13 +22278,6 @@ snapshots: electron-to-chromium@1.5.207: {} - emmet@2.4.11: - dependencies: - '@emmetio/abbreviation': 2.3.3 - '@emmetio/css-abbreviation': 2.1.8 - - emoji-regex@10.5.0: {} - emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -24023,16 +22634,6 @@ snapshots: - supports-color - tedious - extract-zip@2.0.1: - dependencies: - debug: 4.4.1 - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - fast-copy@3.0.2: {} fast-copy@4.0.2: {} @@ -24075,10 +22676,6 @@ snapshots: dependencies: format: 0.2.2 - fd-slicer@1.1.0: - dependencies: - pend: 1.2.0 - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -24117,27 +22714,8 @@ snapshots: path-exists: 5.0.0 unicorn-magic: 0.1.0 - flattie@1.1.1: {} - follow-redirects@1.15.11: {} - fontace@0.3.0: - dependencies: - '@types/fontkit': 2.0.8 - fontkit: 2.0.4 - - fontkit@2.0.4: - dependencies: - '@swc/helpers': 0.5.17 - brotli: 1.3.3 - clone: 2.1.2 - dfa: 1.2.0 - fast-deep-equal: 3.1.3 - restructure: 3.0.2 - tiny-inflate: 1.0.3 - unicode-properties: 1.4.1 - unicode-trie: 2.0.0 - for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -24240,8 +22818,6 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.3.1: {} - get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -24264,10 +22840,6 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@5.2.0: - dependencies: - pump: 3.0.3 - get-stream@6.0.1: {} get-stream@8.0.1: {} @@ -24468,18 +23040,6 @@ snapshots: - encoding - supports-color - h3@1.15.4: - dependencies: - cookie-es: 1.2.2 - crossws: 0.3.5 - defu: 6.1.4 - destr: 2.0.5 - iron-webcrypto: 1.2.1 - node-mock-http: 1.0.2 - radix3: 1.1.2 - ufo: 1.6.1 - uncrypto: 0.1.3 - hachure-fill@0.5.2: {} handlebars@4.7.8: @@ -24767,8 +23327,6 @@ snapshots: html-escaper@2.0.2: {} - html-escaper@3.0.3: {} - html-to-text@9.0.5: dependencies: '@selderee/plugin-htmlparser2': 0.11.0 @@ -24794,8 +23352,6 @@ snapshots: domutils: 3.2.2 entities: 4.5.0 - http-cache-semantics@4.2.0: {} - http-proxy-agent@7.0.0: dependencies: agent-base: 7.1.4 @@ -24873,8 +23429,6 @@ snapshots: cjs-module-lexer: 1.4.3 module-details-from-path: 1.0.4 - import-meta-resolve@4.2.0: {} - indent-string@4.0.0: {} inflight@1.0.6: @@ -24958,8 +23512,6 @@ snapshots: ip-address@10.0.1: {} - iron-webcrypto@1.2.1: {} - is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -25292,10 +23844,6 @@ snapshots: jsonata@2.1.0: {} - jsonc-parser@2.3.1: {} - - jsonc-parser@3.3.1: {} - jsondiffpatch@0.4.1: dependencies: chalk: 2.4.2 @@ -25762,12 +24310,6 @@ snapshots: marked: 7.0.4 react: 19.2.3 - mdast-util-definitions@6.0.0: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - unist-util-visit: 5.0.0 - mdast-util-directive@3.1.0: dependencies: '@types/mdast': 4.0.4 @@ -25968,8 +24510,6 @@ snapshots: dependencies: '@types/mdast': 4.0.4 - mdn-data@2.12.2: {} - mdurl@2.0.0: {} memfs-browser@3.5.10302: @@ -26457,8 +24997,6 @@ snapshots: mudder@2.1.1: {} - muggle-string@0.4.1: {} - mute-stream@0.0.8: {} mute-stream@2.0.0: {} @@ -26475,8 +25013,6 @@ snapshots: neo-async@2.6.2: {} - neotraverse@0.6.18: {} - netmask@2.0.2: {} new-github-issue-url@0.2.1: {} @@ -26616,8 +25152,6 @@ snapshots: dependencies: lower-case: 1.1.4 - node-fetch-native@1.6.7: {} - node-fetch@2.6.11: dependencies: whatwg-url: 5.0.0 @@ -26630,8 +25164,6 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-mock-http@1.0.2: {} - node-plop@0.26.3: dependencies: '@babel/runtime-corejs3': 7.28.3 @@ -26729,14 +25261,6 @@ snapshots: dependencies: isobject: 3.0.1 - ofetch@1.4.1: - dependencies: - destr: 2.0.5 - node-fetch-native: 1.6.7 - ufo: 1.6.1 - - ohash@2.0.11: {} - on-exit-leak-free@2.1.2: {} on-headers@1.0.2: {} @@ -26845,10 +25369,6 @@ snapshots: dependencies: yocto-queue: 1.2.1 - p-limit@6.2.0: - dependencies: - yocto-queue: 1.2.1 - p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -26923,8 +25443,6 @@ snapshots: '@pagefind/linux-x64': 1.3.0 '@pagefind/windows-x64': 1.3.0 - pako@0.2.9: {} - param-case@2.1.1: dependencies: no-case: 2.3.2 @@ -27034,8 +25552,6 @@ snapshots: peberminta@0.9.0: {} - pend@1.2.0: {} - pg-cloudflare@1.2.7: optional: true @@ -27222,16 +25738,6 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-astro@0.14.1: - dependencies: - '@astrojs/compiler': 2.12.2 - prettier: 3.6.2 - sass-formatter: 0.7.9 - optional: true - - prettier@2.8.7: - optional: true - prettier@3.6.2: {} pretty-bytes@6.1.1: {} @@ -27470,35 +25976,6 @@ snapshots: punycode@2.3.1: {} - puppeteer-core@23.11.1: - dependencies: - '@puppeteer/browsers': 2.6.1 - chromium-bidi: 0.11.0(devtools-protocol@0.0.1367902) - debug: 4.4.1 - devtools-protocol: 0.0.1367902 - typed-query-selector: 2.12.0 - ws: 8.18.3 - transitivePeerDependencies: - - bare-buffer - - bufferutil - - supports-color - - utf-8-validate - - puppeteer@23.11.1(typescript@5.9.2): - dependencies: - '@puppeteer/browsers': 2.6.1 - chromium-bidi: 0.11.0(devtools-protocol@0.0.1367902) - cosmiconfig: 9.0.0(typescript@5.9.2) - devtools-protocol: 0.0.1367902 - puppeteer-core: 23.11.1 - typed-query-selector: 2.12.0 - transitivePeerDependencies: - - bare-buffer - - bufferutil - - supports-color - - typescript - - utf-8-validate - qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -27513,8 +25990,6 @@ snapshots: quick-lru@5.1.1: {} - radix3@1.1.2: {} - ramda@0.30.1: {} ramda@0.31.3: {} @@ -28075,10 +26550,6 @@ snapshots: replace-string@3.1.0: {} - request-light@0.5.8: {} - - request-light@0.7.0: {} - require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -28127,8 +26598,6 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - restructure@3.0.2: {} - retext-latin@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -28230,9 +26699,6 @@ snapshots: dependencies: tslib: 2.8.1 - s.color@0.0.15: - optional: true - safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -28260,11 +26726,6 @@ snapshots: safer-buffer@2.1.2: {} - sass-formatter@0.7.9: - dependencies: - suf-log: 2.5.3 - optional: true - saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -28372,33 +26833,6 @@ snapshots: shallow-equal@3.1.0: {} - sharp@0.33.5: - dependencies: - color: 4.2.3 - detect-libc: 2.0.4 - semver: 7.7.2 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.5 - '@img/sharp-darwin-x64': 0.33.5 - '@img/sharp-libvips-darwin-arm64': 1.0.4 - '@img/sharp-libvips-darwin-x64': 1.0.4 - '@img/sharp-libvips-linux-arm': 1.0.5 - '@img/sharp-libvips-linux-arm64': 1.0.4 - '@img/sharp-libvips-linux-s390x': 1.0.4 - '@img/sharp-libvips-linux-x64': 1.0.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - '@img/sharp-linux-arm': 0.33.5 - '@img/sharp-linux-arm64': 0.33.5 - '@img/sharp-linux-s390x': 0.33.5 - '@img/sharp-linux-x64': 0.33.5 - '@img/sharp-linuxmusl-arm64': 0.33.5 - '@img/sharp-linuxmusl-x64': 0.33.5 - '@img/sharp-wasm32': 0.33.5 - '@img/sharp-win32-ia32': 0.33.5 - '@img/sharp-win32-x64': 0.33.5 - optional: true - sharp@0.34.3: dependencies: color: 4.2.3 @@ -28513,8 +26947,6 @@ snapshots: smart-buffer@4.2.0: {} - smol-toml@1.4.2: {} - snake-case@2.1.0: dependencies: no-case: 2.3.2 @@ -28685,12 +27117,6 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 - string-width@7.2.0: - dependencies: - emoji-regex: 10.5.0 - get-east-asian-width: 1.3.1 - strip-ansi: 7.1.0 - string.prototype.trim@1.2.10: dependencies: call-bind: 1.0.8 @@ -28796,11 +27222,6 @@ snapshots: stylis@4.3.6: {} - suf-log@2.5.3: - dependencies: - s.color: 0.0.15 - optional: true - supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -28845,16 +27266,6 @@ snapshots: tapable@2.2.2: {} - tar-fs@3.1.0: - dependencies: - pump: 3.0.3 - tar-stream: 3.1.7 - optionalDependencies: - bare-fs: 4.2.2 - bare-path: 3.0.0 - transitivePeerDependencies: - - bare-buffer - tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -28944,8 +27355,6 @@ snapshots: tildify@2.0.0: {} - tiny-inflate@1.0.3: {} - tiny-invariant@1.3.3: {} tinybench@2.9.0: {} @@ -29202,8 +27611,6 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typed-query-selector@2.12.0: {} - typedarray.prototype.slice@1.0.5: dependencies: call-bind: 1.0.8 @@ -29217,12 +27624,6 @@ snapshots: typedarray@0.0.6: {} - typesafe-path@0.2.2: {} - - typescript-auto-import-cache@0.3.6: - dependencies: - semver: 7.7.2 - typescript@5.9.2: {} uc.micro@2.1.0: {} @@ -29232,8 +27633,6 @@ snapshots: uglify-js@3.19.3: optional: true - ultrahtml@1.6.0: {} - unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -29241,13 +27640,6 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - unbzip2-stream@1.4.3: - dependencies: - buffer: 5.7.1 - through: 2.3.8 - - uncrypto@0.1.3: {} - undici-types@6.21.0: {} undici-types@7.10.0: {} @@ -29261,18 +27653,8 @@ snapshots: unicode-match-property-value-ecmascript@2.2.0: {} - unicode-properties@1.4.1: - dependencies: - base64-js: 1.5.1 - unicode-trie: 2.0.0 - unicode-property-aliases-ecmascript@2.1.0: {} - unicode-trie@2.0.0: - dependencies: - pako: 0.2.9 - tiny-inflate: 1.0.3 - unicorn-magic@0.1.0: {} unified@11.0.5: @@ -29285,12 +27667,6 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 - unifont@0.5.2: - dependencies: - css-tree: 3.1.0 - ofetch: 1.4.1 - ohash: 2.0.11 - unique-string@2.0.0: dependencies: crypto-random-string: 2.0.0 @@ -29384,19 +27760,6 @@ snapshots: acorn: 8.15.0 webpack-virtual-modules: 0.6.2 - unstorage@1.17.0(ioredis@5.7.0): - dependencies: - anymatch: 3.1.3 - chokidar: 4.0.3 - destr: 2.0.5 - h3: 1.15.4 - lru-cache: 10.4.3 - node-fetch-native: 1.6.7 - ofetch: 1.4.1 - ufo: 1.6.1 - optionalDependencies: - ioredis: 5.7.0 - update-browserslist-db@1.1.3(browserslist@4.25.3): dependencies: browserslist: 4.25.3 @@ -29585,23 +27948,6 @@ snapshots: tsx: 4.20.5 yaml: 2.8.1 - vite@6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1): - dependencies: - esbuild: 0.25.9 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.46.4 - tinyglobby: 0.2.14 - optionalDependencies: - '@types/node': 22.17.2 - fsevents: 2.3.3 - jiti: 2.6.1 - lightningcss: 1.30.2 - terser: 5.43.1 - tsx: 4.20.5 - yaml: 2.8.1 - vite@6.3.5(@types/node@24.3.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1): dependencies: esbuild: 0.25.9 @@ -29619,10 +27965,6 @@ snapshots: tsx: 4.20.5 yaml: 2.8.1 - vitefu@1.1.1(vite@6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)): - optionalDependencies: - vite: 6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) - vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.11)(@vitest/browser@3.0.5)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.10.5(@types/node@20.19.11)(typescript@5.9.2))(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 @@ -29710,93 +28052,8 @@ snapshots: - tsx - yaml - volar-service-css@0.0.62(@volar/language-service@2.4.23): - dependencies: - vscode-css-languageservice: 6.3.7 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - optionalDependencies: - '@volar/language-service': 2.4.23 - - volar-service-emmet@0.0.62(@volar/language-service@2.4.23): - dependencies: - '@emmetio/css-parser': 0.4.0 - '@emmetio/html-matcher': 1.3.0 - '@vscode/emmet-helper': 2.11.0 - vscode-uri: 3.1.0 - optionalDependencies: - '@volar/language-service': 2.4.23 - - volar-service-html@0.0.62(@volar/language-service@2.4.23): - dependencies: - vscode-html-languageservice: 5.5.1 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - optionalDependencies: - '@volar/language-service': 2.4.23 - - volar-service-prettier@0.0.62(@volar/language-service@2.4.23)(prettier@3.6.2): - dependencies: - vscode-uri: 3.1.0 - optionalDependencies: - '@volar/language-service': 2.4.23 - prettier: 3.6.2 - - volar-service-typescript-twoslash-queries@0.0.62(@volar/language-service@2.4.23): - dependencies: - vscode-uri: 3.1.0 - optionalDependencies: - '@volar/language-service': 2.4.23 - - volar-service-typescript@0.0.62(@volar/language-service@2.4.23): - dependencies: - path-browserify: 1.0.1 - semver: 7.7.2 - typescript-auto-import-cache: 0.3.6 - vscode-languageserver-textdocument: 1.0.12 - vscode-nls: 5.2.0 - vscode-uri: 3.1.0 - optionalDependencies: - '@volar/language-service': 2.4.23 - - volar-service-yaml@0.0.62(@volar/language-service@2.4.23): - dependencies: - vscode-uri: 3.1.0 - yaml-language-server: 1.15.0 - optionalDependencies: - '@volar/language-service': 2.4.23 - - vscode-css-languageservice@6.3.7: - dependencies: - '@vscode/l10n': 0.0.18 - vscode-languageserver-textdocument: 1.0.12 - vscode-languageserver-types: 3.17.5 - vscode-uri: 3.1.0 - - vscode-html-languageservice@5.5.1: - dependencies: - '@vscode/l10n': 0.0.18 - vscode-languageserver-textdocument: 1.0.12 - vscode-languageserver-types: 3.17.5 - vscode-uri: 3.1.0 - - vscode-json-languageservice@4.1.8: - dependencies: - jsonc-parser: 3.3.1 - vscode-languageserver-textdocument: 1.0.12 - vscode-languageserver-types: 3.17.5 - vscode-nls: 5.2.0 - vscode-uri: 3.1.0 - - vscode-jsonrpc@6.0.0: {} - vscode-jsonrpc@8.2.0: {} - vscode-languageserver-protocol@3.16.0: - dependencies: - vscode-jsonrpc: 6.0.0 - vscode-languageserver-types: 3.16.0 - vscode-languageserver-protocol@3.17.5: dependencies: vscode-jsonrpc: 8.2.0 @@ -29804,24 +28061,14 @@ snapshots: vscode-languageserver-textdocument@1.0.12: {} - vscode-languageserver-types@3.16.0: {} - vscode-languageserver-types@3.17.5: {} - vscode-languageserver@7.0.0: - dependencies: - vscode-languageserver-protocol: 3.16.0 - vscode-languageserver@9.0.1: dependencies: vscode-languageserver-protocol: 3.17.5 - vscode-nls@5.2.0: {} - vscode-uri@3.0.8: {} - vscode-uri@3.1.0: {} - w3c-keyname@2.2.8: {} w3c-xmlserializer@5.0.0: @@ -29926,8 +28173,6 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-pm-runs@1.1.0: {} - which-typed-array@1.1.19: dependencies: available-typed-arrays: 1.0.7 @@ -29953,10 +28198,6 @@ snapshots: dependencies: string-width: 5.1.2 - widest-line@5.0.0: - dependencies: - string-width: 7.2.0 - wildcard@1.1.2: {} word-wrap@1.2.5: {} @@ -29981,12 +28222,6 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 - wrap-ansi@9.0.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 7.2.0 - strip-ansi: 7.1.0 - wrappy@1.0.2: {} ws@8.17.1: {} @@ -29999,8 +28234,6 @@ snapshots: xtend@4.0.2: {} - xxhash-wasm@1.1.0: {} - y18n@5.0.8: {} yallist@3.1.1: {} @@ -30009,25 +28242,8 @@ snapshots: yallist@5.0.0: {} - yaml-language-server@1.15.0: - dependencies: - ajv: 8.17.1 - lodash: 4.17.21 - request-light: 0.5.8 - vscode-json-languageservice: 4.1.8 - vscode-languageserver: 7.0.0 - vscode-languageserver-textdocument: 1.0.12 - vscode-languageserver-types: 3.17.5 - vscode-nls: 5.2.0 - vscode-uri: 3.1.0 - yaml: 2.2.2 - optionalDependencies: - prettier: 2.8.7 - yaml@1.10.2: {} - yaml@2.2.2: {} - yaml@2.3.1: {} yaml@2.8.1: {} @@ -30044,11 +28260,6 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yauzl@2.10.0: - dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 - yjs@13.6.19: dependencies: lib0: 0.2.114 @@ -30059,14 +28270,8 @@ snapshots: yocto-queue@1.2.1: {} - yocto-spinner@0.2.3: - dependencies: - yoctocolors: 2.1.2 - yoctocolors-cjs@2.1.2: {} - yoctocolors@2.1.2: {} - zip-stream@4.1.1: dependencies: archiver-utils: 3.0.4 @@ -30079,21 +28284,10 @@ snapshots: compress-commons: 5.0.3 readable-stream: 3.6.2 - zod-to-json-schema@3.24.6(zod@3.25.76): - dependencies: - zod: 3.25.76 - - zod-to-ts@1.2.0(typescript@5.9.2)(zod@3.25.76): - dependencies: - typescript: 5.9.2 - zod: 3.25.76 - zod-validation-error@5.0.0(zod@3.25.76): dependencies: zod: 3.25.76 - zod@3.23.8: {} - zod@3.25.76: {} zod@4.0.0-beta.20250424T163858: diff --git a/self-host/caddy/Caddyfile b/self-host/caddy/Caddyfile index d381c02f35..4efdbcd159 100644 --- a/self-host/caddy/Caddyfile +++ b/self-host/caddy/Caddyfile @@ -3,9 +3,9 @@ } example.com { - # keep this if you want your files to be accessible at example.com/a* + # keep this if you want your files to be accessible at example.com/assets* # this should instead be a subdomain, like assets.example.com - handle_path /a* { + handle_path /assets* { reverse_proxy minio:9000 } @@ -18,16 +18,24 @@ example.com { } # serve static sites from s3/minio - # requires caddy built with s3fs module (see Dockerfile.caddy) - handle_path /sites/* { - root * /sites - file_server { - fs s3 { - bucket {$S3_BUCKET_NAME:assets} - region {$S3_REGION:us-east-1} - endpoint {$S3_ENDPOINT:http://minio:9000} - access_key {$S3_ACCESS_KEY} - secret_key {$S3_SECRET_KEY} + handle_path /sites/* { + @pathWithSlash path_regexp dir (.+)/$ + handle @pathWithSlash { + redir {re.dir.1} permanent + } + + rewrite * /assets{uri} + + reverse_proxy host.docker.internal:{minio_port} { + @error status 403 404 + handle_response @error { + rewrite * {uri}/index.html + reverse_proxy host.docker.internal:{minio_port} { + @nestedError status 404 + handle_response @nestedError { + respond "Not found" 404 + } + } } } } diff --git a/self-host/docker-compose.yml b/self-host/docker-compose.yml index 3cc5be40c5..30604e10a6 100644 --- a/self-host/docker-compose.yml +++ b/self-host/docker-compose.yml @@ -13,7 +13,7 @@ services: minio-init: condition: service_completed_successfully platform: linux/amd64 - image: ghcr.io/pubpub/platform:latest + image: ghcr.io/knowledgefutures/pubplatform:latest env_file: .env environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} @@ -29,7 +29,7 @@ services: db: condition: service_started platform: linux/amd64 - image: ghcr.io/pubpub/platform-jobs:latest + image: ghcr.io/knowledgefutures/pubplatform-jobs:latest env_file: .env environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} @@ -67,7 +67,7 @@ services: # note: requires the caddy-sites image built from Dockerfile.caddy # you can build it with: docker build -f Dockerfile.caddy -t ghcr.io/pubpub/caddy-sites:latest . caddy: - image: ghcr.io/pubpub/caddy-sites:latest + image: caddy:latest depends_on: - platform - platform-jobs @@ -90,7 +90,13 @@ services: image: minio/minio:latest env_file: .env healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/ready"] + test: + [ + "CMD", + "curl", + "-f", + "http://localhost:9000/minio/health/ready", + ] interval: 1m30s timeout: 30s retries: 5 From 568d57b9f01f6a42b00aece301dd9a8f504a4ada Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 14 Apr 2026 13:30:52 +0200 Subject: [PATCH 66/78] fix: readd sitebuilder image to test --- .github/workflows/e2e.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a03400b9d2..10735368a5 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -87,6 +87,7 @@ jobs: run: | echo "core_label=ghcr.io/knowledgefutures/platform:$IMAGE_TAG" >> $GITHUB_OUTPUT echo "jobs_label=ghcr.io/knowledgefutures/platform-jobs:$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 @@ -110,6 +111,7 @@ jobs: env: INTEGRATION_TESTS_IMAGE: ${{ steps.label.outputs.core_label }} JOBS_IMAGE: ${{ steps.label.outputs.jobs_label }} + SITE_BUILDER_IMAGE: ${{ steps.label.outputs.site_builder_label }} - name: Log out Container ID for health check id: log-container-id @@ -131,6 +133,7 @@ jobs: env: INTEGRATION_TESTS_IMAGE: ${{steps.label.outputs.core_label}} JOBS_IMAGE: ${{steps.label.outputs.jobs_label}} + SITE_BUILDER_IMAGE: ${{steps.label.outputs.site_builder_label}} - name: Upload core playwright snapshots artifact if: failure() && matrix.package == 'core' From 3a9874a6e868a3e8a30cf308af36a5f602083f88 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 14 Apr 2026 13:33:52 +0200 Subject: [PATCH 67/78] fix: better error reporting --- core/lib/server/migrate.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/lib/server/migrate.ts b/core/lib/server/migrate.ts index 4b2fa60db6..31e513baac 100644 --- a/core/lib/server/migrate.ts +++ b/core/lib/server/migrate.ts @@ -157,6 +157,9 @@ export async function runMigrations() { .set({ logs: String(err) }) .where("id", "=", id) .execute() + + logger.error({ msg: `Error applying migration ${dir}`, sql: migrationSql, err }) + throw err } From 1e5204d3efb4693399a9fb98d1db5366dd1ce0bd Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 14 Apr 2026 13:37:15 +0200 Subject: [PATCH 68/78] fix: cascade drop --- .../migration.sql | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/core/prisma/migrations/20260408120000_rm_build_journal_site/migration.sql b/core/prisma/migrations/20260408120000_rm_build_journal_site/migration.sql index 61103ae9e7..60d8973667 100644 --- a/core/prisma/migrations/20260408120000_rm_build_journal_site/migration.sql +++ b/core/prisma/migrations/20260408120000_rm_build_journal_site/migration.sql @@ -1,20 +1,35 @@ /* - Warnings: - - - The values [buildJournalSite] on the enum `Action` will be removed. If these variants are still used in the database, this will fail. - -*/ + Warnings: + - The values [buildJournalSite] on the enum `Action` will be removed. If these variants are still used in the database, this will fail. + */ -- Delete buildJournalSite action instances -DELETE FROM "action_instances" WHERE "action" = 'buildJournalSite'; -DELETE FROM "action_config_defaults" WHERE "action" = 'buildJournalSite'; +DELETE FROM "action_instances" +WHERE "action" = 'buildJournalSite'; + +DELETE FROM "action_config_defaults" +WHERE "action" = 'buildJournalSite'; -- AlterEnum BEGIN; -CREATE TYPE "Action_new" AS ENUM ('log', 'email', 'http', 'move', 'googleDriveImport', 'datacite', 'buildSite', 'createPub'); -ALTER TABLE "action_instances" ALTER COLUMN "action" TYPE "Action_new" USING ("action"::text::"Action_new"); -ALTER TABLE "action_config_defaults" ALTER COLUMN "action" TYPE "Action_new" USING ("action"::text::"Action_new"); +CREATE TYPE "Action_new" AS ENUM( + 'log', + 'email', + 'http', + 'move', + 'googleDriveImport', + 'datacite', + 'buildSite', + 'createPub' +); +ALTER TABLE "action_instances" + ALTER COLUMN "action" TYPE "Action_new" + USING ("action"::text::"Action_new"); +ALTER TABLE "action_config_defaults" + ALTER COLUMN "action" TYPE "Action_new" + USING ("action"::text::"Action_new"); ALTER TYPE "Action" RENAME TO "Action_old"; ALTER TYPE "Action_new" RENAME TO "Action"; -DROP TYPE "Action_old"; +DROP TYPE "Action_old" CASCADE; COMMIT; + From d50ea1440c5897227dd39125cfccd4a1c5a27c84 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 14 Apr 2026 13:48:20 +0200 Subject: [PATCH 69/78] fix: allow inbucket --- infra/Caddyfile.gateway | 2 +- infra/stack.preview.yml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/infra/Caddyfile.gateway b/infra/Caddyfile.gateway index d005aa4990..a0cdb60a53 100644 --- a/infra/Caddyfile.gateway +++ b/infra/Caddyfile.gateway @@ -49,7 +49,7 @@ } } - handle_path /emails/* { + handle_path /emails* { reverse_proxy host.docker.internal:{inbucket_port} } diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index ae3d1317a1..047bfcbc5b 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -129,6 +129,8 @@ services: inbucket: image: inbucket/inbucket:latest networks: [appnet] + environment: + INBUCKET_WEB_BASEPATH: /emails ports: - target: 9000 published: ${INBUCKET_PORT} From 4ee8bcd3b6f19e6786857bf650f4d296f3cd0003 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 14 Apr 2026 13:49:58 +0200 Subject: [PATCH 70/78] fix: expose mock notify server port --- infra/Caddyfile.gateway | 10 +++++++--- infra/stack.preview.yml | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/infra/Caddyfile.gateway b/infra/Caddyfile.gateway index a0cdb60a53..159cb6687d 100644 --- a/infra/Caddyfile.gateway +++ b/infra/Caddyfile.gateway @@ -8,13 +8,17 @@ on_demand } - map {labels.2} {platform_port} {builder_port} {minio_port} {minio_console_port} {inbucket_port} { - ~^pr-(\d+)$ "1$1" "2$1" "3$1" "4$1" "5$1" - default "" "" "" "" "" + map {labels.2} {platform_port} {builder_port} {minio_port} {minio_console_port} {inbucket_port} {mock_notify_port} { + ~^pr-(\d+)$ "1$1" "2$1" "3$1" "4$1" "5$1" "6$1" + default "" "" "" "" "" "" } encode gzip + handle_path /mock-notify* { + reverse_proxy host.docker.internal:{mock_notify_port} + } + handle_path /assets* { reverse_proxy host.docker.internal:{minio_port} } diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index 047bfcbc5b..d7b771f55a 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -140,6 +140,20 @@ services: replicas: 1 restart_policy: condition: any + + mock-notify: + image: ghcr.io/knowledgefutures/mock-notify-server:${IMAGE_TAG} + networks: [appnet] + ports: + - target: 8080 + published: ${MOCK_NOTIFY_PORT} + protocol: tcp + mode: host + deploy: + replicas: 1 + restart_policy: + condition: on-failure + max_attempts: 5 networks: appnet: From cc42f5bedc7a638f3ef8d33a4c0f11817919274e Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 14 Apr 2026 14:01:31 +0200 Subject: [PATCH 71/78] fix(skip-build): set env vars correctly --- .github/workflows/deploy-stack.yml | 3 ++- infra/Caddyfile.gateway | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy-stack.yml b/.github/workflows/deploy-stack.yml index 2a186858af..ab8975016f 100644 --- a/.github/workflows/deploy-stack.yml +++ b/.github/workflows/deploy-stack.yml @@ -143,11 +143,12 @@ jobs: MINIO_PORT="3${PR_NUM}" MINIO_CONSOLE_PORT="4${PR_NUM}" INBUCKET_PORT="5${PR_NUM}" + MOCK_NOTIFY_PORT="6${PR_NUM}" sudo env IMAGE_TAG="$IMAGE_TAG" DEPLOY_HOST="$DEPLOY_HOST" \ PLATFORM_PORT="$PLATFORM_PORT" BUILDER_PORT="$BUILDER_PORT" \ MINIO_PORT="$MINIO_PORT" MINIO_CONSOLE_PORT="$MINIO_CONSOLE_PORT" \ - INBUCKET_PORT="$INBUCKET_PORT" \ + INBUCKET_PORT="$INBUCKET_PORT" MOCK_NOTIFY_PORT="$MOCK_NOTIFY_PORT" \ docker stack deploy -c "$STACK_FILE" \ --with-registry-auth --resolve-image always --prune "$STACK_NAME" diff --git a/infra/Caddyfile.gateway b/infra/Caddyfile.gateway index 159cb6687d..fb099d9b64 100644 --- a/infra/Caddyfile.gateway +++ b/infra/Caddyfile.gateway @@ -8,6 +8,7 @@ on_demand } + # this is configured in the deploy-stack.yml workflow map {labels.2} {platform_port} {builder_port} {minio_port} {minio_console_port} {inbucket_port} {mock_notify_port} { ~^pr-(\d+)$ "1$1" "2$1" "3$1" "4$1" "5$1" "6$1" default "" "" "" "" "" "" @@ -15,10 +16,6 @@ encode gzip - handle_path /mock-notify* { - reverse_proxy host.docker.internal:{mock_notify_port} - } - handle_path /assets* { reverse_proxy host.docker.internal:{minio_port} } @@ -57,6 +54,10 @@ reverse_proxy host.docker.internal:{inbucket_port} } + handle_path /mock-notify* { + reverse_proxy host.docker.internal:{mock_notify_port} + } + handle { reverse_proxy host.docker.internal:{platform_port} } From 6f16004d8983bc89630b9dc8129e6e4779cfb5c1 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 14 Apr 2026 14:13:04 +0200 Subject: [PATCH 72/78] fix: correct mock notify --- Dockerfile | 2 -- infra/stack.preview.yml | 4 +++- mock-notify/next.config.ts | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index a554830d7d..b454d542c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -134,8 +134,6 @@ WORKDIR /usr/src/app COPY --from=withpackage --chown=node:node /usr/src/app/core/.next/standalone ./ COPY --from=withpackage --chown=node:node /usr/src/app/core/.next/static ./core/.next/static COPY --from=withpackage --chown=node:node /usr/src/app/core/public ./core/public -# needed to set the database url correctly based on PGHOST variables -COPY --from=withpackage --chown=node:node /usr/src/app/core/.env.docker ./core/.env # migration sql files, applied automatically during startup instrumentation COPY --from=withpackage --chown=node:node /usr/src/app/core/prisma/migrations ./core/prisma/migrations diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index d7b771f55a..a19d9953b5 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -142,8 +142,10 @@ services: condition: any mock-notify: - image: ghcr.io/knowledgefutures/mock-notify-server:${IMAGE_TAG} + image: ghcr.io/knowledgefutures/mock-coar-notify-server:${IMAGE_TAG} networks: [appnet] + environment: + BASE_PATH: /mock-notify ports: - target: 8080 published: ${MOCK_NOTIFY_PORT} diff --git a/mock-notify/next.config.ts b/mock-notify/next.config.ts index f172758858..e0b3350f80 100644 --- a/mock-notify/next.config.ts +++ b/mock-notify/next.config.ts @@ -2,6 +2,7 @@ import type { NextConfig } from "next" const nextConfig: NextConfig = { reactStrictMode: true, + basePath: process.env.BASE_PATH || "", } export default nextConfig From 276fe32421cb34a3f09aa90c2ad3761e02165766 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 14 Apr 2026 14:41:14 +0200 Subject: [PATCH 73/78] fix: fix things --- .github/workflows/deploy-stack.yml | 3 +++ .github/workflows/ghcr-build-all.yml | 1 - Dockerfile | 9 +++++++++ infra/Caddyfile.gateway | 2 +- infra/stack.preview.yml | 4 +--- mock-notify/next.config.ts | 1 + 6 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy-stack.yml b/.github/workflows/deploy-stack.yml index ab8975016f..c448d6b582 100644 --- a/.github/workflows/deploy-stack.yml +++ b/.github/workflows/deploy-stack.yml @@ -135,6 +135,9 @@ jobs: if [[ "$USES_GATEWAY" == "true" ]]; then echo "deploying gateway stack..." sudo docker stack deploy -c stack.gateway.yml --prune gateway + # caddy reads its config once at startup, force a restart so it + # picks up any changes to the bind-mounted Caddyfile + sudo docker service update --force gateway_proxy # derive host ports from PR number (prefix digit + pr number) PR_NUM="${STACK_NAME##*-}" diff --git a/.github/workflows/ghcr-build-all.yml b/.github/workflows/ghcr-build-all.yml index 3139648d6f..6698821132 100644 --- a/.github/workflows/ghcr-build-all.yml +++ b/.github/workflows/ghcr-build-all.yml @@ -64,7 +64,6 @@ jobs: uses: ./.github/workflows/ghcr-build-template.yml with: package: mock-notify - target: jobs ghcr_image_name: mock-coar-notify-server publish_latest: ${{ inputs.publish_latest }} image_tag: ${{ inputs.image_tag }} diff --git a/Dockerfile b/Dockerfile index b454d542c8..fed7443dd6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -138,3 +138,12 @@ COPY --from=withpackage --chown=node:node /usr/src/app/core/public ./core/public COPY --from=withpackage --chown=node:node /usr/src/app/core/prisma/migrations ./core/prisma/migrations CMD ["node", "--enable-source-maps", "core/server.js"] + +### Mock Notify + +FROM prod-setup AS next-app-mock-notify +WORKDIR /usr/src/app +COPY --from=withpackage --chown=node:node /usr/src/app/mock-notify/.next/standalone ./ +COPY --from=withpackage --chown=node:node /usr/src/app/mock-notify/.next/static ./mock-notify/.next/static + +CMD ["node", "mock-notify/server.js"] diff --git a/infra/Caddyfile.gateway b/infra/Caddyfile.gateway index fb099d9b64..716e0827f4 100644 --- a/infra/Caddyfile.gateway +++ b/infra/Caddyfile.gateway @@ -50,7 +50,7 @@ } } - handle_path /emails* { + handle /emails* { reverse_proxy host.docker.internal:{inbucket_port} } diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index a19d9953b5..bd090bc402 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -144,10 +144,8 @@ services: mock-notify: image: ghcr.io/knowledgefutures/mock-coar-notify-server:${IMAGE_TAG} networks: [appnet] - environment: - BASE_PATH: /mock-notify ports: - - target: 8080 + - target: 3000 published: ${MOCK_NOTIFY_PORT} protocol: tcp mode: host diff --git a/mock-notify/next.config.ts b/mock-notify/next.config.ts index e0b3350f80..2eb04d5c31 100644 --- a/mock-notify/next.config.ts +++ b/mock-notify/next.config.ts @@ -3,6 +3,7 @@ import type { NextConfig } from "next" const nextConfig: NextConfig = { reactStrictMode: true, basePath: process.env.BASE_PATH || "", + output: "standalone", } export default nextConfig From a0eecf7d40a19fe00521c010ed98974fdf42fae6 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 14 Apr 2026 15:07:13 +0200 Subject: [PATCH 74/78] fix: debug automation run --- core/actions/api/serverAction.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/actions/api/serverAction.tsx b/core/actions/api/serverAction.tsx index 3a549f084a..1ef3f36c2f 100644 --- a/core/actions/api/serverAction.tsx +++ b/core/actions/api/serverAction.tsx @@ -5,6 +5,7 @@ import type { RunAutomationArgs } from "../_lib/runAutomation" import type { AutomationRunResult } from "../results" import { AutomationEvent, Capabilities, MembershipType } from "db/public" +import { logger } from "logger" import { getLoginData } from "~/lib/authentication/loginData" import { userCan } from "~/lib/authorization/capabilities" @@ -12,7 +13,7 @@ import { defineServerAction } from "~/lib/server/defineServerAction" import { runAutomation } from "../_lib/runAutomation" import { getActionByName } from "." -export const runAutomationManual = defineServerAction(async function runActionInstance( +export const runAutomationManual = defineServerAction(async function runAutomationManual( args: Omit & { manualActionInstancesOverrideArgs: { [actionInstanceId: ActionInstancesId]: Record @@ -85,6 +86,8 @@ export const runAutomationManual = defineServerAction(async function runActionIn }) if (!result.success) { + logger.error({ msg: "Automation run failed", result }) + return { success: false, error: result.actionRuns From 81e9970385f0c4a9ecc19063187e78bb71abb049 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 14 Apr 2026 15:08:30 +0200 Subject: [PATCH 75/78] fix: invert prose in dark mode --- packages/ui/src/editors/LexicalEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/editors/LexicalEditor.tsx b/packages/ui/src/editors/LexicalEditor.tsx index 21377531aa..2aa2ac8850 100644 --- a/packages/ui/src/editors/LexicalEditor.tsx +++ b/packages/ui/src/editors/LexicalEditor.tsx @@ -96,7 +96,7 @@ export const LexicalEditor = (props: LexicalEditorProps) => { ariaLabelledBy={props["aria-labelledby"]} className={cn( "editor", - "prose prose-sm", + "prose prose-sm dark:prose-invert", props.singleLine ? "min-h-5" : "min-h-[200px]", // Copied from ui/src/input.tsx "w-full min-w-0 rounded-md border border-input bg-background px-3 py-1 text-base shadow-xs outline-none transition-[color,box-shadow] selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30", From 5bbd6397da6e68f7de0bcdcd30ad120d43e024f4 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 14 Apr 2026 15:11:02 +0200 Subject: [PATCH 76/78] fix: correct notify subpath --- .github/workflows/ghcr-build-all.yml | 1 + .github/workflows/ghcr-build-template.yml | 5 +++++ Dockerfile | 3 +++ core/prisma/seeds/coar-notify.ts | 2 +- infra/Caddyfile.gateway | 2 +- infra/stack.preview.yml | 1 + 6 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ghcr-build-all.yml b/.github/workflows/ghcr-build-all.yml index 6698821132..1b63f91879 100644 --- a/.github/workflows/ghcr-build-all.yml +++ b/.github/workflows/ghcr-build-all.yml @@ -64,6 +64,7 @@ jobs: uses: ./.github/workflows/ghcr-build-template.yml with: package: mock-notify + base_path: /mock-notify ghcr_image_name: mock-coar-notify-server publish_latest: ${{ inputs.publish_latest }} image_tag: ${{ inputs.image_tag }} diff --git a/.github/workflows/ghcr-build-template.yml b/.github/workflows/ghcr-build-template.yml index 246eb9d86a..1daaf6c96f 100644 --- a/.github/workflows/ghcr-build-template.yml +++ b/.github/workflows/ghcr-build-template.yml @@ -19,6 +19,10 @@ on: image_tag: type: string description: "Override the image tag (e.g. v1.2.3). Falls back to git describe." + base_path: + type: string + default: "" + description: "Base path for Next.js apps (e.g. /mock-notify). Baked into the build." outputs: image-sha: description: 'Full GHCR image ref with SHA tag' @@ -85,6 +89,7 @@ jobs: build-args: | PACKAGE=${{ inputs.package }} CI=true + BASE_PATH=${{ inputs.base_path }} secrets: | SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} target: ${{ steps.label.outputs.target }} diff --git a/Dockerfile b/Dockerfile index fed7443dd6..b737df72d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -78,6 +78,9 @@ ENV DOCKERBUILD=1 ARG CI ENV CI=$CI +ARG BASE_PATH="" +ENV BASE_PATH=$BASE_PATH + RUN --mount=type=secret,id=SENTRY_AUTH_TOKEN,env=SENTRY_AUTH_TOKEN \ pnpm --filter $PACKAGE build diff --git a/core/prisma/seeds/coar-notify.ts b/core/prisma/seeds/coar-notify.ts index a9e927a4e9..c6481609e1 100644 --- a/core/prisma/seeds/coar-notify.ts +++ b/core/prisma/seeds/coar-notify.ts @@ -12,7 +12,7 @@ import { env } from "~/lib/env/env" import { seedCommunity } from "../seed/seedCommunity" const WEBHOOK_PATH = "coar-inbox" -const REMOTE_INBOX_URL = "http://localhost:4001/api/inbox" +const REMOTE_INBOX_URL = process.env.MOCK_NOTIFY_INBOX_URL ?? "http://localhost:4001/api/inbox" const adminId = "dddddddd-dddd-4ddd-dddd-dddddddddd01" as UsersId const joeAuthorId = "dddddddd-dddd-4ddd-dddd-dddddddddd02" as UsersId diff --git a/infra/Caddyfile.gateway b/infra/Caddyfile.gateway index 716e0827f4..14d1672e26 100644 --- a/infra/Caddyfile.gateway +++ b/infra/Caddyfile.gateway @@ -54,7 +54,7 @@ reverse_proxy host.docker.internal:{inbucket_port} } - handle_path /mock-notify* { + handle /mock-notify* { reverse_proxy host.docker.internal:{mock_notify_port} } diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index bd090bc402..f4bedc49f4 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -16,6 +16,7 @@ services: FLAGS: "uploads:off,invites:off,disabled-actions:http+email" DB_RESET: "true" DB_SEED: "true" + MOCK_NOTIFY_INBOX_URL: http://mock-notify:3000/mock-notify/api/inbox networks: [appnet] ports: - target: 3000 From 6427290fe813e0b65324030068449e88ecf67a57 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 14 Apr 2026 15:49:44 +0200 Subject: [PATCH 77/78] feat: set correct urls for mock-notify --- .../developers/docs/openapi.json/openApi.ts | 4 +- infra/stack.preview.yml | 2 + mock-notify/next.config.ts | 7 +- mock-notify/src/app/components/HomeClient.tsx | 179 ++++++++++++++++++ .../src/app/components/NotificationCard.tsx | 7 +- .../app/components/SendNotificationForm.tsx | 102 +++++----- mock-notify/src/app/page.tsx | 175 +---------------- .../app/reviews/sample-review/docmap/route.ts | 7 +- .../src/app/reviews/sample-review/route.ts | 7 +- mock-notify/src/lib/urls.ts | 9 + 10 files changed, 277 insertions(+), 222 deletions(-) create mode 100644 mock-notify/src/app/components/HomeClient.tsx create mode 100644 mock-notify/src/lib/urls.ts diff --git a/core/app/c/[communitySlug]/developers/docs/openapi.json/openApi.ts b/core/app/c/[communitySlug]/developers/docs/openapi.json/openApi.ts index 51c534e8e7..5bdced24fc 100644 --- a/core/app/c/[communitySlug]/developers/docs/openapi.json/openApi.ts +++ b/core/app/c/[communitySlug]/developers/docs/openapi.json/openApi.ts @@ -6,6 +6,8 @@ import { generateOpenApi } from "@ts-rest/open-api" import { siteApi } from "contracts" +import { env } from "~/lib/env/env" + type TraverseContractResult = "SKIP" | "CONTINUE" | "STOP" const traverseContract = ( @@ -54,7 +56,7 @@ export const createOpenApiDocument = (communitySlug?: string): OpenAPIObject => description: "The development API server", }, { - url: "https://app.pubpub.org/", + url: env.PUBPUB_URL, description: "The production API server", }, ], diff --git a/infra/stack.preview.yml b/infra/stack.preview.yml index f4bedc49f4..7f84f8c233 100644 --- a/infra/stack.preview.yml +++ b/infra/stack.preview.yml @@ -145,6 +145,8 @@ services: mock-notify: image: ghcr.io/knowledgefutures/mock-coar-notify-server:${IMAGE_TAG} networks: [appnet] + environment: + PUBPUB_URL: https://${DEPLOY_HOST} ports: - target: 3000 published: ${MOCK_NOTIFY_PORT} diff --git a/mock-notify/next.config.ts b/mock-notify/next.config.ts index 2eb04d5c31..d264af46c1 100644 --- a/mock-notify/next.config.ts +++ b/mock-notify/next.config.ts @@ -1,9 +1,14 @@ import type { NextConfig } from "next" +const basePath = process.env.BASE_PATH || "" + const nextConfig: NextConfig = { reactStrictMode: true, - basePath: process.env.BASE_PATH || "", + basePath, output: "standalone", + env: { + NEXT_PUBLIC_BASE_PATH: basePath, + }, } export default nextConfig diff --git a/mock-notify/src/app/components/HomeClient.tsx b/mock-notify/src/app/components/HomeClient.tsx new file mode 100644 index 0000000000..e1bf4cabff --- /dev/null +++ b/mock-notify/src/app/components/HomeClient.tsx @@ -0,0 +1,179 @@ +"use client" + +import type { PayloadTemplateType } from "~/lib/payloads" +import type { StoredNotification } from "~/lib/store" +import type { AppConfig } from "~/lib/urls" + +import { useCallback, useEffect, useRef, useState } from "react" + +import { apiUrl } from "~/lib/urls" +import { NotificationCard, type ResponsePrefill } from "./NotificationCard" +import { SendNotificationForm } from "./SendNotificationForm" + +export function HomeClient({ config }: { config: AppConfig }) { + const [notifications, setNotifications] = useState([]) + const [filter, setFilter] = useState<"all" | "received" | "sent">("all") + const [isLoading, setIsLoading] = useState(true) + const [prefill, setPrefill] = useState(undefined) + const [formKey, setFormKey] = useState(0) + const formRef = useRef(null) + + const fetchNotifications = useCallback(async () => { + try { + const params = filter !== "all" ? `?direction=${filter}` : "" + const res = await fetch(apiUrl(`/api/notifications${params}`)) + const data = await res.json() + setNotifications(data.notifications) + } catch (error) { + // biome-ignore lint/suspicious/noConsole: shh + console.error("Failed to fetch notifications:", error) + } finally { + setIsLoading(false) + } + }, [filter]) + + useEffect(() => { + void fetchNotifications() + const interval = setInterval(fetchNotifications, 2000) + return () => clearInterval(interval) + }, [fetchNotifications]) + + const handleClearAll = async () => { + if (!confirm("Are you sure you want to clear all notifications?")) return + + await fetch(apiUrl("/api/notifications"), { method: "DELETE" }) + setNotifications([]) + } + + const handleDelete = async (id: string) => { + await fetch(apiUrl(`/api/notifications/${id}`), { method: "DELETE" }) + setNotifications((prev) => prev.filter((n) => n.id !== id)) + } + + const handleRespond = (_responseType: PayloadTemplateType, newPrefill: ResponsePrefill) => { + setPrefill(newPrefill) + setFormKey((k) => k + 1) + formRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }) + } + + const handleSent = () => { + setPrefill(undefined) + void fetchNotifications() + } + + const receivedCount = notifications.filter((n) => n.direction === "received").length + const sentCount = notifications.filter((n) => n.direction === "sent").length + + return ( +
+
+
+ {/* Send Notification Form */} +
+ + {prefill && ( + + )} +
+ + {/* Notifications List */} +
+
+ {/* Header with filters */} +
+
+

Notifications

+
+ + + +
+
+ + {notifications.length > 0 && ( + + )} +
+ + {/* Notifications */} +
+ {isLoading ? ( +
+ Loading notifications... +
+ ) : notifications.length === 0 ? ( +
+ No notifications yet. Send one or wait for incoming + requests. +
+ ) : ( + notifications.map((notification, index) => ( + handleDelete(notification.id)} + onRespond={handleRespond} + /> + )) + )} +
+
+
+
+
+
+ ) +} diff --git a/mock-notify/src/app/components/NotificationCard.tsx b/mock-notify/src/app/components/NotificationCard.tsx index 2dff1e6979..a9221c179e 100644 --- a/mock-notify/src/app/components/NotificationCard.tsx +++ b/mock-notify/src/app/components/NotificationCard.tsx @@ -1,6 +1,7 @@ "use client" import type { StoredNotification } from "~/lib/store" +import type { AppConfig } from "~/lib/urls" import { useState } from "react" @@ -11,6 +12,7 @@ import { } from "~/lib/payloads" interface NotificationCardProps { + config: AppConfig notification: StoredNotification notifications: StoredNotification[] onDelete: () => void @@ -28,6 +30,7 @@ export interface ResponsePrefill { } export function NotificationCard({ + config, notification, notifications, onDelete, @@ -134,7 +137,7 @@ export function NotificationCard({ templateType: responseType, inReplyTo: payload.inReplyTo ?? payload.id, inReplyToObjectUrl, - originUrl: payload.origin?.id ?? "http://localhost:4001", + originUrl: payload.origin?.id ?? config.selfUrl, targetServiceUrl: payload.target?.id ?? "", } onRespond(responseType, prefill) @@ -146,7 +149,7 @@ export function NotificationCard({ templateType: responseType, inReplyTo: payload.id, inReplyToObjectUrl: payload.object?.id ?? "", - originUrl: "http://localhost:4001", + originUrl: config.selfUrl, targetServiceUrl: payload.origin?.id ?? "", } onRespond(responseType, prefill) diff --git a/mock-notify/src/app/components/SendNotificationForm.tsx b/mock-notify/src/app/components/SendNotificationForm.tsx index 58639f0a7f..939190b618 100644 --- a/mock-notify/src/app/components/SendNotificationForm.tsx +++ b/mock-notify/src/app/components/SendNotificationForm.tsx @@ -1,6 +1,7 @@ "use client" import type { CoarNotifyPayload } from "~/lib/store" +import type { AppConfig } from "~/lib/urls" import { useState } from "react" @@ -13,10 +14,11 @@ import { createRejectPayload, type PayloadTemplateType, } from "~/lib/payloads" +import { apiUrl } from "~/lib/urls" interface SendNotificationFormProps { + config: AppConfig onSent: () => void - /** Pre-fill values for a response to an existing notification */ prefill?: { targetUrl?: string templateType?: PayloadTemplateType @@ -38,73 +40,79 @@ const TEMPLATE_OPTIONS: PayloadTemplateType[] = [ "Reject", ] -export function SendNotificationForm({ onSent, prefill }: SendNotificationFormProps) { +function makeTemplateDefaults(pubpubUrl: string): Record< + PayloadTemplateType, + { targetUrl: string; targetServiceUrl: string } +> { + return { + "Offer Review": { + targetUrl: `${pubpubUrl}/api/v0/c/coar-us2-unjournal/site/webhook/coar-inbox`, + targetServiceUrl: `${pubpubUrl}/c/coar-us2-unjournal`, + }, + "Announce Review": { + targetUrl: `${pubpubUrl}/api/v0/c/coar-us1-arcadia/site/webhook/coar-inbox`, + targetServiceUrl: `${pubpubUrl}/c/coar-us1-arcadia`, + }, + "Offer Ingest": { + targetUrl: `${pubpubUrl}/api/v0/c/coar-us4-repository/site/webhook/coar-inbox`, + targetServiceUrl: `${pubpubUrl}/c/coar-us4-repository`, + }, + "Announce Ingest": { + targetUrl: `${pubpubUrl}/api/v0/c/coar-us4-repository/site/webhook/coar-inbox`, + targetServiceUrl: `${pubpubUrl}/c/coar-us4-repository`, + }, + Accept: { + targetUrl: `${pubpubUrl}/api/v0/c/coar-us1-arcadia/site/webhook/coar-inbox`, + targetServiceUrl: `${pubpubUrl}/c/coar-us1-arcadia`, + }, + Reject: { + targetUrl: `${pubpubUrl}/api/v0/c/coar-us1-arcadia/site/webhook/coar-inbox`, + targetServiceUrl: `${pubpubUrl}/c/coar-us1-arcadia`, + }, + } +} + +export function SendNotificationForm({ config, onSent, prefill }: SendNotificationFormProps) { + const { pubpubUrl, selfUrl } = config + const templateDefaults = makeTemplateDefaults(pubpubUrl) + const [mode, setMode] = useState("template") + const [targetUrl, setTargetUrl] = useState( - prefill?.targetUrl ?? - "http://localhost:3000/api/v0/c/coar-us2-unjournal/site/webhook/coar-inbox" + prefill?.targetUrl ?? templateDefaults["Offer Review"].targetUrl ) + const [templateType, setTemplateType] = useState( prefill?.templateType ?? "Offer Review" ) + const [customPayload, setCustomPayload] = useState("") const [isSending, setIsSending] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(false) - // Template fields - using complete URLs instead of IDs const [objectUrl, setObjectUrl] = useState( prefill?.inReplyToObjectUrl ?? "https://www.biorxiv.org/content/10.1101/2024.01.01.123456" ) const [objectCiteAs, setObjectCiteAs] = useState("") const [objectItemUrl, setObjectItemUrl] = useState("") - const [reviewUrl, setReviewUrl] = useState("http://localhost:4001/reviews/sample-review") - const [originUrl, setOriginUrl] = useState(prefill?.originUrl ?? "http://localhost:4001") + const [reviewUrl, setReviewUrl] = useState(`${selfUrl}/reviews/sample-review`) + const [originUrl, setOriginUrl] = useState(prefill?.originUrl ?? selfUrl) const [targetServiceUrl, setTargetServiceUrl] = useState( - prefill?.targetServiceUrl ?? "http://localhost:3000" + prefill?.targetServiceUrl ?? pubpubUrl ) const [serviceName, setServiceName] = useState("Mock Review Service") const [inReplyTo, setInReplyTo] = useState(prefill?.inReplyTo ?? "") const [inReplyToUrl, setInReplyToUrl] = useState(prefill?.inReplyToObjectUrl ?? "") const [workUrl, setWorkUrl] = useState( - "http://localhost:3000/c/coar-us4-repository/pub/{pubId}" + `${pubpubUrl}/c/coar-us4-repository/pub/{pubId}` ) - // Default target URLs per template type for demo convenience - const TEMPLATE_DEFAULTS: Record< - PayloadTemplateType, - { targetUrl: string; targetServiceUrl: string } - > = { - "Offer Review": { - targetUrl: "http://localhost:3000/api/v0/c/coar-us2-unjournal/site/webhook/coar-inbox", - targetServiceUrl: "http://localhost:3000/c/coar-us2-unjournal", - }, - "Announce Review": { - targetUrl: "http://localhost:3000/api/v0/c/coar-us1-arcadia/site/webhook/coar-inbox", - targetServiceUrl: "http://localhost:3000/c/coar-us1-arcadia", - }, - "Offer Ingest": { - targetUrl: "http://localhost:3000/api/v0/c/coar-us4-repository/site/webhook/coar-inbox", - targetServiceUrl: "http://localhost:3000/c/coar-us4-repository", - }, - "Announce Ingest": { - targetUrl: "http://localhost:3000/api/v0/c/coar-us4-repository/site/webhook/coar-inbox", - targetServiceUrl: "http://localhost:3000/c/coar-us4-repository", - }, - Accept: { - targetUrl: "http://localhost:3000/api/v0/c/coar-us1-arcadia/site/webhook/coar-inbox", - targetServiceUrl: "http://localhost:3000/c/coar-us1-arcadia", - }, - Reject: { - targetUrl: "http://localhost:3000/api/v0/c/coar-us1-arcadia/site/webhook/coar-inbox", - targetServiceUrl: "http://localhost:3000/c/coar-us1-arcadia", - }, - } - const handleTemplateChange = (newType: PayloadTemplateType) => { setTemplateType(newType) + if (!prefill) { - const defaults = TEMPLATE_DEFAULTS[newType] + const defaults = templateDefaults[newType] setTargetUrl(defaults.targetUrl) setTargetServiceUrl(defaults.targetServiceUrl) } @@ -165,13 +173,14 @@ export function SendNotificationForm({ onSent, prefill }: SendNotificationFormPr try { let payload: CoarNotifyPayload + if (mode === "template") { payload = generatePayload() } else { payload = JSON.parse(customPayload) } - const res = await fetch("/api/send", { + const res = await fetch(apiUrl("/api/send"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetUrl, payload }), @@ -278,7 +287,6 @@ export function SendNotificationForm({ onSent, prefill }: SendNotificationFormPr type="text" value={reviewUrl} onChange={(e) => setReviewUrl(e.target.value)} - placeholder="http://localhost:4000/review/..." className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" /> @@ -290,7 +298,6 @@ export function SendNotificationForm({ onSent, prefill }: SendNotificationFormPr type="text" value={inReplyToUrl} onChange={(e) => setInReplyToUrl(e.target.value)} - placeholder="http://localhost:3000/c/community/pub/..." className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" /> @@ -344,7 +351,6 @@ export function SendNotificationForm({ onSent, prefill }: SendNotificationFormPr type="text" value={reviewUrl} onChange={(e) => setReviewUrl(e.target.value)} - placeholder="http://localhost:4001/review/..." className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" /> @@ -386,19 +392,18 @@ export function SendNotificationForm({ onSent, prefill }: SendNotificationFormPr type="text" value={reviewUrl} onChange={(e) => setReviewUrl(e.target.value)} - placeholder="http://localhost:4001/review/..." className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
@@ -533,7 +538,6 @@ export function SendNotificationForm({ onSent, prefill }: SendNotificationFormPr value={targetUrl} onChange={(e) => setTargetUrl(e.target.value)} className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" - placeholder="http://localhost:3000/api/v0/c/..." />
diff --git a/mock-notify/src/app/page.tsx b/mock-notify/src/app/page.tsx index e0fa5fbf99..284ae6baeb 100644 --- a/mock-notify/src/app/page.tsx +++ b/mock-notify/src/app/page.tsx @@ -1,172 +1,17 @@ -"use client" +import type { AppConfig } from "~/lib/urls" -import type { PayloadTemplateType } from "~/lib/payloads" -import type { StoredNotification } from "~/lib/store" +import { HomeClient } from "./components/HomeClient" -import { useCallback, useEffect, useRef, useState } from "react" +const basePath = process.env.BASE_PATH || "" -import { NotificationCard, type ResponsePrefill } from "./components/NotificationCard" -import { SendNotificationForm } from "./components/SendNotificationForm" +export default function Page() { + const pubpubUrl = process.env.PUBPUB_URL || "http://localhost:3000" -export default function Home() { - const [notifications, setNotifications] = useState([]) - const [filter, setFilter] = useState<"all" | "received" | "sent">("all") - const [isLoading, setIsLoading] = useState(true) - const [prefill, setPrefill] = useState(undefined) - const [formKey, setFormKey] = useState(0) - const formRef = useRef(null) + const selfUrl = basePath + ? `${pubpubUrl}${basePath}` + : `http://localhost:${process.env.PORT || "4001"}` - const fetchNotifications = useCallback(async () => { - try { - const params = filter !== "all" ? `?direction=${filter}` : "" - const res = await fetch(`/api/notifications${params}`) - const data = await res.json() - setNotifications(data.notifications) - } catch (error) { - // biome-ignore lint/suspicious/noConsole: shh - console.error("Failed to fetch notifications:", error) - } finally { - setIsLoading(false) - } - }, [filter]) + const config: AppConfig = { pubpubUrl, selfUrl } - useEffect(() => { - void fetchNotifications() - // Poll for new notifications every 2 seconds - const interval = setInterval(fetchNotifications, 2000) - return () => clearInterval(interval) - }, [fetchNotifications]) - - const handleClearAll = async () => { - if (!confirm("Are you sure you want to clear all notifications?")) return - await fetch("/api/notifications", { method: "DELETE" }) - setNotifications([]) - } - - const handleDelete = async (id: string) => { - await fetch(`/api/notifications/${id}`, { method: "DELETE" }) - setNotifications((prev) => prev.filter((n) => n.id !== id)) - } - - const handleRespond = (_responseType: PayloadTemplateType, newPrefill: ResponsePrefill) => { - setPrefill(newPrefill) - // Force form to re-mount with new prefill values - setFormKey((k) => k + 1) - // Scroll to form - formRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }) - } - - const handleSent = () => { - setPrefill(undefined) - void fetchNotifications() - } - - const receivedCount = notifications.filter((n) => n.direction === "received").length - const sentCount = notifications.filter((n) => n.direction === "sent").length - - return ( -
-
-
- {/* Send Notification Form */} -
- - {prefill && ( - - )} -
- - {/* Notifications List */} -
-
- {/* Header with filters */} -
-
-

Notifications

-
- - - -
-
- {notifications.length > 0 && ( - - )} -
- - {/* Notifications */} -
- {isLoading ? ( -
- Loading notifications... -
- ) : notifications.length === 0 ? ( -
- No notifications yet. Send one or wait for incoming - requests. -
- ) : ( - notifications.map((notification, index) => ( - handleDelete(notification.id)} - onRespond={handleRespond} - /> - )) - )} -
-
-
-
-
-
- ) + return } diff --git a/mock-notify/src/app/reviews/sample-review/docmap/route.ts b/mock-notify/src/app/reviews/sample-review/docmap/route.ts index c815b1e2fe..0bebb186ac 100644 --- a/mock-notify/src/app/reviews/sample-review/docmap/route.ts +++ b/mock-notify/src/app/reviews/sample-review/docmap/route.ts @@ -1,5 +1,8 @@ -const MOCK_BASE = "http://localhost:4001" -const REVIEW_URL = `${MOCK_BASE}/reviews/sample-review` +const basePath = process.env.BASE_PATH || "" +const pubpubUrl = process.env.PUBPUB_URL || "http://localhost:4001" + +const selfUrl = basePath ? `${pubpubUrl}${basePath}` : pubpubUrl +const REVIEW_URL = `${selfUrl}/reviews/sample-review` const CORS_HEADERS = { "Access-Control-Allow-Origin": "*", diff --git a/mock-notify/src/app/reviews/sample-review/route.ts b/mock-notify/src/app/reviews/sample-review/route.ts index 196862802b..c1f0111812 100644 --- a/mock-notify/src/app/reviews/sample-review/route.ts +++ b/mock-notify/src/app/reviews/sample-review/route.ts @@ -1,5 +1,8 @@ -const MOCK_BASE = "http://localhost:4001" -const REVIEW_URL = `${MOCK_BASE}/reviews/sample-review` +const basePath = process.env.BASE_PATH || "" +const pubpubUrl = process.env.PUBPUB_URL || "http://localhost:4001" + +const selfUrl = basePath ? `${pubpubUrl}${basePath}` : pubpubUrl +const REVIEW_URL = `${selfUrl}/reviews/sample-review` const CORS_HEADERS = { "Access-Control-Allow-Origin": "*", diff --git a/mock-notify/src/lib/urls.ts b/mock-notify/src/lib/urls.ts new file mode 100644 index 0000000000..0d44fee584 --- /dev/null +++ b/mock-notify/src/lib/urls.ts @@ -0,0 +1,9 @@ +const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "" + +/** prefix a path with the app's basePath for client-side fetch calls */ +export const apiUrl = (path: string) => `${basePath}${path}` + +export interface AppConfig { + pubpubUrl: string + selfUrl: string +} From 2d80b5827ea9e3d57ae441ad2d5388e1333329b5 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 14 Apr 2026 16:08:16 +0200 Subject: [PATCH 78/78] fix: make coar page dynamic --- .../app/components/SendNotificationForm.tsx | 18 ++++++------------ mock-notify/src/app/page.tsx | 2 ++ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/mock-notify/src/app/components/SendNotificationForm.tsx b/mock-notify/src/app/components/SendNotificationForm.tsx index 939190b618..69f357a878 100644 --- a/mock-notify/src/app/components/SendNotificationForm.tsx +++ b/mock-notify/src/app/components/SendNotificationForm.tsx @@ -40,10 +40,9 @@ const TEMPLATE_OPTIONS: PayloadTemplateType[] = [ "Reject", ] -function makeTemplateDefaults(pubpubUrl: string): Record< - PayloadTemplateType, - { targetUrl: string; targetServiceUrl: string } -> { +function makeTemplateDefaults( + pubpubUrl: string +): Record { return { "Offer Review": { targetUrl: `${pubpubUrl}/api/v0/c/coar-us2-unjournal/site/webhook/coar-inbox`, @@ -98,15 +97,11 @@ export function SendNotificationForm({ config, onSent, prefill }: SendNotificati const [objectItemUrl, setObjectItemUrl] = useState("") const [reviewUrl, setReviewUrl] = useState(`${selfUrl}/reviews/sample-review`) const [originUrl, setOriginUrl] = useState(prefill?.originUrl ?? selfUrl) - const [targetServiceUrl, setTargetServiceUrl] = useState( - prefill?.targetServiceUrl ?? pubpubUrl - ) + const [targetServiceUrl, setTargetServiceUrl] = useState(prefill?.targetServiceUrl ?? pubpubUrl) const [serviceName, setServiceName] = useState("Mock Review Service") const [inReplyTo, setInReplyTo] = useState(prefill?.inReplyTo ?? "") const [inReplyToUrl, setInReplyToUrl] = useState(prefill?.inReplyToObjectUrl ?? "") - const [workUrl, setWorkUrl] = useState( - `${pubpubUrl}/c/coar-us4-repository/pub/{pubId}` - ) + const [workUrl, setWorkUrl] = useState(`${pubpubUrl}/c/coar-us4-repository/pub/{pubId}`) const handleTemplateChange = (newType: PayloadTemplateType) => { setTemplateType(newType) @@ -398,8 +393,7 @@ export function SendNotificationForm({ config, onSent, prefill }: SendNotificati