Merge pull request #11 from pior-labs/docs-current-state #42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI/CD | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| concurrency: | |
| group: finlens-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| packages: read | |
| jobs: | |
| ci: | |
| name: CI | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Set up pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10.8.1 | |
| - name: Install workspace dependencies | |
| run: pnpm install --frozen-lockfile | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Typecheck workspace | |
| run: pnpm typecheck | |
| - name: Build workspace | |
| run: pnpm build | |
| - name: Build API Docker image | |
| run: docker build -f packages/api/Dockerfile . | |
| - name: Build web Docker image | |
| run: docker build -f packages/web/Dockerfile --secret id=node_auth_token,env=NODE_AUTH_TOKEN . | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| deploy: | |
| name: Deploy to OptiPlex | |
| needs: ci | |
| if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') | |
| runs-on: | |
| - self-hosted | |
| - linux | |
| - finlens | |
| defaults: | |
| run: | |
| working-directory: /opt/docker/finance-tracker | |
| steps: | |
| - name: Update production checkout | |
| run: | | |
| git fetch origin main | |
| git reset --hard origin/main | |
| - name: Write production env | |
| env: | |
| FINLENS_ENV: ${{ secrets.FINLENS_ENV }} | |
| run: | | |
| if [ -z "$FINLENS_ENV" ]; then | |
| echo "Missing required secret: FINLENS_ENV" | |
| exit 1 | |
| fi | |
| printf '%s\n' "$FINLENS_ENV" > .env | |
| chmod 600 .env | |
| - name: Validate required production settings | |
| run: | | |
| required=( | |
| DATABASE_URL | |
| BETTER_AUTH_SECRET | |
| BETTER_AUTH_URL | |
| BETTER_AUTH_TRUSTED_ORIGINS | |
| CENTRAL_AUTH_DISCOVERY_URL | |
| CENTRAL_AUTH_ISSUER | |
| CENTRAL_AUTH_CLIENT_ID | |
| CENTRAL_AUTH_CLIENT_SECRET | |
| ) | |
| for name in "${required[@]}"; do | |
| value="$(grep -E "^${name}=" .env | tail -n 1 | cut -d= -f2-)" | |
| if [ -z "$value" ]; then | |
| echo "Missing required setting in FINLENS_ENV: $name" | |
| exit 1 | |
| fi | |
| done | |
| - name: Ensure external Docker networks | |
| run: | | |
| for network in pior_edge pior_data; do | |
| docker network inspect "$network" >/dev/null 2>&1 || docker network create "$network" | |
| done | |
| - name: Validate production database URL | |
| run: | | |
| database_url="$(grep -E '^DATABASE_URL=' .env | tail -n 1 | cut -d= -f2-)" | |
| database_host="$(printf '%s\n' "$database_url" | sed -E 's#^postgres(ql)?://([^/@]+@)?([^/:?]+).*#\3#')" | |
| if [ -z "$database_host" ] || [ "$database_host" = "$database_url" ]; then | |
| echo "DATABASE_URL must be a Postgres connection URL" | |
| exit 1 | |
| fi | |
| if [ "$database_host" = "localhost" ] || [ "$database_host" = "127.0.0.1" ]; then | |
| echo "DATABASE_URL points at $database_host, which is the API container during deploy. Use the Postgres container hostname or network alias." | |
| exit 1 | |
| fi | |
| echo "Using production database host: $database_host" | |
| - name: Build Compose images | |
| run: docker compose build | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Verify central Auth discovery | |
| run: | | |
| docker compose run --rm --no-deps api node --input-type=module -e ' | |
| const discoveryUrl = process.env.CENTRAL_AUTH_DISCOVERY_URL; | |
| const expectedIssuer = process.env.CENTRAL_AUTH_ISSUER; | |
| const response = await fetch(discoveryUrl); | |
| if (!response.ok) { | |
| throw new Error(`Discovery request failed with HTTP ${response.status}`); | |
| } | |
| const discovery = await response.json(); | |
| if (discovery.issuer !== expectedIssuer) { | |
| throw new Error(`OIDC issuer mismatch: expected ${expectedIssuer}, received ${discovery.issuer}`); | |
| } | |
| console.log(`Verified central Auth issuer: ${discovery.issuer}`); | |
| ' | |
| - name: Run database migrations | |
| run: docker compose run --rm api node dist/db/migrate.js | |
| - name: Restart stack | |
| run: docker compose up -d --remove-orphans | |
| - name: Verify container health | |
| run: | | |
| healthy=false | |
| for attempt in $(seq 1 24); do | |
| if docker compose exec -T api wget -qO- http://127.0.0.1:3000/health >/dev/null \ | |
| && docker compose exec -T web wget -qO- http://127.0.0.1:80/ >/dev/null; then | |
| healthy=true | |
| break | |
| fi | |
| sleep 5 | |
| done | |
| docker compose ps | |
| if [ "$healthy" != true ]; then | |
| echo "Finance containers did not become healthy" | |
| docker compose logs --tail=100 api web | |
| exit 1 | |
| fi | |
| - name: Verify Finance through platform Caddy | |
| run: | | |
| healthy=false | |
| for attempt in $(seq 1 12); do | |
| if curl --fail --silent --show-error \ | |
| --resolve finance.szarans.ca:443:127.0.0.1 \ | |
| https://finance.szarans.ca/ >/dev/null; then | |
| healthy=true | |
| break | |
| fi | |
| sleep 5 | |
| done | |
| if [ "$healthy" != true ]; then | |
| echo "Finance did not become reachable through Caddy at https://finance.szarans.ca/" | |
| exit 1 | |
| fi |