diff --git a/.dockerignore b/.dockerignore index 122332251..242c1b08a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,14 @@ **/node_modules **/dist +**/.turbo +**/.git **/.github **/.vscode -**/.git +**/.idea **/.env -.env +**/.env.* +!**/.env.example +**/coverage +**/*.tsbuildinfo +**/*.log +.DS_Store diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml deleted file mode 100644 index fa4bef238..000000000 --- a/.github/workflows/deploy.yaml +++ /dev/null @@ -1,96 +0,0 @@ -name: Automatic Deployment - -on: - push: - branches: - - dev -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - environment: - name: ๐ŸŒ Setup Environment - runs-on: [self-hosted] - outputs: - target: ${{ steps.dev.outputs.target || steps.staging.outputs.target }} - steps: - - name: Verify NPM Token - env: - # We map the secret here to check its existence - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - if [ -z "$NPM_TOKEN" ]; then - echo "::error::The NPM_TOKEN secret is missing or empty. Please add it to your GitHub Secrets." - exit 1 - fi - echo "NPM_TOKEN is present, proceeding with build..." - - - name: ๐Ÿ› ๏ธ Set Development Environment - id: dev - if: ${{github.ref_name == 'dev'}} - run: | - echo "target=dev" >> $GITHUB_OUTPUT - - name: ๐Ÿš€ Set Staging Environment - id: staging - if: ${{github.ref_name == 'staging'}} - run: | - echo "target=staging" >> $GITHUB_OUTPUT - - build-base-image: - name: ๐Ÿ—๏ธ Build Base Image - runs-on: [self-hosted, dev] - needs: [environment] - steps: - - name: ๐Ÿ” Checkout - uses: actions/checkout@v4 - - - name: ๐Ÿณ Build Docker Image - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - set -euo pipefail - - # Create the multi-line file - cat < .npmrc_temp - @tria-plc:registry=https://npm.pkg.github.com - //npm.pkg.github.com/:_authToken=${NPM_TOKEN} - always-auth=true - EOF - - # Build using the file - docker build --secret id=npmrc,src=.npmrc_temp -t edr-${{needs.environment.outputs.target}} . - docker build --secret id=npmrc,src=.npmrc_temp --target passenger-migration -t edr-passenger-migration-${{needs.environment.outputs.target}} . - - # Shred/Remove the sensitive file - rm .npmrc_temp - - deploy-service: - name: ${{ matrix.display_name }} - runs-on: [self-hosted, dev] - needs: [build-base-image, environment] - strategy: - fail-fast: false - matrix: - include: - - service: freight-api - env_file: .env.freight-api - display_name: ๐Ÿšš Deploy Freight API Service - - service: passenger-api - env_file: .env.passenger-api - display_name: ๐Ÿง‘โ€๐Ÿฆฒ Deploy Passenger API Service - - steps: - - name: ๐Ÿ” Checkout - uses: actions/checkout@v4 - - - name: ๐Ÿ“‹ Copy ${{ matrix.service }} Environment - run: cp ~/environment/edr/${{needs.environment.outputs.target}}/${{ matrix.env_file }} .env - - - name: ๐Ÿงช Run Passenger API migrations - if: ${{ matrix.service == 'passenger-api' }} - run: | - docker run --rm --env-file .env edr-passenger-migration-${{needs.environment.outputs.target}} - - - name: ๐Ÿš€ Start ${{ matrix.service }} Service - run: docker compose --project-name="edr-${{needs.environment.outputs.target}}" up -d --force-recreate ${{ matrix.service }} --build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..359ce74f1 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,92 @@ +name: Deploy Stacks + +on: + push: + branches: + - main + - dev + - staging + paths: + - "apps/edr-freight-api/**" + - "apps/edr-freight-web/**" + - "apps/edr-passenger-api/**" + - "apps/edr-passenger-web/**" + - "packages/**" + - "infrastructure/docker/Dockerfile.web" + - "infrastructure/nginx/**" + - "docker-compose.yaml" + - "pnpm-lock.yaml" + - "scripts/deploy/**" + - ".github/workflows/deploy.yml" + workflow_dispatch: + +concurrency: + group: deploy-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + deploy: + name: Deploy ${{ matrix.service }} + runs-on: self-hosted + strategy: + fail-fast: false + matrix: + include: + - project: edr-freight + build_env_file: freight-web.build.env + service: freight-api + - project: edr-freight + build_env_file: freight-web.build.env + service: freight-portal + - project: edr-freight + build_env_file: freight-web.build.env + service: freight-backoffice + - project: edr-passenger + build_env_file: passenger-web.build.env + service: passenger-api + - project: edr-passenger + build_env_file: passenger-web.build.env + service: passenger-portal + - project: edr-passenger + build_env_file: passenger-web.build.env + service: passenger-backoffice + env: + PROJECT: ${{ matrix.project }} + BRANCH: ${{ github.ref_name }} + DEPLOY_USER: tria + BUILD_ENV_FILE: ${{ matrix.build_env_file }} + DOCKER_BUILDKIT: "1" + COMPOSE_DOCKER_CLI_BUILD: "1" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Sync environment from server + run: | + chmod +x scripts/deploy/*.sh + ./scripts/deploy/sync-env-from-server.sh "${{ matrix.service }}" + + - name: Set compose project name + run: | + set -euo pipefail + branch_slug=$(echo "${BRANCH}" | tr "[:upper:]" "[:lower:]" | sed -E "s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//") + echo "COMPOSE_PROJECT_NAME=${PROJECT}-${branch_slug}" >> "${GITHUB_ENV}" + + - name: Configure npm auth for Docker builds + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: ./scripts/deploy/create-npmrc.sh + + - name: Build ${{ matrix.service }} + run: | + set -euo pipefail + docker compose --project-name "${COMPOSE_PROJECT_NAME}" build "${{ matrix.service }}" + + - name: Deploy ${{ matrix.service }} + run: | + set -euo pipefail + docker compose --project-name "${COMPOSE_PROJECT_NAME}" up -d "${{ matrix.service }}" + + - name: Remove npm credentials from workspace + if: always() + run: rm -f .npmrc .npmrc_temp diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 000000000..00ca421d1 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,187 @@ +# Deployment Runbook + +This document explains how deployments work for the EDR platform using Docker, GitHub Actions, and self-hosted runners. + +## Overview + +- Monorepo contains 6 deployable services: + - `freight-api` + - `freight-portal` + - `freight-backoffice` + - `passenger-api` + - `passenger-portal` + - `passenger-backoffice` +- Deployments run through one workflow: `.github/workflows/deploy.yml` +- Each service is built/deployed independently in parallel (matrix jobs). +- Docker Compose project names are branch-aware to avoid environment collisions on the same host. + +## Prerequisites + +- Docker Engine with Compose plugin on the self-hosted runner. +- GitHub self-hosted runner registered for this repository. +- Repository secret configured: + - `NPM_TOKEN` (for private `@tria-plc/*` package install during Docker build) +- Server-side env files created for each branch/environment. + +## Server Environment Files + +`sync-env-from-server.sh` reads env files from: + +`/home//environment/edr///` + +Where: + +- `` defaults to `tria` (overridable by `DEPLOY_USER`) +- `` is derived from Git branch (lowercase, non-alphanumeric replaced with `-`) +- `` is `edr-freight` or `edr-passenger` + +### Required files per project + +For `edr-freight`: + +- `freight-api.env` +- `freight-portal.env` +- `freight-backoffice.env` +- optional: `freight-web.build.env` + +For `edr-passenger`: + +- `passenger-api.env` +- `passenger-portal.env` +- `passenger-backoffice.env` +- optional: `passenger-web.build.env` + +### Required env key + +Each service env file must contain: + +- `PORT=` + +The sync script validates this and fails if missing. + +### Build env files (optional) + +Used for build-time variables (example: Vite API URLs), with `export` syntax: + +```bash +export FREIGHT_VITE_API_URL=https://freight-api.example.com/api +export PASSENGER_VITE_API_URL=https://passenger-api.example.com +``` + +These are injected into `GITHUB_ENV` during workflow execution. + +## Docker Compose Port Mapping + +`docker-compose.yaml` uses per-service env variables for host/container port mappings: + +- `FREIGHT_API_PORT` +- `PASSENGER_API_PORT` +- `FREIGHT_PORTAL_PORT` +- `FREIGHT_BACKOFFICE_PORT` +- `PASSENGER_PORTAL_PORT` +- `PASSENGER_BACKOFFICE_PORT` + +`scripts/deploy/sync-env-from-server.sh` extracts `PORT` from each synced `.env` and exports the corresponding `*_PORT` variable to `GITHUB_ENV`. + +## GitHub Actions Deployment Flow + +Workflow file: `.github/workflows/deploy.yml` + +### 1) `prepare` job + +- Checks out repository once. +- Creates workspace artifact (`workspace.tgz`) and uploads it. + +### 2) `deploy` matrix job (parallel) + +For each service: + +- Downloads and extracts workspace artifact. +- Syncs that service env file from server path. +- Computes branch slug and sets: + - `COMPOSE_PROJECT_NAME=-` +- Creates `.npmrc`/`.npmrc_temp` from `NPM_TOKEN`. +- Runs: + - `docker compose --project-name "$COMPOSE_PROJECT_NAME" build ` + - `docker compose --project-name "$COMPOSE_PROJECT_NAME" up -d ` +- Cleans `.npmrc`/`.npmrc_temp`. + +## Branch/Environment Isolation + +Compose project name is generated as: + +`-` + +Examples: + +- `edr-freight-main` +- `edr-freight-staging` +- `edr-passenger-dev` + +This prevents container/network/volume name collisions between branches. + +## Local Manual Deployment (Optional) + +From repo root: + +```bash +DOCKER_BUILDKIT=1 docker compose build +docker compose up -d +``` + +If private packages are required locally, create `.npmrc`: + +```bash +cat < .npmrc +@tria-plc:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken= +always-auth=true +EOF +``` + +## Passenger API Startup Behavior + +Passenger container entrypoint runs on startup: + +1. `npm run prisma:generate` +2. `npm run prisma:migrate` (deploy mode) +3. `npm run prisma:seed` +4. starts API process + +## Troubleshooting + +### Missing env file + +Error: + +- `Missing env file: ...` + +Fix: + +- Create the required file in the server env directory for that project/branch slug. + +### Missing PORT in env file + +Error: + +- `Missing required PORT in env file: ...` + +Fix: + +- Add `PORT=` to that service env file. + +### Private package install fails + +Check: + +- `NPM_TOKEN` exists in repo secrets. +- Workflow created `.npmrc` successfully. + +### Prisma seed/migrate failures (passenger) + +Check: + +- `DATABASE_URL` in `passenger-api.env` +- DB reachability from runner host/container network +- migration history consistency + diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 7693a2939..000000000 --- a/Dockerfile +++ /dev/null @@ -1,84 +0,0 @@ -FROM node:24.15.0 AS base -RUN corepack enable && corepack prepare pnpm@latest-11 --activate -WORKDIR /app - -FROM base AS deps - -COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ - -COPY apps/edr-freight-api/package.json ./apps/edr-freight-api/ -COPY apps/edr-passenger-api/package.json ./apps/edr-passenger-api/ - -COPY apps/edr-freight-web/backoffice/package.json ./apps/edr-freight-web/backoffice/ -COPY apps/edr-freight-web/portal/package.json ./apps/edr-freight-web/portal/ - -COPY apps/edr-passenger-web/backoffice/package.json ./apps/edr-passenger-web/backoffice/ -COPY apps/edr-passenger-web/portal/package.json ./apps/edr-passenger-web/portal/ - - -COPY packages/api-common/package.json packages/api-common/ - -COPY packages/config/eslint-config/package.json packages/config/eslint-config/ -COPY packages/config/prettier-config/package.json packages/config/prettier-config/ -COPY packages/config/tsconfig/package.json packages/config/tsconfig/ - -COPY packages/types/package.json packages/types/ -COPY packages/ui-common/package.json packages/ui-common/ - -RUN --mount=type=cache,id=pnpm,target=/pnpm/store\ - --mount=type=secret,id=npmrc,target=./.npmrc \ - pnpm install --frozen-lockfile -FROM deps AS build -COPY . . - -RUN pnpm run build --filter=\!"@edr/passenger-portal" - -FROM base AS freight-api -# RUN corepack enable && corepack prepare pnpm@9.12.0 --activate -WORKDIR /app/apps/edr-freight-api -ENV NODE_ENV=production - -COPY --from=deps /app/node_modules ./../../node_modules -COPY --from=deps /app/apps/edr-freight-api/node_modules ./node_modules -COPY --from=build /app/apps/edr-freight-api/dist ./dist -COPY --from=build /app/apps/edr-freight-api/package.json ./package.json -COPY --from=build /app/packages ./../../packages - -EXPOSE 3001 -CMD ["node", "dist/main.js"] - - -FROM base AS passenger-api -RUN apt-get update -y && apt-get install -y openssl -# RUN corepack enable && corepack prepare pnpm@9.12.0 --activate -WORKDIR /app/apps/edr-passenger-api -ENV NODE_ENV=production - -# Use build-stage node_modules (not deps): `pnpm run build` runs `prisma generate`, which -# writes the real @prisma/client (enums, types). deps never runs generate, so @IsEnum(ServiceClass) -# and similar would see undefined at runtime if we copied deps only. -COPY --from=build /app/node_modules ./../../node_modules -COPY --from=build /app/apps/edr-passenger-api/node_modules ./node_modules -COPY --from=build /app/apps/edr-passenger-api/dist ./dist -COPY --from=build /app/apps/edr-passenger-api/package.json ./package.json -COPY --from=build /app/packages ./../../packages - -EXPOSE 3001 -CMD ["node", "dist/main.js"] - -FROM build as passenger-migration -WORKDIR /app/apps/edr-passenger-api -CMD pnpm run prisma:migrate && pnpm run prisma:seed - - - -FROM nginx:1.27-alpine AS freight-web-portal -COPY --from=build /app/apps/edr-freight-web/portal/dist /usr/share/nginx/html -EXPOSE 5173 -CMD ["nginx", "-g", "daemon off;"] - - -FROM nginx:1.27-alpine AS freight-web-backoffice -COPY --from=build /app/apps/edr-freight-web/backoffice/dist /usr/share/nginx/html -EXPOSE 5173 -CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index c35b3cfb2..1b488ee81 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ pnpm --filter @edr/passenger-api run prisma:generate #### Run Migrations ```bash -pnpm --filter @edr/passenger-api run prisma:migrate +pnpm --filter @edr/passenger-api run prisma:migrate:dev ``` #### Seed Database @@ -523,59 +523,103 @@ pnpm --filter @edr/passenger-api run type-check # TypeScript check # Database pnpm --filter @edr/passenger-api run prisma:generate # Generate Prisma client -pnpm --filter @edr/passenger-api run prisma:migrate # Run migrations +pnpm --filter @edr/passenger-api run prisma:migrate:dev # Run migrations (local dev) pnpm --filter @edr/passenger-api run prisma:seed # Seed database ``` ## ๐Ÿณ Docker Deployment -### Build Image +All six apps build from Dockerfiles: each API has its own (`apps/edr-freight-api/Dockerfile`, `apps/edr-passenger-api/Dockerfile`); Vite frontends share `infrastructure/docker/Dockerfile.web` and are served with **nginx**. APIs run on **Node 22**. + +**Prerequisites** + +- Docker with BuildKit enabled +- A local [`.npmrc`](.gitignore) with GitHub Packages auth for `@tria-plc/*` (required for **freight** API and web images) +- External Postgres for each API (compose does **not** include databases) +- Copy `apps/edr-freight-api/.env.example` โ†’ `.env` and `apps/edr-passenger-api/.env.example` โ†’ `.env` with real connection strings + +### Build and run (all apps) + ```bash # From monorepo root -docker build -f apps/edr-passenger-api/Dockerfile -t edr-passenger-api . +DOCKER_BUILDKIT=1 pnpm docker:build +pnpm docker:up ``` -### Run Container +Or without pnpm scripts: + ```bash -docker run -d \ - --name edr-api \ - -p 4000:4000 \ - --env-file apps/edr-passenger-api/.env \ - edr-passenger-api +DOCKER_BUILDKIT=1 docker compose build +docker compose up -d ``` -### Docker Compose (Recommended) -```yaml -version: '3.8' -services: - postgres: - image: postgres:15 - environment: - POSTGRES_USER: edr - POSTGRES_PASSWORD: edr_secret - POSTGRES_DB: edr_passenger - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data +| Service | URL (default) | +|---------|----------------| +| Freight API | http://localhost:3001 | +| Passenger API | http://localhost:4000 | +| Freight portal | http://localhost:5173 | +| Freight backoffice | http://localhost:5183 | +| Passenger portal | http://localhost:5174 | +| Passenger backoffice | http://localhost:5184 | - api: - build: - context: . - dockerfile: apps/edr-passenger-api/Dockerfile - ports: - - "4000:4000" - environment: - DATABASE_URL: postgresql://edr:edr_secret@postgres:5432/edr_passenger - JWT_SECRET: your-secret-key - PORT: 4000 - depends_on: - - postgres +### Build a single service -volumes: - postgres_data: +```bash +docker compose build freight-api +docker compose build passenger-portal ``` +Freight images mount `.npmrc` as a BuildKit secret during `pnpm install`. Passenger web images do not require private packages. + +### `VITE_API_URL` (frontends) + +API URLs are **baked in at image build time** (`import.meta.env.VITE_API_URL`). Defaults in [`docker-compose.yaml`](docker-compose.yaml) use `http://localhost:3001/api` (freight) and `http://localhost:4000` (passenger) for local smoke tests. Override build args for production, e.g.: + +```bash +docker compose build freight-portal \ + --build-arg VITE_API_URL=https://freight-api.example.com/api +``` + +### Migrations + +- **Freight API:** TypeORM migrations are not run on container startup โ€” apply them separately before deploy. +- **Passenger API:** On each container start, the entrypoint runs `npm run prisma:migrate` and `npm run prisma:seed` (same `package.json` scripts as `pnpm run`) before starting the server. Ensure `DATABASE_URL` in `.env` points at a reachable Postgres instance. + +For local development, use `pnpm --filter @edr/passenger-api run prisma:migrate:dev` instead of `prisma:migrate`. + +### GitHub Actions (self-hosted runner) + +Two workflows deploy independently on push to `main`, `develop`, or `staging`: + +| Workflow | Services | Server env root | +|----------|----------|-----------------| +| [`.github/workflows/deploy-freight.yml`](.github/workflows/deploy-freight.yml) | freight-api, freight-portal, freight-backoffice | `/home/user/environmen/edr-freight//` | +| [`.github/workflows/deploy-passenger.yml`](.github/workflows/deploy-passenger.yml) | passenger-api, passenger-portal, passenger-backoffice | `/home/user/environmen/edr-passenger//` | + +**On the runner**, place env files before the first deploy (example for branch `main`): + +```text +/home/user/environmen/edr-freight/main/ + freight-api.env + freight-portal.env # optional runtime env for Vite/nginx + freight-backoffice.env + freight-web.build.env # exports FREIGHT_VITE_API_URL=... + +/home/user/environmen/edr-passenger/main/ + passenger-api.env + passenger-portal.env + passenger-backoffice.env + passenger-web.build.env # exports PASSENGER_VITE_API_URL=... +``` + +Example `freight-web.build.env`: + +```bash +export FREIGHT_VITE_API_URL=https://freight-api.example.com/api +``` + +The workflow copies `*.env` into each app directory, creates `.npmrc` from the `NPM_TOKEN` repository secret, then runs `docker compose build` and `docker compose up -d` for that stack. + ## ๐Ÿ”’ Security Best Practices 1. **Environment Variables** - Never commit `.env` files. Use secrets management in production. diff --git a/apps/edr-freight-api/.env.example b/apps/edr-freight-api/.env.example index 6c7738a7a..a3ddffc40 100644 --- a/apps/edr-freight-api/.env.example +++ b/apps/edr-freight-api/.env.example @@ -1,17 +1,7 @@ -# App -NODE_ENV=development +# Copy to .env for local/docker compose (not committed). PORT=3001 - -# Database DB_HOST=localhost DB_PORT=5433 -DB_NAME=edr_freight DB_USER=postgres DB_PASSWORD= - -# JWT (provided by external auth package โ€” placeholder only) -JWT_SECRET= - -# Redis -REDIS_HOST=localhost -REDIS_PORT=6379 +DB_NAME=edr_freight diff --git a/apps/edr-freight-api/Dockerfile b/apps/edr-freight-api/Dockerfile new file mode 100644 index 000000000..b0850737b --- /dev/null +++ b/apps/edr-freight-api/Dockerfile @@ -0,0 +1,37 @@ +# syntax=docker/dockerfile:1 +# Build from monorepo root: docker build -f apps/edr-freight-api/Dockerfile . + +FROM node:24.15.0-alpine AS base +RUN apk add --no-cache libc6-compat +RUN corepack enable +WORKDIR /app + +FROM base AS pruner +COPY . . +RUN pnpm dlx turbo prune "@edr/freight-api" --docker + +FROM base AS installer +COPY --from=pruner /app/out/json/ . +COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml +RUN --mount=type=secret,id=npmrc,target=./.npmrc,required=false \ + pnpm install --frozen-lockfile + +FROM base AS builder +COPY --from=installer /app/ . +COPY --from=pruner /app/out/full/ . +RUN pnpm turbo build --filter="@edr/freight-api..." + +FROM base AS deployer +COPY --from=builder /app/ . +RUN pnpm deploy --filter="@edr/freight-api" --prod --legacy /deploy + +FROM node:24.15.0-alpine AS runner +RUN apk add --no-cache libc6-compat +ENV NODE_ENV=production +WORKDIR /app +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 --ingroup nodejs nestjs +COPY --from=deployer --chown=nestjs:nodejs /deploy . +USER nestjs +EXPOSE 3001 +CMD ["node", "dist/main.js"] diff --git a/apps/edr-passenger-api/.env.example b/apps/edr-passenger-api/.env.example index 4a535478a..241c12d63 100644 --- a/apps/edr-passenger-api/.env.example +++ b/apps/edr-passenger-api/.env.example @@ -103,4 +103,4 @@ FAYDA_ACR_VALUES=mosip:idp:acr:generated-code FAYDA_CLAIMS_LOCALES=en am FAYDA_SESSION_TTL_MINUTES=10 -GITHUB_PACKAGE_TOKEN= \ No newline at end of file +GITHUB_PACKAGE_TOKEN= diff --git a/apps/edr-passenger-api/Dockerfile b/apps/edr-passenger-api/Dockerfile new file mode 100644 index 000000000..5fd647968 --- /dev/null +++ b/apps/edr-passenger-api/Dockerfile @@ -0,0 +1,51 @@ +# syntax=docker/dockerfile:1 +# Build from monorepo root: docker build -f apps/edr-passenger-api/Dockerfile . +# On start: runs prisma migrate deploy + seed, then the API. + +FROM node:24.15.0-alpine AS base +RUN apk add --no-cache libc6-compat +RUN corepack enable +WORKDIR /app + +FROM base AS pruner +COPY . . +RUN pnpm dlx turbo prune "@edr/passenger-api" --docker + +FROM base AS installer +COPY --from=pruner /app/out/json/ . +COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml +RUN --mount=type=secret,id=npmrc,target=./.npmrc,required=false \ + --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install --frozen-lockfile + +FROM base AS builder +COPY --from=installer /app/ . +COPY --from=pruner /app/out/full/ . +RUN pnpm --filter "@edr/passenger-api" exec prisma generate +RUN pnpm turbo build --filter="@edr/passenger-api..." + +FROM base AS deployer +COPY --from=builder /app/ . +RUN pnpm deploy --filter="@edr/passenger-api" --legacy /deploy +RUN if [ -d node_modules/.prisma ]; then \ + mkdir -p /deploy/node_modules && \ + cp -r node_modules/.prisma /deploy/node_modules/.prisma; \ + fi + +FROM node:24.15.0-alpine AS runner +RUN apk add --no-cache libc6-compat +RUN corepack enable && corepack prepare pnpm@11.1.1 --activate +ENV NODE_ENV=production +WORKDIR /app +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 --ingroup nodejs nestjs +COPY --from=deployer /deploy . +COPY apps/edr-passenger-api/docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh \ + && chown -R nestjs:nodejs /app +USER nestjs +ENV CI=true +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 +EXPOSE 4000 +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["node", "dist/main.js"] diff --git a/apps/edr-passenger-api/docker-entrypoint.sh b/apps/edr-passenger-api/docker-entrypoint.sh new file mode 100644 index 000000000..087ff1e74 --- /dev/null +++ b/apps/edr-passenger-api/docker-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +cd /app + +# npm run executes the same package.json scripts as pnpm run (pnpm reinstalls in deploy layout) +npm run prisma:generate +npm run prisma:migrate +npm run prisma:seed + +exec "$@" diff --git a/apps/edr-passenger-api/package.json b/apps/edr-passenger-api/package.json index 6b9358ebc..043211ce2 100644 --- a/apps/edr-passenger-api/package.json +++ b/apps/edr-passenger-api/package.json @@ -44,7 +44,8 @@ "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "swagger-ui-express": "^5.0.0", - "tsconfig-paths": "^4.2.0" + "tsconfig-paths": "^4.2.0", + "@prisma/client": "^6.19.3" }, "devDependencies": { "@edr/eslint-config": "workspace:*", @@ -52,7 +53,6 @@ "@nestjs/cli": "^11.0.21", "@nestjs/schematics": "^11.1.0", "@nestjs/testing": "^11.1.19", - "@prisma/client": "^6.19.3", "@types/bcrypt": "^5.0.2", "@types/jest": "^29.5.11", "@types/node": "^20.10.6", diff --git a/apps/edr-passenger-api/prisma/seed.ts b/apps/edr-passenger-api/prisma/seed.ts index 1f3ea7e04..8a9707c8e 100644 --- a/apps/edr-passenger-api/prisma/seed.ts +++ b/apps/edr-passenger-api/prisma/seed.ts @@ -1,4 +1,4 @@ -import { PrismaClient, SeatKind } from '@prisma/client'; +import { PrismaClient } from '@prisma/client'; import * as bcrypt from 'bcrypt'; const prisma = new PrismaClient(); @@ -136,11 +136,11 @@ async function seedCoachesAndSeats(seatClasses: any[]) { col, label: `${row}${col}`, seatNumber: `${config.label}${row}${col}`, - kind: (row === 1 && col === 'A' ? 'ACCESSIBLE' : 'STANDARD') as SeatKind, + kind: row === 1 && col === 'A' ? 'ACCESSIBLE' : 'STANDARD', }); } } - await prisma.seat.createMany({ data: seats }); + await prisma.seat.createMany({ data: seats as any }); } } @@ -162,7 +162,7 @@ async function seedSchedules(trains: any[], stations: any[], routes: any[]) { const existingScheduleIds = (await prisma.trainSchedule.findMany({ where: { trainId: { in: [train301.id, train302.id, train303.id] } }, select: { id: true }, - })).map((s) => s.id); + })).map((s: { id: string }) => s.id); if (existingScheduleIds.length > 0) { // Delete in correct order to avoid foreign key constraints diff --git a/apps/edr-passenger-web/portal/src/services/stations.service.ts b/apps/edr-passenger-web/portal/src/services/stations.service.ts index 1c1b4f4d2..20010954a 100644 --- a/apps/edr-passenger-web/portal/src/services/stations.service.ts +++ b/apps/edr-passenger-web/portal/src/services/stations.service.ts @@ -1,12 +1,13 @@ -import type { IStation } from "../types"; +// import type { IStation } from "../types"; import { api } from "../utils/api"; +import type { Passenger } from "@edr/types"; export const stationsService = { - list: async (): Promise => { + list: async (): Promise => { const { data } = await api.get("/stations"); return data.data; }, - get: async (id: string): Promise => { + get: async (id: string): Promise => { const { data } = await api.get(`/stations/${id}`); return data.data; }, diff --git a/checkpoint.md b/checkpoint.md new file mode 100644 index 000000000..31eb86b2b --- /dev/null +++ b/checkpoint.md @@ -0,0 +1,63 @@ +# Checkpoint + +## Major Tasks Completed + +1. Docker deployment scaffolded for all 6 apps in the monorepo. +2. Split API images into app-specific Dockerfiles: + - `apps/edr-freight-api/Dockerfile` + - `apps/edr-passenger-api/Dockerfile` +3. Kept shared Vite/nginx image: + - `infrastructure/docker/Dockerfile.web` + - `infrastructure/nginx/spa.conf` +4. Updated `docker-compose.yaml` to run all 6 services (apps only, no Postgres service in compose). +5. Added/updated deployment scripts: + - `scripts/deploy/create-npmrc.sh` + - `scripts/deploy/sync-env-from-server.sh` +6. Added self-hosted GitHub Actions deployment workflow: + - Consolidated into one file: `.github/workflows/deploy.yml` +7. Deployment workflow now: + - uses a single checkout (`prepare` job), + - deploys services via parallel matrix, + - sets compose project names per branch/environment, + - passes explicit `docker compose --project-name`. +8. Environment sync script now: + - supports branch slug paths, + - validates each service env file exists, + - requires `PORT` in each env file, + - exports per-service port vars to `GITHUB_ENV`. +9. `docker-compose.yaml` now reads per-service ports via variables exported from env sync. +10. Passenger startup flow fixed to run: + - `prisma:generate`, + - `prisma:migrate`, + - `prisma:seed`, + before API startup. +11. Passenger seed TypeScript issues fixed in `apps/edr-passenger-api/prisma/seed.ts` so it compiles under strict checks. +12. Added deployment runbook: + - `DEPLOYMENT.md` + +## Key Files to Review + +- `.github/workflows/deploy.yml` +- `docker-compose.yaml` +- `scripts/deploy/sync-env-from-server.sh` +- `scripts/deploy/create-npmrc.sh` +- `apps/edr-passenger-api/docker-entrypoint.sh` +- `apps/edr-passenger-api/prisma/seed.ts` +- `DEPLOYMENT.md` + +## Next Actions + +1. Run full CI on all target branches (`main`, `dev`, `staging`) and verify matrix job behavior. +2. Validate server env directory layout matches script expectations: + - `/home//environment/edr///...` +3. Confirm each service env file includes valid `PORT` and service-specific runtime vars. +4. Verify branch-specific compose project names produce isolated containers/networks/volumes on runner. +5. Smoke test all 6 deployed services behind real environment URLs. + +## Open Risks / Notes + +1. Passenger seed runs on every container start; confirm this is desired for production-like environments. +2. Prisma warns about `package.json#prisma` deprecation (Prisma 7 migration pending). +3. Matrix parallelism increases runner load; ensure self-hosted runner capacity is sufficient. +4. Port collisions are prevented by env-driven mapping, but bad env values can still cause runtime conflicts. + diff --git a/docker-compose.yaml b/docker-compose.yaml index aa9ec58e2..2438b70fa 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,21 +1,80 @@ +# EDR Platform โ€” application containers only (no Postgres). +# Requires a local .npmrc with GitHub Packages auth for @tria-plc (freight API + freight web). +# Copy apps/*/env.example to .env and set real values before `docker compose up`. +# +# Build: DOCKER_BUILDKIT=1 docker compose build +# Run: docker compose up -d + services: freight-api: build: context: . - dockerfile: ./Dockerfile - target: freight-api - env_file: - - .env + dockerfile: apps/edr-freight-api/Dockerfile + secrets: + - npmrc ports: - - ${PORT}:${PORT} - restart: unless-stopped + - "${FREIGHT_API_PORT:-3001}:${FREIGHT_API_PORT:-3001}" + env_file: + - apps/edr-freight-api/.env + passenger-api: build: context: . - dockerfile: ./Dockerfile - target: passenger-api - env_file: - - .env + dockerfile: apps/edr-passenger-api/Dockerfile ports: - - ${PORT}:${PORT} - restart: unless-stopped + - "${PASSENGER_API_PORT:-4000}:${PASSENGER_API_PORT:-4000}" + env_file: + - apps/edr-passenger-api/.env + + freight-portal: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.web + args: + TURBO_FILTER: "@edr/freight-portal" + APP_PATH: apps/edr-freight-web/portal + # Browser-reachable URL; override for production deployments + VITE_API_URL: ${FREIGHT_VITE_API_URL:-http://localhost:3001/api} + secrets: + - npmrc + ports: + - "${FREIGHT_PORTAL_PORT:-5173}:80" + + freight-backoffice: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.web + args: + TURBO_FILTER: "@edr/freight-backoffice" + APP_PATH: apps/edr-freight-web/backoffice + VITE_API_URL: ${FREIGHT_VITE_API_URL:-http://localhost:3001/api} + secrets: + - npmrc + ports: + - "${FREIGHT_BACKOFFICE_PORT:-5183}:80" + + passenger-portal: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.web + args: + TURBO_FILTER: "@edr/passenger-portal" + APP_PATH: apps/edr-passenger-web/portal + VITE_API_URL: ${PASSENGER_VITE_API_URL:-http://localhost:4000} + ports: + - "${PASSENGER_PORTAL_PORT:-5174}:80" + + passenger-backoffice: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.web + args: + TURBO_FILTER: "@edr/passenger-backoffice" + APP_PATH: apps/edr-passenger-web/backoffice + VITE_API_URL: ${PASSENGER_VITE_API_URL:-http://localhost:4000} + ports: + - "${PASSENGER_BACKOFFICE_PORT:-5184}:80" + +secrets: + npmrc: + file: .npmrc diff --git a/infrastructure/docker/Dockerfile.web b/infrastructure/docker/Dockerfile.web new file mode 100644 index 000000000..38e863b86 --- /dev/null +++ b/infrastructure/docker/Dockerfile.web @@ -0,0 +1,38 @@ +# syntax=docker/dockerfile:1 + +ARG TURBO_FILTER=@edr/freight-portal +ARG APP_PATH=apps/edr-freight-web/portal +ARG VITE_API_URL=http://localhost:3001/api + +FROM node:24.15.0-alpine AS base +RUN apk add --no-cache libc6-compat +RUN corepack enable +WORKDIR /app + +FROM base AS pruner +ARG TURBO_FILTER +COPY . . +RUN pnpm dlx turbo prune "${TURBO_FILTER}" --docker + +FROM base AS installer +COPY --from=pruner /app/out/json/ . +COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml +RUN --mount=type=secret,id=npmrc,target=./.npmrc,required=false \ + --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install --frozen-lockfile + +FROM base AS builder +ARG TURBO_FILTER +ARG APP_PATH +ARG VITE_API_URL +ENV VITE_API_URL=${VITE_API_URL} +COPY --from=installer /app/ . +COPY --from=pruner /app/out/full/ . +RUN pnpm turbo build --filter="${TURBO_FILTER}..." + +FROM nginx:alpine AS runner +ARG APP_PATH +COPY infrastructure/nginx/spa.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/${APP_PATH}/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/infrastructure/docker/docker-compose.dev.yml b/infrastructure/docker/docker-compose.dev.yml deleted file mode 100644 index 7ed9e79a7..000000000 --- a/infrastructure/docker/docker-compose.dev.yml +++ /dev/null @@ -1,126 +0,0 @@ -version: "3.9" - -networks: - edr-network: - driver: bridge - -volumes: - postgres-freight-data: - postgres-passenger-data: - redis-data: - -services: - postgres-freight: - image: postgres:17-alpine - container_name: edr-postgres-freight - restart: unless-stopped - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: edr_freight - ports: - - "5433:5432" - volumes: - - postgres-freight-data:/var/lib/postgresql/data - networks: - - edr-network - - postgres-passenger: - image: postgres:17-alpine - container_name: edr-postgres-passenger - restart: unless-stopped - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: edr_passenger - ports: - - "5434:5432" - volumes: - - postgres-passenger-data:/var/lib/postgresql/data - networks: - - edr-network - - redis: - image: redis:7-alpine - container_name: edr-redis - restart: unless-stopped - ports: - - "6379:6379" - volumes: - - redis-data:/data - networks: - - edr-network - - edr-freight-api: - build: - context: ../../ - dockerfile: apps/edr-freight-api/Dockerfile - container_name: edr-freight-api - restart: unless-stopped - depends_on: - - postgres-freight - - redis - environment: - NODE_ENV: development - PORT: 3001 - DB_HOST: postgres-freight - DB_PORT: 5432 - DB_NAME: edr_freight - DB_USER: postgres - DB_PASSWORD: postgres - REDIS_HOST: redis - REDIS_PORT: 6379 - ports: - - "3001:3001" - networks: - - edr-network - - edr-freight-web: - build: - context: ../../ - dockerfile: apps/edr-freight-web/Dockerfile - container_name: edr-freight-web - restart: unless-stopped - depends_on: - - edr-freight-api - ports: - - "5173:5173" - networks: - - edr-network - - edr-passenger-api: - build: - context: ../../ - dockerfile: apps/edr-passenger-api/Dockerfile - container_name: edr-passenger-api - restart: unless-stopped - depends_on: - - postgres-passenger - - redis - environment: - NODE_ENV: development - PORT: 3002 - DB_HOST: postgres-passenger - DB_PORT: 5432 - DB_NAME: edr_passenger - DB_USER: postgres - DB_PASSWORD: postgres - REDIS_HOST: redis - REDIS_PORT: 6379 - ports: - - "3002:3002" - networks: - - edr-network - - edr-passenger-web: - build: - context: ../../ - dockerfile: apps/edr-passenger-web/Dockerfile - container_name: edr-passenger-web - restart: unless-stopped - depends_on: - - edr-passenger-api - ports: - - "5174:5174" - networks: - - edr-network diff --git a/infrastructure/docker/docker-compose.prod.yml b/infrastructure/docker/docker-compose.prod.yml deleted file mode 100644 index a8d18455c..000000000 --- a/infrastructure/docker/docker-compose.prod.yml +++ /dev/null @@ -1,134 +0,0 @@ -version: "3.9" - -networks: - edr-network: - driver: bridge - -volumes: - postgres-freight-data: - postgres-passenger-data: - redis-data: - -services: - postgres-freight: - image: postgres:17-alpine - container_name: edr-postgres-freight - restart: always - environment: - POSTGRES_USER: ${DB_USER} - POSTGRES_PASSWORD: ${DB_PASSWORD} - POSTGRES_DB: ${DB_NAME_FREIGHT} - volumes: - - postgres-freight-data:/var/lib/postgresql/data - networks: - - edr-network - - postgres-passenger: - image: postgres:17-alpine - container_name: edr-postgres-passenger - restart: always - environment: - POSTGRES_USER: ${DB_USER} - POSTGRES_PASSWORD: ${DB_PASSWORD} - POSTGRES_DB: ${DB_NAME_PASSENGER} - volumes: - - postgres-passenger-data:/var/lib/postgresql/data - networks: - - edr-network - - redis: - image: redis:7-alpine - container_name: edr-redis - restart: always - volumes: - - redis-data:/data - networks: - - edr-network - - edr-freight-api: - build: - context: ../../ - dockerfile: apps/edr-freight-api/Dockerfile - container_name: edr-freight-api - restart: always - depends_on: - - postgres-freight - - redis - environment: - NODE_ENV: production - PORT: 3001 - DB_HOST: postgres-freight - DB_PORT: 5432 - DB_NAME: ${DB_NAME_FREIGHT} - DB_USER: ${DB_USER} - DB_PASSWORD: ${DB_PASSWORD} - REDIS_HOST: redis - expose: - - "3001" - networks: - - edr-network - - edr-freight-web: - build: - context: ../../ - dockerfile: apps/edr-freight-web/Dockerfile - container_name: edr-freight-web - restart: always - depends_on: - - edr-freight-api - expose: - - "5173" - networks: - - edr-network - - edr-passenger-api: - build: - context: ../../ - dockerfile: apps/edr-passenger-api/Dockerfile - container_name: edr-passenger-api - restart: always - depends_on: - - postgres-passenger - - redis - environment: - NODE_ENV: production - PORT: 3002 - DB_HOST: postgres-passenger - DB_PORT: 5432 - DB_NAME: ${DB_NAME_PASSENGER} - DB_USER: ${DB_USER} - DB_PASSWORD: ${DB_PASSWORD} - REDIS_HOST: redis - expose: - - "3002" - networks: - - edr-network - - edr-passenger-web: - build: - context: ../../ - dockerfile: apps/edr-passenger-web/Dockerfile - container_name: edr-passenger-web - restart: always - depends_on: - - edr-passenger-api - expose: - - "5174" - networks: - - edr-network - - nginx: - image: nginx:1.27-alpine - container_name: edr-nginx - restart: always - ports: - - "80:80" - depends_on: - - edr-freight-web - - edr-freight-api - - edr-passenger-web - - edr-passenger-api - volumes: - - ../../infrastructure/nginx/nginx.conf:/etc/nginx/nginx.conf:ro - networks: - - edr-network diff --git a/infrastructure/nginx/nginx.conf b/infrastructure/nginx/nginx.conf deleted file mode 100644 index 84be85be9..000000000 --- a/infrastructure/nginx/nginx.conf +++ /dev/null @@ -1,63 +0,0 @@ -user nginx; -worker_processes auto; -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - -events { - worker_connections 1024; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - sendfile on; - keepalive_timeout 65; - server_tokens off; - - upstream edr_freight_api { - server edr-freight-api:3001; - } - upstream edr_freight_web { - server edr-freight-web:5173; - } - upstream edr_passenger_api { - server edr-passenger-api:4000; - } - upstream edr_passenger_web { - server edr-passenger-web:5174; - } - - server { - listen 80; - server_name freight.edr.local; - - location /api/ { - proxy_pass http://edr_freight_api/api/; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - - location / { - proxy_pass http://edr_freight_web/; - proxy_set_header Host $host; - } - } - - server { - listen 80; - server_name passenger.edr.local; - - location /api/ { - proxy_pass http://edr_passenger_api/api/; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - - location / { - proxy_pass http://edr_passenger_web/; - proxy_set_header Host $host; - } - } -} diff --git a/infrastructure/nginx/spa.conf b/infrastructure/nginx/spa.conf new file mode 100644 index 000000000..87a5d7688 --- /dev/null +++ b/infrastructure/nginx/spa.conf @@ -0,0 +1,13 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/package.json b/package.json index 5800955ef..a018436f5 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "lint": "turbo run lint", "type-check": "turbo run type-check", "format": "prettier --write \"**/*.{ts,tsx,json,md}\"", + "docker:build": "docker compose build", + "docker:up": "docker compose up -d", "prepare": "husky" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 233b38a03..b6b31dd31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: '@nestjs/swagger': specifier: ^7.4.0 version: 7.4.2(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@prisma/client': + specifier: ^6.19.3 + version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3) '@sendgrid/mail': specifier: ^8.1.0 version: 8.1.6 @@ -116,7 +119,7 @@ importers: version: 11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@5.9.3) '@nestjs/testing': specifier: ^11.1.19 - version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(@nestjs/platform-express@11.1.23) + version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(@nestjs/microservices@11.1.23)(@nestjs/platform-express@11.1.23) '@prisma/client': specifier: ^6.19.3 version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3) diff --git a/scripts/deploy/create-npmrc.sh b/scripts/deploy/create-npmrc.sh new file mode 100644 index 000000000..ce3df826c --- /dev/null +++ b/scripts/deploy/create-npmrc.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Create .npmrc_temp and .npmrc for Docker BuildKit / compose secrets. +# Requires NPM_TOKEN in the environment. + +set -euo pipefail + +if [[ -z "${NPM_TOKEN:-}" ]]; then + echo "NPM_TOKEN is not set" >&2 + exit 1 +fi + +cat < .npmrc_temp +@tria-plc:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=${NPM_TOKEN} +always-auth=true +EOF + +cp .npmrc_temp .npmrc +echo "Created .npmrc_temp and .npmrc for private @tria-plc packages" diff --git a/scripts/deploy/sync-env-from-server.sh b/scripts/deploy/sync-env-from-server.sh new file mode 100644 index 000000000..a0618982c --- /dev/null +++ b/scripts/deploy/sync-env-from-server.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Sync .env files from the self-hosted runner filesystem into the repo. +# +# Usage: +# PROJECT=edr-freight BRANCH=main ./scripts/deploy/sync-env-from-server.sh freight-api freight-portal freight-backoffice +# +# Server layout (one file per service): +# /home/user/environmen///freight-api.env +# /home/user/environmen///freight-portal.env +# /home/user/environmen///freight-web.build.env (optional, exports VITE_API_URL etc.) + +set -euo pipefail + +DEPLOY_USER="${DEPLOY_USER:-tria}" +BRANCH="${BRANCH:?BRANCH is required}" +BRANCH_SLUG="${BRANCH_SLUG:-$(echo "${BRANCH}" | tr "[:upper:]" "[:lower:]" | sed -E "s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//")}" +ENV_ROOT="${ENV_ROOT:-/home/${DEPLOY_USER}/environment/edr/${BRANCH_SLUG}/${PROJECT:?PROJECT is required}}" + +if [[ ! -d "${ENV_ROOT}" ]]; then + echo "Environment directory not found: ${ENV_ROOT}" >&2 + exit 1 +fi + +echo "Using environment directory: ${ENV_ROOT}" + +declare -A SERVICE_ENV_TARGET=( + ["freight-api"]="apps/edr-freight-api/.env" + ["freight-portal"]="apps/edr-freight-web/portal/.env" + ["freight-backoffice"]="apps/edr-freight-web/backoffice/.env" + ["passenger-api"]="apps/edr-passenger-api/.env" + ["passenger-portal"]="apps/edr-passenger-web/portal/.env" + ["passenger-backoffice"]="apps/edr-passenger-web/backoffice/.env" +) + +for service in "$@"; do + src="${ENV_ROOT}/${service}.env" + dest="${SERVICE_ENV_TARGET[${service}]:-}" + + if [[ -z "${dest}" ]]; then + echo "Unknown service: ${service}" >&2 + exit 1 + fi + + if [[ ! -f "${src}" ]]; then + echo "Missing env file: ${src}" >&2 + exit 1 + fi + + mkdir -p "$(dirname "${dest}")" + cp "${src}" "${dest}" + echo "Synced ${src} -> ${dest}" + + port_value=$(sed -n -E 's/^[[:space:]]*PORT[[:space:]]*=[[:space:]]*"?([^"#]+)"?[[:space:]]*(#.*)?$/\1/p' "${src}" | head -n1 | tr -d '[:space:]') + if [[ -z "${port_value}" ]]; then + echo "Missing required PORT in env file: ${src}" >&2 + exit 1 + fi + + if [[ -n "${GITHUB_ENV:-}" ]]; then + service_var=$(echo "${service}" | tr '[:lower:]-' '[:upper:]_') + echo "${service_var}_PORT=${port_value}" >> "${GITHUB_ENV}" + echo "Exported ${service_var}_PORT from ${src}" + fi +done + +# Optional build-time variables (VITE_API_URL, etc.) +# Set BUILD_ENV_FILE=freight-web.build.env or passenger-web.build.env per workflow. +build_env_file="${BUILD_ENV_FILE:-web.build.env}" +build_env="${ENV_ROOT}/${build_env_file}" +if [[ -f "${build_env}" ]]; then + echo "Loading build variables from ${build_env}" + set -a + # shellcheck disable=SC1090 + source "${build_env}" + set +a + + if [[ -n "${GITHUB_ENV:-}" ]]; then + grep -E '^[[:space:]]*export[[:space:]]+[A-Za-z_][A-Za-z0-9_]*=' "${build_env}" \ + | sed -E 's/^[[:space:]]*export[[:space:]]+//' >> "${GITHUB_ENV}" + echo "Wrote build variables to GITHUB_ENV" + fi +fi