Fix deploy .env writing when secrets contain dollar signs.
All checks were successful
CI / Lint, type check, unit tests, coverage (push) Successful in 1m55s
CI / E2E browser tests (push) Successful in 48s

Docker Compose interpolates $VAR in .env files. Passwords like ...$A72y...
were truncated and the backend failed health checks, triggering rollback.

- Escape $ as $$ when writing production secrets to .env
- Document that deploy handles literal $ in Forgejo secrets
This commit is contained in:
Joakim Mörling 2026-05-21 16:17:36 +02:00
parent 7731eb1155
commit d652a5b862
2 changed files with 27 additions and 12 deletions

View file

@ -28,19 +28,33 @@ jobs:
git push origin ${{ github.event.inputs.version }}
- name: Write production .env
env:
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
STRIPE_PRICE_ID: ${{ secrets.STRIPE_PRICE_ID }}
SWISH_NUMBER: ${{ secrets.SWISH_NUMBER }}
ADMIN_EMAIL: ${{ secrets.ADMIN_EMAIL }}
ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }}
run: |
cat > .env << 'EOF'
POSTGRES_DB=${{ secrets.POSTGRES_DB }}
POSTGRES_USER=${{ secrets.POSTGRES_USER }}
POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}
JWT_SECRET=${{ secrets.JWT_SECRET }}
STRIPE_SECRET_KEY=${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET=${{ secrets.STRIPE_WEBHOOK_SECRET }}
STRIPE_PRICE_ID=${{ secrets.STRIPE_PRICE_ID }}
SWISH_NUMBER=${{ secrets.SWISH_NUMBER }}
ADMIN_EMAIL=${{ secrets.ADMIN_EMAIL }}
ADMIN_PASSWORD=${{ secrets.ADMIN_PASSWORD }}
EOF
# Docker Compose treats $ as variable interpolation in .env files.
# Escape literal dollar signs (e.g. in passwords) as $$.
escape() { printf '%s' "$1" | sed 's/\$/$$/g'; }
{
printf 'POSTGRES_DB=%s\n' "$(escape "$POSTGRES_DB")"
printf 'POSTGRES_USER=%s\n' "$(escape "$POSTGRES_USER")"
printf 'POSTGRES_PASSWORD=%s\n' "$(escape "$POSTGRES_PASSWORD")"
printf 'JWT_SECRET=%s\n' "$(escape "$JWT_SECRET")"
printf 'STRIPE_SECRET_KEY=%s\n' "$(escape "$STRIPE_SECRET_KEY")"
printf 'STRIPE_WEBHOOK_SECRET=%s\n' "$(escape "$STRIPE_WEBHOOK_SECRET")"
printf 'STRIPE_PRICE_ID=%s\n' "$(escape "$STRIPE_PRICE_ID")"
printf 'SWISH_NUMBER=%s\n' "$(escape "$SWISH_NUMBER")"
printf 'ADMIN_EMAIL=%s\n' "$(escape "$ADMIN_EMAIL")"
printf 'ADMIN_PASSWORD=%s\n' "$(escape "$ADMIN_PASSWORD")"
} > .env
- name: Build and start production stack
run: |

View file

@ -273,6 +273,7 @@ Before the first deploy, complete these steps on the production server (`srvr.nu
| `ADMIN_EMAIL` | Production admin email (e.g. `admin@bilhej.se`) |
| `ADMIN_PASSWORD` | Strong unique admin password (password manager) |
Passwords may contain `$` — the deploy workflow escapes these for Docker Compose.
Production does **not** seed `test@bilhej.se` or demo orders. On first start, the
backend creates one admin from `ADMIN_EMAIL` / `ADMIN_PASSWORD` if no admin exists.