diff --git a/.forgejo/workflows/cd-apps.yml b/.forgejo/workflows/cd-apps.yml deleted file mode 100644 index 913b73f..0000000 --- a/.forgejo/workflows/cd-apps.yml +++ /dev/null @@ -1,146 +0,0 @@ -name: CD Apps -# Ported from .github/workflows/cd-apps.yml for Forgejo Actions. -# Differences from the GitHub version: -# - no `environment: production` (Forgejo has no GitHub Environments) -# - image push to ghcr.io authenticates with DEPLOY_GHCR_TOKEN (decrypted -# from SOPS) as user `stigi`, since Forgejo's auto token can't push to -# GHCR the way GitHub's GITHUB_TOKEN could. -on: - push: - branches: [main] - paths: - - "apps/**" - - "packages/**" - - "pnpm-lock.yaml" - workflow_dispatch: {} - -concurrency: - group: deploy-apps - cancel-in-progress: true - -env: - SENTRY_DSN_JOURNAL: "https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728" - SENTRY_DSN_PLANNER: "https://5215134cd78d5e6c199e29300b8425af@o4509530546634752.ingest.de.sentry.io/4511102546608208" - -jobs: - build-images: - name: Build & Push Docker Images - runs-on: ubuntu-latest - strategy: - matrix: - app: [journal, planner] - steps: - - uses: actions/checkout@v7 - - - name: Decrypt secrets + login to GHCR - run: | - curl -sLO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64 - chmod +x sops-v3.9.4.linux.amd64 - SOPS_AGE_KEY="${{ secrets.AGE_SECRET_KEY }}" ./sops-v3.9.4.linux.amd64 -d infrastructure/secrets.app.env > /tmp/secrets.env - grep SENTRY_AUTH_TOKEN /tmp/secrets.env | cut -d= -f2- | tr -d '\n' > /tmp/sentry_token - # GHCR push credential (Forgejo has no GITHUB_TOKEN with packages:write). - grep '^DEPLOY_GHCR_TOKEN=' /tmp/secrets.env | cut -d= -f2- \ - | docker login ghcr.io -u stigi --password-stdin - - - uses: docker/build-push-action@v7 - with: - context: . - file: apps/${{ matrix.app }}/Dockerfile - push: true - tags: | - ghcr.io/trails-cool/${{ matrix.app }}:latest - ghcr.io/trails-cool/${{ matrix.app }}:${{ github.sha }} - build-args: | - SENTRY_RELEASE=${{ github.sha }} - VITE_SENTRY_DSN=${{ matrix.app == 'journal' && env.SENTRY_DSN_JOURNAL || '' }} - secrets: | - SENTRY_AUTH_TOKEN=/tmp/sentry_token - - deploy: - name: Deploy Apps - needs: [build-images] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - name: Decrypt secrets - run: | - curl -sLO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64 - chmod +x sops-v3.9.4.linux.amd64 - SOPS_AGE_KEY="${{ secrets.AGE_SECRET_KEY }}" ./sops-v3.9.4.linux.amd64 -d infrastructure/secrets.app.env > infrastructure/app.env - echo "SENTRY_RELEASE=${{ github.sha }}" >> infrastructure/app.env - echo "DOMAIN=trails.cool" >> infrastructure/app.env - echo "IS_FLAGSHIP=true" >> infrastructure/app.env - echo "FEDERATION_ENABLED=true" >> infrastructure/app.env - echo "FEDERATION_LOG_LEVEL=info" >> infrastructure/app.env - echo "SENTRY_DSN_JOURNAL=$SENTRY_DSN_JOURNAL" >> infrastructure/app.env - echo "SENTRY_DSN_PLANNER=$SENTRY_DSN_PLANNER" >> infrastructure/app.env - - - name: Copy files to server - uses: appleboy/scp-action@v1 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: root - key: ${{ secrets.DEPLOY_SSH_KEY }} - source: "infrastructure/docker-compose.yml,infrastructure/caddy" - target: /opt/trails-cool - strip_components: 1 - - - name: Copy secrets to server - uses: appleboy/scp-action@v1 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: root - key: ${{ secrets.DEPLOY_SSH_KEY }} - source: "infrastructure/app.env" - target: /opt/trails-cool - strip_components: 1 - - - name: Deploy via SSH - uses: appleboy/ssh-action@v1 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: root - key: ${{ secrets.DEPLOY_SSH_KEY }} - script: | - set -euo pipefail - cd /opt/trails-cool - - GHCR_TOKEN=$(grep DEPLOY_GHCR_TOKEN app.env | cut -d= -f2-) - echo "$GHCR_TOKEN" | docker login ghcr.io -u stigi --password-stdin - - docker compose --env-file app.env pull journal planner - docker compose --env-file app.env run --rm journal node --experimental-strip-types /app/packages/db/src/migrate-data.ts - docker compose --env-file app.env run --rm journal npx drizzle-kit push --config /app/packages/db/drizzle.config.ts --force 2>&1 | tee /tmp/drizzle-push.log - if grep -q "Error:" /tmp/drizzle-push.log; then - echo "drizzle-kit push reported an error — failing the deploy" - exit 1 - fi - docker compose --env-file app.env up -d --remove-orphans journal planner - - for svc in journal planner; do - for i in $(seq 1 24); do - status=$(docker inspect -f '{{.State.Health.Status}}' "trails-cool-$svc-1" 2>/dev/null || echo missing) - [ "$status" = "healthy" ] && break - sleep 5 - done - if [ "$status" != "healthy" ]; then - echo "$svc did not become healthy (last status: $status)" - docker compose --env-file app.env logs "$svc" --tail 50 || true - exit 1 - fi - done - - docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile || true - - docker image prune -af || true - docker compose ps - - GRAFANA_TOKEN=$(grep GRAFANA_SERVICE_TOKEN .env 2>/dev/null | cut -d= -f2-) - if [ -n "$GRAFANA_TOKEN" ]; then - docker compose exec -T grafana curl -sf -X POST \ - -H "Authorization: Bearer $GRAFANA_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"text":"Deploy ${{ github.sha }}","tags":["deploy","apps"]}' \ - http://localhost:3000/api/annotations || true - fi diff --git a/.forgejo/workflows/cd-brouter.yml b/.forgejo/workflows/cd-brouter.yml deleted file mode 100644 index 9744231..0000000 --- a/.forgejo/workflows/cd-brouter.yml +++ /dev/null @@ -1,116 +0,0 @@ -name: CD BRouter -# Ported from .github/workflows/cd-brouter.yml for Forgejo Actions. -# Differences: no `environment: infra`; the image push logs in to ghcr.io -# with DEPLOY_GHCR_TOKEN (decrypted) instead of the GitHub auto token. -on: - push: - branches: [main] - paths: - - "docker/brouter/**" - - "infrastructure/brouter-host/**" - workflow_dispatch: {} - -concurrency: - group: deploy-brouter - cancel-in-progress: true - -jobs: - build: - name: Build & Push BRouter Image - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - name: Decrypt secret + login to GHCR - run: | - curl -sLO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64 - chmod +x sops-v3.9.4.linux.amd64 - SOPS_AGE_KEY="${{ secrets.AGE_SECRET_KEY }}" ./sops-v3.9.4.linux.amd64 -d infrastructure/secrets.app.env \ - | grep '^DEPLOY_GHCR_TOKEN=' | cut -d= -f2- \ - | docker login ghcr.io -u stigi --password-stdin - - - uses: docker/build-push-action@v7 - with: - context: docker/brouter - push: true - tags: | - ghcr.io/trails-cool/brouter:latest - ghcr.io/trails-cool/brouter:${{ github.sha }} - - deploy: - name: Deploy BRouter to dedicated host - needs: [build] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - name: Decrypt shared secret - id: decrypt - run: | - curl -sLO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64 - chmod +x sops-v3.9.4.linux.amd64 - SOPS_AGE_KEY="${{ secrets.AGE_SECRET_KEY }}" ./sops-v3.9.4.linux.amd64 -d infrastructure/secrets.app.env \ - | grep '^BROUTER_AUTH_TOKEN=' > infrastructure/brouter-host/.env - chmod 0600 infrastructure/brouter-host/.env - - GHCR_TOKEN=$(SOPS_AGE_KEY="${{ secrets.AGE_SECRET_KEY }}" ./sops-v3.9.4.linux.amd64 -d infrastructure/secrets.app.env \ - | grep '^DEPLOY_GHCR_TOKEN=' | cut -d= -f2-) - echo "::add-mask::$GHCR_TOKEN" - echo "GHCR_TOKEN=$GHCR_TOKEN" >> $GITHUB_ENV - - - name: Deploy to dedicated host (single SSH connection) - env: - SSH_KEY: ${{ secrets.BROUTER_DEPLOY_SSH_KEY }} - HOST: ${{ secrets.BROUTER_DEPLOY_HOST }} - PORT: ${{ secrets.BROUTER_DEPLOY_SSH_PORT }} - run: | - set -euo pipefail - - install -m 0600 /dev/null ~/id_deploy - printf '%s\n' "$SSH_KEY" > ~/id_deploy - - TARBALL_B64=$(tar -C infrastructure/brouter-host -czf - \ - docker-compose.yml Caddyfile promtail-config.yml \ - download-segments.sh .env \ - poi-extract \ - | base64 -w0) - - ssh -i ~/id_deploy -p "$PORT" \ - -o StrictHostKeyChecking=accept-new \ - -o UserKnownHostsFile=/tmp/known_hosts \ - -o ConnectTimeout=30 \ - trails@"$HOST" \ - "TARBALL_B64='$TARBALL_B64' GHCR_TOKEN='$GHCR_TOKEN' bash -s" <<'REMOTE' - set -euo pipefail - mkdir -p ~/brouter && cd ~/brouter - echo "$TARBALL_B64" | base64 -d | tar -xzf - - chmod +x download-segments.sh poi-extract/poi-extract.sh poi-extract/to-ndjson.py - - if [ ! -d segments ] || [ -z "$(ls -A segments 2>/dev/null)" ]; then - echo "WARNING: segments/ is empty. BRouter will start but return 404 until segments are seeded." - mkdir -p segments - fi - - echo "$GHCR_TOKEN" | docker login ghcr.io -u stigi --password-stdin - - docker compose pull - docker compose up -d --remove-orphans - docker compose ps - REMOTE - - - name: Annotate deploy in flagship Grafana - uses: appleboy/ssh-action@v1 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: root - key: ${{ secrets.DEPLOY_SSH_KEY }} - script: | - cd /opt/trails-cool - GRAFANA_TOKEN=$(grep GRAFANA_SERVICE_TOKEN .env | cut -d= -f2-) - if [ -n "$GRAFANA_TOKEN" ]; then - docker compose exec -T grafana curl -sf -X POST \ - -H "Authorization: Bearer $GRAFANA_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"text":"Deploy brouter ${{ github.sha }}","tags":["deploy","brouter"]}' \ - http://localhost:3000/api/annotations || true - fi diff --git a/.forgejo/workflows/cd-infra.yml b/.forgejo/workflows/cd-infra.yml deleted file mode 100644 index 4715a0e..0000000 --- a/.forgejo/workflows/cd-infra.yml +++ /dev/null @@ -1,130 +0,0 @@ -name: CD Infra -# Ported from .github/workflows/cd-infra.yml for Forgejo Actions. -# Only difference: no `environment: infra`. No image build here — GHCR login -# happens on the server using DEPLOY_GHCR_TOKEN from the merged .env. -on: - push: - branches: [main] - paths: - - "infrastructure/**" - workflow_dispatch: - inputs: - restart_all: - description: "Restart all containers (not just infra)" - type: boolean - default: false - -concurrency: - group: deploy-infra - cancel-in-progress: true - -jobs: - deploy: - name: Deploy Infrastructure - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - name: Decrypt secrets - run: | - curl -sLO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64 - chmod +x sops-v3.9.4.linux.amd64 - SOPS_AGE_KEY="${{ secrets.AGE_SECRET_KEY }}" ./sops-v3.9.4.linux.amd64 -d infrastructure/secrets.app.env > infrastructure/.env - SOPS_AGE_KEY="${{ secrets.AGE_SECRET_KEY }}" ./sops-v3.9.4.linux.amd64 -d infrastructure/secrets.infra.env >> infrastructure/.env - echo "DOMAIN=trails.cool" >> infrastructure/.env - echo "IS_FLAGSHIP=true" >> infrastructure/.env - - - name: Copy configs to server - uses: appleboy/scp-action@v1 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: root - key: ${{ secrets.DEPLOY_SSH_KEY }} - source: "infrastructure/docker-compose.yml,infrastructure/caddy,infrastructure/prometheus/prometheus.yml,infrastructure/loki/loki-config.yml,infrastructure/promtail/promtail-config.yml,infrastructure/postgres/queries.yml,infrastructure/postgres/init-grafana-user.sql,infrastructure/grafana/provisioning,infrastructure/grafana/dashboards,infrastructure/scripts" - target: /opt/trails-cool - strip_components: 1 - - - name: Copy secrets to server - uses: appleboy/scp-action@v1 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: root - key: ${{ secrets.DEPLOY_SSH_KEY }} - source: "infrastructure/.env" - target: /opt/trails-cool - strip_components: 1 - - - name: Deploy via SSH - uses: appleboy/ssh-action@v1 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: root - key: ${{ secrets.DEPLOY_SSH_KEY }} - script: | - set -euo pipefail - cd /opt/trails-cool - - GHCR_TOKEN=$(grep DEPLOY_GHCR_TOKEN .env | cut -d= -f2-) - echo "$GHCR_TOKEN" | docker login ghcr.io -u stigi --password-stdin 2>/dev/null || true - - docker compose exec -T postgres psql -U trails -d trails -f /docker-entrypoint-initdb.d/init-grafana-user.sql 2>/dev/null || true - GRAFANA_DB_PW=$(grep GRAFANA_DB_PASSWORD .env | cut -d= -f2-) - if [ -n "$GRAFANA_DB_PW" ]; then - docker compose exec -T postgres psql -U trails -d trails -c "ALTER ROLE grafana_reader PASSWORD '$GRAFANA_DB_PW'" 2>/dev/null || true - fi - - PROMETHEUS_BEFORE_ID=$(docker inspect -f '{{.Id}}' trails-cool-prometheus-1 2>/dev/null || true) - - if [ "${{ github.event.inputs.restart_all }}" = "true" ]; then - docker compose --env-file .env up -d --remove-orphans - else - docker compose --env-file .env up -d --remove-orphans postgres prometheus loki promtail grafana postgres-exporter node-exporter cadvisor - fi - - PROMETHEUS_AFTER_ID=$(docker inspect -f '{{.Id}}' trails-cool-prometheus-1 2>/dev/null || true) - - if [ -n "$PROMETHEUS_BEFORE_ID" ] && [ "$PROMETHEUS_BEFORE_ID" = "$PROMETHEUS_AFTER_ID" ]; then - docker compose --env-file .env kill -s SIGHUP prometheus - fi - docker compose --env-file .env restart loki promtail - docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile - - docker compose ps - - for ctr in trails-cool-postgres-1 trails-cool-journal-1; do - for i in $(seq 1 36); do - status=$(docker inspect -f '{{.State.Health.Status}}' "$ctr" 2>/dev/null || echo missing) - [ "$status" = "healthy" ] && break - sleep 5 - done - if [ "$status" != "healthy" ]; then - echo "$ctr did not become healthy (last status: $status)" - exit 1 - fi - done - - PROMETHEUS_READY= - for i in $(seq 1 36); do - prom_status=$(docker inspect -f '{{.State.Status}}' trails-cool-prometheus-1 2>/dev/null || echo missing) - if [ "$prom_status" = "running" ]; then - prom_ip=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' trails-cool-prometheus-1) - if curl -sf "http://$prom_ip:9090/-/ready" >/dev/null; then - PROMETHEUS_READY=1 - break - fi - fi - sleep 5 - done - if [ -z "$PROMETHEUS_READY" ]; then - echo "trails-cool-prometheus-1 did not become ready (last status: $prom_status)" - exit 1 - fi - - GRAFANA_TOKEN=$(grep GRAFANA_SERVICE_TOKEN .env | cut -d= -f2-) - if [ -n "$GRAFANA_TOKEN" ]; then - docker compose exec -T grafana curl -sf -X POST \ - -H "Authorization: Bearer $GRAFANA_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"text":"Deploy infra ${{ github.sha }}","tags":["deploy","infra"]}' \ - http://localhost:3000/api/annotations || true - fi diff --git a/.forgejo/workflows/cd-staging.yml b/.forgejo/workflows/cd-staging.yml deleted file mode 100644 index 7ce88da..0000000 --- a/.forgejo/workflows/cd-staging.yml +++ /dev/null @@ -1,428 +0,0 @@ -name: CD Staging -# Ported from .github/workflows/cd-staging.yml for Forgejo Actions. -# -# Adaptations applied: -# - no `environment: production` -# - image push logs in to ghcr.io with DEPLOY_GHCR_TOKEN (decrypted) -# -# NEEDS VALIDATION on a real Forgejo PR before relying on it — these depend on -# GitHub-Actions semantics Forgejo may implement differently: -# - pull_request action types (labeled/unlabeled/synchronize) and -# `github.event.pull_request.labels.*.name` filtering -# - the peter-evans find-comment / create-or-update-comment actions, which -# talk to the API as octokit; the bot author is `forgejo-actions[bot]` -# (not `github-actions[bot]`), so the comment-author match likely needs -# updating once confirmed. -on: - push: - branches: [main] - paths: - - "apps/**" - - "packages/**" - - "pnpm-lock.yaml" - - "infrastructure/docker-compose.staging.yml" - - ".forgejo/workflows/cd-staging.yml" - pull_request: - types: [opened, synchronize, reopened, closed, labeled, unlabeled] - paths: - - "apps/**" - - "packages/**" - - "pnpm-lock.yaml" - workflow_dispatch: {} - -concurrency: - group: staging-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || 'main' }} - cancel-in-progress: true - -env: - SENTRY_DSN_JOURNAL: "https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728" - SENTRY_DSN_PLANNER: "https://5215134cd78d5e6c199e29300b8425af@o4509530546634752.ingest.de.sentry.io/4511102546608208" - -jobs: - build-images: - name: Build & Push Docker Images - if: > - github.event_name != 'pull_request' || - (github.event.action != 'closed' && - (contains(github.event.pull_request.labels.*.name, 'preview') || - contains(github.event.pull_request.body, ''))) - runs-on: ubuntu-latest - strategy: - matrix: - app: [journal, planner] - outputs: - tag_primary: ${{ steps.tags.outputs.primary }} - tag_sha: ${{ steps.tags.outputs.sha }} - steps: - - uses: actions/checkout@v7 - - - id: tags - name: Compute image tags - run: | - if [ "${{ github.event_name }}" = "pull_request" ]; then - PRIMARY="pr-${{ github.event.number }}" - SHA="pr-${{ github.event.number }}-${{ github.event.pull_request.head.sha }}" - else - PRIMARY="staging" - SHA="${{ github.sha }}" - fi - echo "primary=$PRIMARY" >> "$GITHUB_OUTPUT" - echo "sha=$SHA" >> "$GITHUB_OUTPUT" - - - name: Decrypt secrets + login to GHCR - run: | - curl -sLO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64 - chmod +x sops-v3.9.4.linux.amd64 - SOPS_AGE_KEY="${{ secrets.AGE_SECRET_KEY }}" ./sops-v3.9.4.linux.amd64 -d infrastructure/secrets.app.env > /tmp/secrets.env - grep SENTRY_AUTH_TOKEN /tmp/secrets.env | cut -d= -f2- | tr -d '\n' > /tmp/sentry_token - grep '^DEPLOY_GHCR_TOKEN=' /tmp/secrets.env | cut -d= -f2- \ - | docker login ghcr.io -u stigi --password-stdin - - - uses: docker/build-push-action@v7 - with: - context: . - file: apps/${{ matrix.app }}/Dockerfile - push: true - tags: | - ghcr.io/trails-cool/${{ matrix.app }}:${{ steps.tags.outputs.primary }} - ghcr.io/trails-cool/${{ matrix.app }}:${{ steps.tags.outputs.sha }} - build-args: | - SENTRY_RELEASE=${{ steps.tags.outputs.sha }} - VITE_SENTRY_DSN=${{ matrix.app == 'journal' && env.SENTRY_DSN_JOURNAL || '' }} - secrets: | - SENTRY_AUTH_TOKEN=/tmp/sentry_token - - deploy-staging: - name: Deploy Staging - if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' - needs: [build-images] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - name: Decrypt secrets - run: | - curl -sLO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64 - chmod +x sops-v3.9.4.linux.amd64 - SOPS_AGE_KEY="${{ secrets.AGE_SECRET_KEY }}" ./sops-v3.9.4.linux.amd64 -d infrastructure/secrets.app.env > infrastructure/staging.env - { - echo "DOMAIN=staging.trails.cool" - echo "STAGING_DATABASE=trails_staging" - echo "JOURNAL_HOST_PORT=3110" - echo "PLANNER_HOST_PORT=3111" - echo "JOURNAL_IMAGE_TAG=staging" - echo "PLANNER_IMAGE_TAG=staging" - echo "SENTRY_RELEASE=${{ github.sha }}" - echo "SENTRY_DSN_JOURNAL=$SENTRY_DSN_JOURNAL" - echo "SENTRY_DSN_PLANNER=$SENTRY_DSN_PLANNER" - echo "FEDERATION_ENABLED=true" - echo "FEDERATION_LOG_LEVEL=debug" - } >> infrastructure/staging.env - - - name: Copy compose file + env to server - uses: appleboy/scp-action@v1 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: root - key: ${{ secrets.DEPLOY_SSH_KEY }} - source: "infrastructure/docker-compose.staging.yml,infrastructure/staging.env" - target: /opt/trails-cool - strip_components: 1 - - - name: Deploy via SSH - uses: appleboy/ssh-action@v1 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: root - key: ${{ secrets.DEPLOY_SSH_KEY }} - script: | - set -euo pipefail - cd /opt/trails-cool - - GHCR_TOKEN=$(grep DEPLOY_GHCR_TOKEN staging.env | cut -d= -f2-) - echo "$GHCR_TOKEN" | docker login ghcr.io -u stigi --password-stdin - - docker network inspect trails-shared >/dev/null 2>&1 || docker network create trails-shared - PG_CONTAINER=$(docker ps --filter "name=trails-cool-postgres" --format '{{.Names}}' | head -1) - if [ -n "$PG_CONTAINER" ]; then - docker network connect trails-shared "$PG_CONTAINER" 2>/dev/null || true - fi - - docker compose exec -T postgres psql -U trails -d postgres -tAc \ - "SELECT 1 FROM pg_database WHERE datname='trails_staging'" \ - | grep -q 1 \ - || docker compose exec -T postgres createdb -U trails trails_staging - docker compose exec -T postgres psql -U trails -d trails_staging -c \ - "CREATE EXTENSION IF NOT EXISTS postgis" - - docker compose -f docker-compose.staging.yml -p trails-staging --env-file staging.env --profile persistent pull - docker compose -f docker-compose.staging.yml -p trails-staging --env-file staging.env --profile persistent run --rm journal npx drizzle-kit push --config /app/packages/db/drizzle.config.ts --force 2>&1 | tee /tmp/drizzle-push.log - if grep -q "Error:" /tmp/drizzle-push.log; then - echo "drizzle-kit push reported an error — failing the deploy" - exit 1 - fi - docker compose -f docker-compose.staging.yml -p trails-staging --env-file staging.env --profile persistent up -d --remove-orphans - - docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile || true - - docker image prune -af --filter "until=1h" || true - - docker compose -f docker-compose.staging.yml -p trails-staging --env-file staging.env --profile persistent ps - - deploy-preview: - name: Deploy PR Preview - if: > - github.event_name == 'pull_request' && - github.event.action != 'closed' && - !startsWith(github.head_ref, 'dependabot/github_actions/') && - (contains(github.event.pull_request.labels.*.name, 'preview') || - contains(github.event.pull_request.body, '')) - needs: [build-images] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - id: ports - name: Compute preview ports + project name - run: | - PR=${{ github.event.number }} - JOURNAL_PORT=$((3200 + 2 * PR)) - PLANNER_PORT=$((3201 + 2 * PR)) - echo "pr=$PR" >> "$GITHUB_OUTPUT" - echo "journal_port=$JOURNAL_PORT" >> "$GITHUB_OUTPUT" - echo "planner_port=$PLANNER_PORT" >> "$GITHUB_OUTPUT" - echo "host=pr-$PR.staging.trails.cool" >> "$GITHUB_OUTPUT" - echo "project=trails-pr-$PR" >> "$GITHUB_OUTPUT" - echo "database=trails_pr_$PR" >> "$GITHUB_OUTPUT" - - - name: Decrypt secrets + assemble env - run: | - curl -sLO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64 - chmod +x sops-v3.9.4.linux.amd64 - SOPS_AGE_KEY="${{ secrets.AGE_SECRET_KEY }}" ./sops-v3.9.4.linux.amd64 -d infrastructure/secrets.app.env > infrastructure/staging-pr-${{ steps.ports.outputs.pr }}.env - { - echo "DOMAIN=${{ steps.ports.outputs.host }}" - echo "STAGING_DATABASE=${{ steps.ports.outputs.database }}" - echo "JOURNAL_HOST_PORT=${{ steps.ports.outputs.journal_port }}" - echo "PLANNER_HOST_PORT=${{ steps.ports.outputs.planner_port }}" - echo "JOURNAL_IMAGE_TAG=pr-${{ steps.ports.outputs.pr }}" - echo "PLANNER_IMAGE_TAG=pr-${{ steps.ports.outputs.pr }}" - echo "PLANNER_URL=https://planner.staging.trails.cool" - echo "SENTRY_RELEASE=${{ github.event.pull_request.head.sha }}" - echo "SENTRY_DSN_JOURNAL=$SENTRY_DSN_JOURNAL" - echo "SENTRY_DSN_PLANNER=$SENTRY_DSN_PLANNER" - echo "FEDERATION_ENABLED=true" - echo "FEDERATION_LOG_LEVEL=debug" - } >> infrastructure/staging-pr-${{ steps.ports.outputs.pr }}.env - - - name: Generate per-PR Caddyfile snippet - run: | - mkdir -p infrastructure/sites - cat > infrastructure/sites/pr-${{ steps.ports.outputs.pr }}.caddyfile </tmp/trails-preview-deploy.lock - flock --timeout 300 9 || { echo "Timed out waiting for deploy lock after 300s"; exit 1; } - - docker network inspect trails-shared >/dev/null 2>&1 || docker network create trails-shared - PG_CONTAINER=$(docker ps --filter "name=trails-cool-postgres" --format '{{.Names}}' | head -1) - if [ -n "$PG_CONTAINER" ]; then - docker network connect trails-shared "$PG_CONTAINER" 2>/dev/null || true - fi - - ACTIVE=$(docker compose ls --format json --filter "name=trails-pr-" | python3 -c 'import json,sys; data=json.load(sys.stdin); print("\n".join(d["Name"] for d in data))' 2>/dev/null || true) - if [ -n "$ACTIVE" ] && ! echo "$ACTIVE" | grep -qx "$PROJECT"; then - COUNT=$(echo "$ACTIVE" | grep -c '^trails-pr-' || true) - if [ "$COUNT" -ge 3 ]; then - OLDEST=$(docker ps -a --filter "name=trails-pr-" --format '{{.Names}} {{.CreatedAt}}' \ - | awk '{ split($1,a,"-"); print "trails-pr-"a[3], $2" "$3" "$4 }' \ - | sort -k2 \ - | head -1 \ - | awk '{print $1}') - if [ -n "$OLDEST" ] && [ "$OLDEST" != "$PROJECT" ]; then - echo "At cap; evicting oldest preview: $OLDEST" - OLD_PR=${OLDEST#trails-pr-} - OLD_ENV="staging-pr-${OLD_PR}.env" - EVICT_ENV=$( [ -f "$OLD_ENV" ] && echo "$OLD_ENV" || echo "staging.env" ) - docker compose -f docker-compose.staging.yml -p "$OLDEST" --env-file "$EVICT_ENV" down --remove-orphans || true - docker compose exec -T postgres dropdb -U trails --if-exists "trails_pr_$OLD_PR" || true - rm -f "sites/pr-$OLD_PR.caddyfile" "$OLD_ENV" - fi - fi - fi - - docker compose exec -T postgres psql -U trails -d postgres -tAc \ - "SELECT 1 FROM pg_database WHERE datname='$DB'" \ - | grep -q 1 \ - || docker compose exec -T postgres createdb -U trails "$DB" - docker compose exec -T postgres psql -U trails -d "$DB" -c \ - "CREATE EXTENSION IF NOT EXISTS postgis" - - docker compose -f docker-compose.staging.yml -p "$PROJECT" --env-file "$ENV_FILE" pull journal - docker compose -f docker-compose.staging.yml -p "$PROJECT" --env-file "$ENV_FILE" run --rm journal npx drizzle-kit push --config /app/packages/db/drizzle.config.ts --force 2>&1 | tee /tmp/drizzle-push.log - if grep -q "Error:" /tmp/drizzle-push.log; then - echo "drizzle-kit push reported an error — failing the preview deploy" - exit 1 - fi - docker compose -f docker-compose.staging.yml -p "$PROJECT" --env-file "$ENV_FILE" up -d --remove-orphans journal - - docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile - - docker image prune -af --filter "until=1h" || true - - docker compose -f docker-compose.staging.yml -p "$PROJECT" --env-file "$ENV_FILE" ps - - - name: Find existing preview comment - uses: peter-evans/find-comment@v4 - id: find-comment - with: - issue-number: ${{ github.event.number }} - comment-author: "forgejo-actions[bot]" - body-includes: "" - - - name: Upsert preview comment on PR - uses: peter-evans/create-or-update-comment@v5 - with: - comment-id: ${{ steps.find-comment.outputs.comment-id }} - issue-number: ${{ github.event.number }} - edit-mode: replace - body: | - 🚀 **PR preview deployed** - - - **Journal:** https://${{ steps.ports.outputs.host }} - - **Planner (shared staging):** https://planner.staging.trails.cool - - **Database:** `${{ steps.ports.outputs.database }}` (separate from production / persistent staging) - - **Build:** [run ${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) · commit `${{ github.event.pull_request.head.sha }}` - - Updates automatically on push. Tears down when this PR closes. - - - teardown-preview: - name: Tear Down PR Preview - if: > - github.event_name == 'pull_request' && - (github.event.action == 'closed' || - (github.event.action == 'unlabeled' && - !(contains(github.event.pull_request.labels.*.name, 'preview') || - contains(github.event.pull_request.body, '')))) - runs-on: ubuntu-latest - steps: - - id: ports - name: Compute project + database name - run: | - PR=${{ github.event.number }} - echo "pr=$PR" >> "$GITHUB_OUTPUT" - echo "project=trails-pr-$PR" >> "$GITHUB_OUTPUT" - echo "database=trails_pr_$PR" >> "$GITHUB_OUTPUT" - - - uses: actions/checkout@v7 - - - name: Decrypt secrets (needed to satisfy compose env vars during down) - run: | - curl -sLO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64 - chmod +x sops-v3.9.4.linux.amd64 - SOPS_AGE_KEY="${{ secrets.AGE_SECRET_KEY }}" ./sops-v3.9.4.linux.amd64 -d infrastructure/secrets.app.env > infrastructure/staging-pr-${{ steps.ports.outputs.pr }}.env - { - echo "DOMAIN=pr-${{ steps.ports.outputs.pr }}.staging.trails.cool" - echo "STAGING_DATABASE=${{ steps.ports.outputs.database }}" - echo "JOURNAL_HOST_PORT=$((3200 + 2 * ${{ steps.ports.outputs.pr }}))" - echo "PLANNER_HOST_PORT=$((3201 + 2 * ${{ steps.ports.outputs.pr }}))" - } >> infrastructure/staging-pr-${{ steps.ports.outputs.pr }}.env - - - name: Copy compose + env (teardown still needs the file) - uses: appleboy/scp-action@v1 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: root - key: ${{ secrets.DEPLOY_SSH_KEY }} - source: "infrastructure/docker-compose.staging.yml,infrastructure/staging-pr-${{ steps.ports.outputs.pr }}.env" - target: /opt/trails-cool - strip_components: 1 - - - name: Tear down via SSH - uses: appleboy/ssh-action@v1 - env: - PR: ${{ steps.ports.outputs.pr }} - PROJECT: ${{ steps.ports.outputs.project }} - DB: ${{ steps.ports.outputs.database }} - with: - host: ${{ secrets.DEPLOY_HOST }} - username: root - key: ${{ secrets.DEPLOY_SSH_KEY }} - envs: PR,PROJECT,DB - script: | - set -euo pipefail - cd /opt/trails-cool - ENV_FILE="staging-pr-${PR}.env" - - docker compose -f docker-compose.staging.yml -p "$PROJECT" --env-file "$ENV_FILE" down --remove-orphans || true - - docker compose exec -T postgres dropdb -U trails --if-exists "$DB" || true - - rm -f "sites/pr-$PR.caddyfile" "$ENV_FILE" - docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile || true - - - name: Find existing preview comment - uses: peter-evans/find-comment@v4 - id: find-comment - with: - issue-number: ${{ github.event.number }} - comment-author: "forgejo-actions[bot]" - body-includes: "" - - - name: Update preview comment on close - if: steps.find-comment.outputs.comment-id - uses: peter-evans/create-or-update-comment@v5 - with: - comment-id: ${{ steps.find-comment.outputs.comment-id }} - edit-mode: replace - body: | - 🧹 **PR preview torn down**. - - Database `trails_pr_${{ steps.ports.outputs.pr }}` dropped, containers removed, Caddyfile snippet cleared. - - [Teardown run ${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) - diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml deleted file mode 100644 index 8a8c1b6..0000000 --- a/.forgejo/workflows/ci.yml +++ /dev/null @@ -1,406 +0,0 @@ -name: CI -# Ported from .github/workflows/ci.yml for Forgejo Actions. -# Differences from the GitHub version: -# - no `merge_group` trigger (Forgejo has no merge queue) -# - gitleaks action referenced by full GitHub URL (custom action, not on -# the Forgejo actions mirror) -# - the visual-diff PR comment posts via the Forgejo API instead of `gh` -# The `ci/forgejo-actions` push branch is temporary (lets this branch re-run -# CI pre-merge); drop it once merged to main. -on: - push: - branches: [main, ci/forgejo-actions] - pull_request: - branches: [main] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - security: - name: Security Scan - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - name: Gitleaks - if: github.actor != 'dependabot[bot]' - uses: https://github.com/trails-cool/gitleaks-action@4cbc857b9cfa2a3297fe2be1078e196d30d1b424 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} - # The action's SARIF artifact upload uses @actions/artifact v2, which - # throws GHESNotSupportedError against Forgejo. The scan itself works; - # disable the upload so a clean scan doesn't fail the job. - GITLEAKS_ENABLE_UPLOAD_ARTIFACT: "false" - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Dependency audit - run: pnpm audit --audit-level=high - continue-on-error: true - - dockerfile-check: - name: Dockerfile Package Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - run: bash scripts/check-dockerfiles.sh - - openspec: - name: OpenSpec Validate - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm openspec validate --all --strict --no-interactive - - typecheck: - name: Typecheck - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm typecheck - - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm lint - - test: - name: Unit Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm test - - build: - name: Build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm build - - visual-tests: - name: Visual Tests - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile - - - name: Cache Playwright browsers - id: playwright-cache - uses: actions/cache@v6 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ hashFiles('pnpm-lock.yaml') }} - - - name: Install Playwright Chromium - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: pnpm exec playwright install --with-deps chromium - - - name: Install Playwright deps only - if: steps.playwright-cache.outputs.cache-hit == 'true' - run: pnpm exec playwright install-deps chromium - - - name: Run visual regression tests - id: visual-tests - run: pnpm --filter @trails-cool/planner test:visual - - - name: Post diff comment on PR - if: failure() && steps.visual-tests.outcome == 'failure' && github.event_name == 'pull_request' - env: - TOKEN: ${{ github.token }} - run: | - diffs=$(find apps/planner/.vitest-attachments -name "*-diff-*.png" 2>/dev/null | sort) - if [ -z "$diffs" ]; then exit 0; fi - - artifact_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - body="## Visual regression failures"$'\n\n' - body+="The following tests produced screenshot diffs:"$'\n\n' - for diff in $diffs; do - name=$(basename "$diff" | sed 's/-diff-chromium-[a-z]*\.png//' | sed 's/-/ /g') - body+="- \`$name\`"$'\n' - done - body+=$'\n'"**[Download the \`visual-snapshots-diff\` artifact]($artifact_url)** to inspect the diffs locally."$'\n\n' - body+="To update snapshots if the change is intentional:"$'\n' - body+="\`\`\`"$'\n' - body+="pnpm --filter @trails-cool/planner test:visual:update"$'\n' - body+="\`\`\`" - - # Forgejo API (GitHub-compatible issues/comments endpoint). - curl -sf -X POST \ - -H "Authorization: token $TOKEN" \ - -H "Content-Type: application/json" \ - "${{ github.api_url }}/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \ - -d "$(jq -n --arg b "$body" '{body:$b}')" || true - - - name: Upload screenshots on failure - if: failure() - uses: https://code.forgejo.org/forgejo/upload-artifact@v4 - with: - name: visual-snapshots-diff - path: apps/planner/.vitest-attachments/ - include-hidden-files: true - retention-days: 7 - - e2e: - name: E2E Tests - needs: build - runs-on: ubuntu-latest - env: - # Services run on the DinD daemon; from the job container their published - # ports are reachable at host.docker.internal (not localhost). - DATABASE_URL: postgres://trails:trails@host.docker.internal:5432/trails - DOCKER_HOST: tcp://host.docker.internal:2375 - steps: - - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile - - - name: Clean this runner's DinD (fresh state per job) - # The per-slot DinD persists across jobs, so wipe leftover containers + - # volumes from a previous run (a stale pgdata leaves postgres unhealthy). - # Safe: this DinD only ever runs CI containers for this one runner. - run: | - docker rm -f $(docker ps -aq) 2>/dev/null || true - docker volume rm -f trails_brouter_segments trails_pgdata 2>/dev/null || true - - - name: Seed BRouter segment volume - # Download the segment directly into the compose volume via a DinD - # container — a job-side download + bind mount would not be visible to - # the DinD daemon (separate filesystem). Project name pinned to `trails` - # so the volume matches docker-compose.dev.yml's `brouter_segments`. - # Verify the download is non-empty (brouter.de can be flaky) so a bad - # segment fails here rather than as a mysterious routing timeout later. - run: | - docker volume create trails_brouter_segments - docker run --rm -v trails_brouter_segments:/dst alpine sh -c ' - set -e - for a in 1 2 3; do - wget -q https://brouter.de/brouter/segments4/E10_N50.rd5 -O /dst/E10_N50.rd5 && break - echo "segment download attempt $a failed; retrying"; sleep 5 - done - test -s /dst/E10_N50.rd5 - chmod a+r /dst/*.rd5 - ls -la /dst' - - - name: Start services - run: docker compose -p trails -f docker-compose.dev.yml up -d --wait --build postgres brouter - env: - BROUTER_URL: http://host.docker.internal:17777 - - - name: Wait for BRouter routing - run: | - for i in $(seq 1 120); do - curl -s 'http://host.docker.internal:17777/brouter?lonlats=13.4,52.5|13.5,52.5&profile=trekking&format=geojson' 2>/dev/null | grep -q "FeatureCollection" && echo "BRouter ready" && break - [ "$i" = "120" ] && echo "BRouter not ready after 240s" && exit 1 - sleep 2 - done - - - name: Ensure PostGIS extension - # The postgis image does NOT auto-create the extension in POSTGRES_DB, - # so drizzle-kit push's geometry columns fail (silently, behind its - # spinner). Create it first (trails is superuser in the image). - run: node -e "const{Client}=require('pg');const c=new Client(process.env.DATABASE_URL);c.connect().then(()=>c.query('CREATE EXTENSION IF NOT EXISTS postgis')).then(()=>console.log('postgis ready')).catch(e=>{console.error(e.message);process.exit(1)}).finally(()=>c.end())" - - - name: Push database schema - run: pnpm db:push 2>&1 | cat - - - name: Seed database - run: pnpm db:seed - - - name: Run integration tests - run: pnpm --filter @trails-cool/journal exec vitest run --no-file-parallelism --reporter=default app/lib/explore.integration.test.ts app/lib/follow.integration.test.ts app/lib/demo-bot.integration.test.ts app/lib/notifications.integration.test.ts app/jobs/notifications-fanout.integration.test.ts - env: - EXPLORE_INTEGRATION: "1" - FOLLOW_INTEGRATION: "1" - DEMO_BOT_INTEGRATION: "1" - NOTIFICATIONS_INTEGRATION: "1" - - - name: Cache Playwright browsers - id: playwright-cache - uses: actions/cache@v6 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ hashFiles('pnpm-lock.yaml') }} - - - name: Install Playwright - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: pnpm exec playwright install --with-deps chromium - - - name: Install Playwright deps only - if: steps.playwright-cache.outputs.cache-hit == 'true' - run: pnpm exec playwright install-deps chromium - - - name: Build for production - run: pnpm build - env: - VITE_SENTRY_ENVIRONMENT: ci - - - name: Run E2E tests - run: pnpm test:e2e - env: - BROUTER_URL: http://host.docker.internal:17777 - E2E: "true" - INTEGRATION_SECRET: ${{ secrets.INTEGRATION_SECRET }} - - - name: Playwright job summary - if: ${{ !cancelled() }} - run: | - if [ -f playwright-results.json ]; then - node -e " - const r = require('./playwright-results.json'); - const s = r.stats; - const dur = (s.duration / 1000).toFixed(1); - let md = '## Playwright E2E Results\n\n'; - md += '| Status | Count |\n|--------|-------|\n'; - md += '| :white_check_mark: Passed | ' + s.expected + ' |\n'; - if (s.unexpected > 0) md += '| :x: Failed | ' + s.unexpected + ' |\n'; - if (s.flaky > 0) md += '| :warning: Flaky | ' + s.flaky + ' |\n'; - if (s.skipped > 0) md += '| :fast_forward: Skipped | ' + s.skipped + ' |\n'; - md += '| :stopwatch: Duration | ' + dur + 's |\n\n'; - for (const file of r.suites) { - for (const describe of (file.suites || [])) { - md += '### ' + describe.title + '\n\n'; - for (const spec of (describe.specs || [])) { - const icon = spec.ok ? ':white_check_mark:' : ':x:'; - const t = spec.tests?.[0]?.results?.[0]?.duration; - md += '- ' + icon + ' ' + spec.title + (t ? ' (' + t + 'ms)' : '') + '\n'; - } - md += '\n'; - } - } - require('fs').appendFileSync(process.env.GITHUB_STEP_SUMMARY, md); - " - fi - - - uses: actions/upload-artifact@v7 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 - - journal-image-smoke: - name: Journal Image Smoke Test - runs-on: ubuntu-latest - services: - postgres: - image: imresamu/postgis:16-3.4 - env: - POSTGRES_USER: trails - POSTGRES_PASSWORD: trails - POSTGRES_DB: trails - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U trails" - --health-interval 5s - --health-timeout 5s - --health-retries 20 - env: - # From the job container the postgres service is at host.docker.internal. - DATABASE_URL: postgres://trails:trails@host.docker.internal:5432/trails - DOCKER_HOST: tcp://host.docker.internal:2375 - steps: - - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile - - - name: Ensure PostGIS extension - # The postgis image does NOT auto-create the extension in POSTGRES_DB, - # so drizzle-kit push's geometry columns fail (silently, behind its - # spinner). Create it first (trails is superuser in the image). - run: node -e "const{Client}=require('pg');const c=new Client(process.env.DATABASE_URL);c.connect().then(()=>c.query('CREATE EXTENSION IF NOT EXISTS postgis')).then(()=>console.log('postgis ready')).catch(e=>{console.error(e.message);process.exit(1)}).finally(()=>c.end())" - - - name: Push database schema - run: pnpm db:push 2>&1 | cat - - - name: Build journal runtime image - run: docker build --target runtime -f apps/journal/Dockerfile -t journal-smoke . - - - name: Boot image and wait for healthy - run: | - # Journal runs --network host on the DinD daemon, so it reaches the - # postgres service (published on DinD) at localhost:5432. - docker run -d --name journal-smoke --network host \ - -e NODE_ENV=production -e E2E=true \ - -e DATABASE_URL="postgres://trails:trails@localhost:5432/trails" \ - journal-smoke - code=000 - for i in $(seq 1 30); do - code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 http://host.docker.internal:3000/api/health || echo 000) - echo "attempt $i: /api/health -> $code" - [ "$code" = "200" ] && break - if [ "$(docker inspect -f '{{.State.Running}}' journal-smoke 2>/dev/null)" != "true" ]; then - echo "::error::journal container exited during boot" - break - fi - sleep 2 - done - if [ "$code" != "200" ]; then - echo "::error::journal production image failed to boot healthy (see logs below)" - docker logs journal-smoke 2>&1 || true - exit 1 - fi - echo "journal production image booted healthy" - - - name: Container logs - if: always() - run: docker logs journal-smoke 2>&1 | tail -40 || true diff --git a/apps/journal/package.json b/apps/journal/package.json index 30dbe0a..659169b 100644 --- a/apps/journal/package.json +++ b/apps/journal/package.json @@ -9,7 +9,7 @@ "start": "node --experimental-strip-types server.ts", "typecheck": "react-router typegen && tsc", "lint": "eslint .", - "test": "vitest run --testTimeout=20000" + "test": "vitest run" }, "dependencies": { "@fedify/fedify": "2.3.1", diff --git a/packages/db/package.json b/packages/db/package.json index 5251013..117c843 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -10,7 +10,7 @@ "main": "./src/index.ts", "types": "./src/index.ts", "scripts": { - "test": "vitest run --testTimeout=20000", + "test": "vitest run", "lint": "eslint .", "typecheck": "tsc" }, diff --git a/packages/gpx/package.json b/packages/gpx/package.json index 6ce1d39..92a67c2 100644 --- a/packages/gpx/package.json +++ b/packages/gpx/package.json @@ -8,7 +8,7 @@ "main": "./src/index.ts", "types": "./src/index.ts", "scripts": { - "test": "vitest run --testTimeout=20000", + "test": "vitest run", "lint": "eslint .", "typecheck": "tsc" },