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