Self-hosted instances no longer inherit the trails.cool flagship Sentry DSNs. Code paths now read DSN strictly from env — unset = no Sentry init, no events sent. Flagship continues to report unchanged: cd-apps.yml and cd-staging.yml inject the public DSNs as workflow env vars, which feed into: - runtime via \`infrastructure/app.env\` / \`staging.env\` (\`SENTRY_DSN_JOURNAL\` / \`SENTRY_DSN_PLANNER\` → compose env per service) - build time via docker build-arg \`VITE_SENTRY_DSN\` (journal only; planner has no client Sentry init). Sentry DSNs are public-by-design (transmitted unencrypted from the client JS bundle), so embedding them as plaintext workflow env vars is no worse than the runtime exposure. Forks should replace these with their own DSNs or remove the workflow env lines to ship Sentry-free. Files changed: - apps/journal/server.ts: \`process.env.SENTRY_DSN\` only, no fallback - apps/planner/server.ts: same - apps/journal/app/lib/sentry.client.ts: \`import.meta.env.VITE_SENTRY_DSN\` only; falsy = skip init - apps/journal/Dockerfile: new \`ARG VITE_SENTRY_DSN\` baked into build - infrastructure/docker-compose.yml: pass \`SENTRY_DSN\` per service - infrastructure/docker-compose.staging.yml: same - .github/workflows/cd-apps.yml: workflow env + build-arg + app.env echo - .github/workflows/cd-staging.yml: same Full repo: pnpm typecheck, pnpm lint, pnpm test, journal build all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
166 lines
7.1 KiB
YAML
166 lines
7.1 KiB
YAML
name: CD Apps
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- "apps/**"
|
|
- "packages/**"
|
|
- "pnpm-lock.yaml"
|
|
workflow_dispatch: {}
|
|
|
|
concurrency:
|
|
group: deploy-apps
|
|
cancel-in-progress: true
|
|
|
|
# Public Sentry DSNs for the trails.cool flagship instance. Public by
|
|
# design — Sentry DSNs are transmitted unencrypted from the client JS
|
|
# bundle, embedding them in this workflow is no worse than embedding
|
|
# them in the runtime env. Self-hosted forks should either replace
|
|
# these with their own DSNs or remove the lines to ship without Sentry.
|
|
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
|
|
environment: production
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
strategy:
|
|
matrix:
|
|
app: [journal, planner]
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- uses: docker/login-action@v4
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Decrypt Sentry auth token
|
|
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
|
|
|
|
- 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 }}
|
|
# VITE_SENTRY_DSN bakes the client-side DSN into the journal's
|
|
# built bundle. Only journal has a client Sentry init; planner
|
|
# ignores the build-arg if present.
|
|
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
|
|
environment: production
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- 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
|
|
# Flagship marker — see cd-infra.yml for what this gates.
|
|
echo "IS_FLAGSHIP=true" >> infrastructure/app.env
|
|
# Sentry DSNs (public — see workflow top-level env for context).
|
|
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/Caddyfile"
|
|
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: |
|
|
cd /opt/trails-cool
|
|
|
|
# Login to ghcr.io
|
|
GHCR_TOKEN=$(grep DEPLOY_GHCR_TOKEN app.env | cut -d= -f2-)
|
|
echo "$GHCR_TOKEN" | docker login ghcr.io -u stigi --password-stdin
|
|
|
|
# Pull and deploy app containers
|
|
docker compose --env-file app.env pull journal planner
|
|
# Hand-written data migrations (idempotent) run BEFORE drizzle-kit
|
|
# push so unique-key reshapes can collapse duplicate rows first.
|
|
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
|
|
# --remove-orphans cleans up containers whose service was deleted
|
|
# from the compose file, matching cd-infra's behaviour.
|
|
docker compose --env-file app.env up -d --remove-orphans journal planner
|
|
|
|
# Reload Caddy with the Caddyfile we just scp'd. cd-apps
|
|
# ships infrastructure/Caddyfile alongside docker-compose.yml
|
|
# (see scp step above), but containers don't auto-pick-up
|
|
# config changes. cd-infra reloads Caddy as part of its
|
|
# deploy; cd-apps did NOT, which meant any Caddyfile change
|
|
# touching only `apps/`/`packages/` paths sat on disk
|
|
# unapplied until the next cd-infra run. The reload is
|
|
# idempotent (Caddy validates first, swaps live, no
|
|
# downtime) so doing it on every cd-apps deploy is safe
|
|
# even when Caddyfile is unchanged. `|| true` keeps the
|
|
# deploy from failing if Caddy itself is unhealthy.
|
|
docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile || true
|
|
|
|
# Clean up
|
|
docker image prune -af
|
|
docker compose ps
|
|
|
|
# Annotate deploy in Grafana. GRAFANA_SERVICE_TOKEN lives
|
|
# in secrets.infra.env (decrypted by cd-infra.yml into the
|
|
# merged /opt/trails-cool/.env on the server). cd-apps's
|
|
# own app.env intentionally does NOT carry it — apps don't
|
|
# need it at runtime. So we read from the merged `.env`
|
|
# that cd-infra populated. If cd-infra has never run on
|
|
# this host, .env may not exist; the `2>/dev/null` and
|
|
# the `if -n` guard make the annotation a silent no-op
|
|
# rather than a deploy failure in that case.
|
|
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
|