modify pipeline to speed up deployment

This commit is contained in:
SennayT
2026-05-28 12:52:56 +03:00
parent 671d24b81d
commit 342ccb63de
23 changed files with 561 additions and 672 deletions

View File

@@ -1,7 +1,14 @@
**/node_modules **/node_modules
**/dist **/dist
**/.turbo
**/.git
**/.github **/.github
**/.vscode **/.vscode
**/.git **/.idea
**/.env **/.env
.env **/.env.*
!**/.env.example
**/coverage
**/*.tsbuildinfo
**/*.log
.DS_Store

71
.github/workflows/deploy-freight.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
name: Deploy Freight
on:
push:
branches:
- main
- develop
- staging
paths:
- "apps/edr-freight-api/**"
- "apps/edr-freight-web/**"
- "packages/**"
- "infrastructure/docker/Dockerfile.web"
- "infrastructure/nginx/**"
- "docker-compose.yaml"
- "pnpm-lock.yaml"
- "scripts/deploy/**"
- ".github/workflows/deploy-freight.yml"
workflow_dispatch:
concurrency:
group: deploy-freight-${{ github.ref_name }}
cancel-in-progress: true
jobs:
deploy:
name: Build and deploy freight stack
runs-on: self-hosted
env:
PROJECT: edr-freight
BRANCH: ${{ github.ref_name }}
DEPLOY_USER: user
BUILD_ENV_FILE: freight-web.build.env
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 \
freight-api \
freight-portal \
freight-backoffice
- name: Configure npm auth for Docker builds
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: ./scripts/deploy/create-npmrc.sh
- name: Build images
run: |
set -euo pipefail
docker compose build \
freight-api \
freight-portal \
freight-backoffice
- name: Deploy containers
run: |
set -euo pipefail
docker compose up -d \
freight-api \
freight-portal \
freight-backoffice
- name: Remove npm credentials from workspace
if: always()
run: rm -f .npmrc .npmrc_temp

71
.github/workflows/deploy-passenger.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
name: Deploy Passenger
on:
push:
branches:
- main
- develop
- staging
paths:
- "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-passenger.yml"
workflow_dispatch:
concurrency:
group: deploy-passenger-${{ github.ref_name }}
cancel-in-progress: true
jobs:
deploy:
name: Build and deploy passenger stack
runs-on: self-hosted
env:
PROJECT: edr-passenger
BRANCH: ${{ github.ref_name }}
DEPLOY_USER: user
BUILD_ENV_FILE: passenger-web.build.env
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 \
passenger-api \
passenger-portal \
passenger-backoffice
- name: Configure npm auth for Docker builds
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: ./scripts/deploy/create-npmrc.sh
- name: Build images
run: |
set -euo pipefail
docker compose build \
passenger-api \
passenger-portal \
passenger-backoffice
- name: Deploy containers
run: |
set -euo pipefail
docker compose up -d \
passenger-api \
passenger-portal \
passenger-backoffice
- name: Remove npm credentials from workspace
if: always()
run: rm -f .npmrc .npmrc_temp

View File

@@ -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 <<EOF > .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

View File

@@ -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 --log-order grouped
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;"]

120
README.md
View File

@@ -170,7 +170,7 @@ pnpm --filter @edr/passenger-api run prisma:generate
#### Run Migrations #### Run Migrations
```bash ```bash
pnpm --filter @edr/passenger-api run prisma:migrate pnpm --filter @edr/passenger-api run prisma:migrate:dev
``` ```
#### Seed Database #### Seed Database
@@ -523,59 +523,103 @@ pnpm --filter @edr/passenger-api run type-check # TypeScript check
# Database # Database
pnpm --filter @edr/passenger-api run prisma:generate # Generate Prisma client 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 pnpm --filter @edr/passenger-api run prisma:seed # Seed database
``` ```
## 🐳 Docker Deployment ## 🐳 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 ```bash
# From monorepo root # 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 ```bash
docker run -d \ DOCKER_BUILDKIT=1 docker compose build
--name edr-api \ docker compose up -d
-p 4000:4000 \
--env-file apps/edr-passenger-api/.env \
edr-passenger-api
``` ```
### Docker Compose (Recommended) | Service | URL (default) |
```yaml |---------|----------------|
version: '3.8' | Freight API | http://localhost:3001 |
services: | Passenger API | http://localhost:4000 |
postgres: | Freight portal | http://localhost:5173 |
image: postgres:15 | Freight backoffice | http://localhost:5183 |
environment: | Passenger portal | http://localhost:5174 |
POSTGRES_USER: edr | Passenger backoffice | http://localhost:5184 |
POSTGRES_PASSWORD: edr_secret
POSTGRES_DB: edr_passenger
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
api: ### Build a single service
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
volumes: ```bash
postgres_data: 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/<branch>/` |
| [`.github/workflows/deploy-passenger.yml`](.github/workflows/deploy-passenger.yml) | passenger-api, passenger-portal, passenger-backoffice | `/home/user/environmen/edr-passenger/<branch>/` |
**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 ## 🔒 Security Best Practices
1. **Environment Variables** - Never commit `.env` files. Use secrets management in production. 1. **Environment Variables** - Never commit `.env` files. Use secrets management in production.

View File

@@ -1,17 +1,7 @@
# App # Copy to .env for local/docker compose (not committed).
NODE_ENV=development
PORT=3001 PORT=3001
# Database
DB_HOST=localhost DB_HOST=localhost
DB_PORT=5433 DB_PORT=5433
DB_NAME=edr_freight
DB_USER=postgres DB_USER=postgres
DB_PASSWORD= DB_PASSWORD=
DB_NAME=edr_freight
# JWT (provided by external auth package — placeholder only)
JWT_SECRET=
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379

View File

@@ -0,0 +1,37 @@
# syntax=docker/dockerfile:1
# Build from monorepo root: docker build -f apps/edr-freight-api/Dockerfile .
FROM node:22-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:22-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"]

View File

@@ -1,96 +1,4 @@
# App # Copy to .env for local/docker compose (not committed).
NODE_ENV=development
PORT=4000 PORT=4000
DATABASE_URL=postgresql://user:password@host:5432/edr_passenger
# Database (Prisma) JWT_SECRET=change-me-in-production
DATABASE_URL=postgresql://edr:edr_secret@localhost:5432/edr_passenger?schema=edr_passenger
# CORS
FRONTEND_URL=http://localhost:3000
PORTAL_URL=http://localhost:3001
# JWT
JWT_SECRET=edr-platform-secret-change-in-production
JWT_EXPIRES_IN=7d
# SendGrid
SENDGRID_API_KEY=
SENDGRID_FROM_EMAIL=noreply@edr-platform.com
# SMS Configuration
SMS_PROVIDER=twilio
SMS_API_KEY=
# Twilio (if SMS_PROVIDER=twilio)
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_FROM_NUMBER=
# Africa's Talking (if SMS_PROVIDER=africastalking)
AFRICASTALKING_USERNAME=
AFRICASTALKING_FROM=
# Telebirr
TELEBIRR_BASE_URL=
TELEBIRR_WEB_BASE_URL=
TELEBIRR_FABRIC_APP_ID=
TELEBIRR_APP_SECRET=
TELEBIRR_MERCHANT_APP_ID=
TELEBIRR_MERCHANT_CODE=
TELEBIRR_NOTIFY_URL=
TELEBIRR_RETURN_URL=
TELEBIRR_TIMEOUT_EXPRESS=15m
TELEBIRR_PRIVATE_KEY=
TELEBIRR_PUBLIC_KEY=
TELEBIRR_INSECURE_TLS=false
# CBE Birr
CBE_BASE_URL=
CBE_MERCHANT_ID=
CBE_SECRET_KEY=
CBE_NOTIFY_URL=
CBE_RETURN_URL=
# eBirr
EBIRR_BASE_URL=
EBIRR_MERCHANT_CODE=
EBIRR_SECRET_KEY=
EBIRR_NOTIFY_URL=
EBIRR_RETURN_URL=
# Card Gateway (Stripe-like)
CARD_BASE_URL=
CARD_API_KEY=
CARD_WEBHOOK_SECRET=
CARD_WEBHOOK_URL=
CARD_RETURN_URL=
# Waafi (Djibouti Mobile Money)
WAAFI_BASE_URL=https://api.waafipay.net
WAAFI_MERCHANT_UID=
WAAFI_API_USER_ID=
WAAFI_API_KEY=
WAAFI_NOTIFY_URL=
WAAFI_RETURN_URL=
# Payment Configuration
PAYMENT_PROVIDERS_ENABLED=TELEBIRR,CBE_BIRR,EBIRR,CARD,WALLET,WAAFI
# Session Configuration
SESSION_INACTIVITY_MINUTES=30
# i18n Configuration
DEFAULT_LOCALE=en
SUPPORTED_LOCALES=en,am,fr,om
# Corporate IAM Configuration (for back-office authentication)
IAM_ENABLED=false
IAM_API_URL=https://iam.tria-plc.com/api
IAM_API_KEY=
# Verifayda 2.0 Configuration (Ethiopian National ID Verification)
VERIFAYDA_ENABLED=false
VERIFAYDA_API_URL=https://api.verifayda.gov.et/v2
VERIFAYDA_API_KEY=
GITHUB_PACKAGE_TOKEN=

View File

@@ -0,0 +1,50 @@
# 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:22-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 \
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:22-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"]

View File

@@ -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 "$@"

View File

@@ -12,7 +12,8 @@
"test:e2e": "jest --config ./test/jest-e2e.json", "test:e2e": "jest --config ./test/jest-e2e.json",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"prisma:generate": "prisma generate", "prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev", "prisma:migrate": "prisma migrate deploy",
"prisma:migrate:dev": "prisma migrate dev",
"prisma:seed": "ts-node prisma/seed.ts", "prisma:seed": "ts-node prisma/seed.ts",
"prisma:backfill": "ts-node prisma/backfill-fields.ts", "prisma:backfill": "ts-node prisma/backfill-fields.ts",
"prisma:verify": "ts-node prisma/verify-backfill.ts" "prisma:verify": "ts-node prisma/verify-backfill.ts"
@@ -42,7 +43,8 @@
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.0", "swagger-ui-express": "^5.0.0",
"tsconfig-paths": "^4.2.0" "tsconfig-paths": "^4.2.0",
"@prisma/client": "^6.19.3"
}, },
"devDependencies": { "devDependencies": {
"@edr/eslint-config": "workspace:*", "@edr/eslint-config": "workspace:*",
@@ -50,7 +52,6 @@
"@nestjs/cli": "^11.0.21", "@nestjs/cli": "^11.0.21",
"@nestjs/schematics": "^11.1.0", "@nestjs/schematics": "^11.1.0",
"@nestjs/testing": "^11.1.19", "@nestjs/testing": "^11.1.19",
"@prisma/client": "^6.19.3",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
"@types/node": "^20.10.6", "@types/node": "^20.10.6",

View File

@@ -1,4 +1,4 @@
import { PrismaClient, SeatKind } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@@ -136,11 +136,11 @@ async function seedCoachesAndSeats(seatClasses: any[]) {
col, col,
label: `${row}${col}`, label: `${row}${col}`,
seatNumber: `${config.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 });
} }
} }
@@ -161,7 +161,7 @@ async function seedSchedules(trains: any[], stations: any[]) {
const existingScheduleIds = (await prisma.trainSchedule.findMany({ const existingScheduleIds = (await prisma.trainSchedule.findMany({
where: { trainId: { in: [train301.id, train302.id, train303.id] } }, where: { trainId: { in: [train301.id, train302.id, train303.id] } },
select: { id: true }, select: { id: true },
})).map((s) => s.id); })).map((s: { id: string }) => s.id);
if (existingScheduleIds.length > 0) { if (existingScheduleIds.length > 0) {
await prisma.fareRule.deleteMany({ where: { tripId: { in: existingScheduleIds } } }); await prisma.fareRule.deleteMany({ where: { tripId: { in: existingScheduleIds } } });

View File

@@ -1,21 +1,84 @@
# 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: services:
freight-api: freight-api:
build: build:
context: . context: .
dockerfile: ./Dockerfile dockerfile: apps/edr-freight-api/Dockerfile
target: freight-api secrets:
env_file: - npmrc
- .env
ports: ports:
- ${PORT}:${PORT} - "3001:3001"
restart: unless-stopped environment:
PORT: "3001"
env_file:
- apps/edr-freight-api/.env
passenger-api: passenger-api:
build: build:
context: . context: .
dockerfile: ./Dockerfile dockerfile: apps/edr-passenger-api/Dockerfile
target: passenger-api
env_file:
- .env
ports: ports:
- ${PORT}:${PORT} - "4000:4000"
restart: unless-stopped environment:
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:
- "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:
- "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:
- "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:
- "5184:80"
secrets:
npmrc:
file: .npmrc

View File

@@ -0,0 +1,37 @@
# 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:22-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 \
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;"]

View File

@@ -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

View File

@@ -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

View File

@@ -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:3002;
}
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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -14,6 +14,8 @@
"lint": "turbo run lint", "lint": "turbo run lint",
"type-check": "turbo run type-check", "type-check": "turbo run type-check",
"format": "prettier --write \"**/*.{ts,tsx,json,md}\"", "format": "prettier --write \"**/*.{ts,tsx,json,md}\"",
"docker:build": "docker compose build",
"docker:up": "docker compose up -d",
"prepare": "husky" "prepare": "husky"
}, },
"devDependencies": { "devDependencies": {

6
pnpm-lock.yaml generated
View File

@@ -305,6 +305,9 @@ importers:
'@nestjs/swagger': '@nestjs/swagger':
specifier: ^7.4.0 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) 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': '@sendgrid/mail':
specifier: ^8.1.0 specifier: ^8.1.0
version: 8.1.6 version: 8.1.6
@@ -357,9 +360,6 @@ importers:
'@nestjs/testing': '@nestjs/testing':
specifier: ^11.1.19 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/microservices@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)
'@types/bcrypt': '@types/bcrypt':
specifier: ^5.0.2 specifier: ^5.0.2
version: 5.0.2 version: 5.0.2

View File

@@ -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 <<EOF > .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"

View File

@@ -0,0 +1,68 @@
#!/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/<project>/<branch>/freight-api.env
# /home/user/environmen/<project>/<branch>/freight-portal.env
# /home/user/environmen/<project>/<branch>/freight-web.build.env (optional, exports VITE_API_URL etc.)
set -euo pipefail
DEPLOY_USER="${DEPLOY_USER:-tria}"
ENV_ROOT="${ENV_ROOT:-/home/${DEPLOY_USER}/environmen/${PROJECT:?PROJECT is required}/${BRANCH:?BRANCH 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}"
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