From 342ccb63de92a00d055ede17892af11485e1f057 Mon Sep 17 00:00:00 2001 From: SennayT Date: Thu, 28 May 2026 12:52:56 +0300 Subject: [PATCH] modify pipeline to speed up deployment --- .dockerignore | 11 +- .github/workflows/deploy-freight.yml | 71 ++++++++++ .github/workflows/deploy-passenger.yml | 71 ++++++++++ .github/workflows/deploy.yaml | 96 ------------- Dockerfile | 84 ----------- README.md | 120 +++++++++++----- apps/edr-freight-api/.env.example | 14 +- apps/edr-freight-api/Dockerfile | 37 +++++ apps/edr-passenger-api/.env.example | 98 +------------ apps/edr-passenger-api/Dockerfile | 50 +++++++ apps/edr-passenger-api/docker-entrypoint.sh | 11 ++ apps/edr-passenger-api/package.json | 7 +- apps/edr-passenger-api/prisma/seed.ts | 8 +- docker-compose.yaml | 87 ++++++++++-- infrastructure/docker/Dockerfile.web | 37 +++++ infrastructure/docker/docker-compose.dev.yml | 126 ---------------- infrastructure/docker/docker-compose.prod.yml | 134 ------------------ infrastructure/nginx/nginx.conf | 63 -------- infrastructure/nginx/spa.conf | 13 ++ package.json | 2 + pnpm-lock.yaml | 6 +- scripts/deploy/create-npmrc.sh | 19 +++ scripts/deploy/sync-env-from-server.sh | 68 +++++++++ 23 files changed, 561 insertions(+), 672 deletions(-) create mode 100644 .github/workflows/deploy-freight.yml create mode 100644 .github/workflows/deploy-passenger.yml delete mode 100644 .github/workflows/deploy.yaml delete mode 100644 Dockerfile create mode 100644 apps/edr-freight-api/Dockerfile create mode 100644 apps/edr-passenger-api/Dockerfile create mode 100644 apps/edr-passenger-api/docker-entrypoint.sh create mode 100644 infrastructure/docker/Dockerfile.web delete mode 100644 infrastructure/docker/docker-compose.dev.yml delete mode 100644 infrastructure/docker/docker-compose.prod.yml delete mode 100644 infrastructure/nginx/nginx.conf create mode 100644 infrastructure/nginx/spa.conf create mode 100644 scripts/deploy/create-npmrc.sh create mode 100644 scripts/deploy/sync-env-from-server.sh 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-freight.yml b/.github/workflows/deploy-freight.yml new file mode 100644 index 000000000..a09faf599 --- /dev/null +++ b/.github/workflows/deploy-freight.yml @@ -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 diff --git a/.github/workflows/deploy-passenger.yml b/.github/workflows/deploy-passenger.yml new file mode 100644 index 000000000..ae4864fe8 --- /dev/null +++ b/.github/workflows/deploy-passenger.yml @@ -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 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/Dockerfile b/Dockerfile deleted file mode 100644 index b35bd68ed..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 --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;"] diff --git a/README.md b/README.md index 63e3c6b8b..5adb57773 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..bd25c8050 --- /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: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"] diff --git a/apps/edr-passenger-api/.env.example b/apps/edr-passenger-api/.env.example index 75d2df681..adaaee399 100644 --- a/apps/edr-passenger-api/.env.example +++ b/apps/edr-passenger-api/.env.example @@ -1,96 +1,4 @@ -# App -NODE_ENV=development +# Copy to .env for local/docker compose (not committed). PORT=4000 - -# Database (Prisma) -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= \ No newline at end of file +DATABASE_URL=postgresql://user:password@host:5432/edr_passenger +JWT_SECRET=change-me-in-production diff --git a/apps/edr-passenger-api/Dockerfile b/apps/edr-passenger-api/Dockerfile new file mode 100644 index 000000000..425c5a95a --- /dev/null +++ b/apps/edr-passenger-api/Dockerfile @@ -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"] 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 ae10747f5..0331adb9b 100644 --- a/apps/edr-passenger-api/package.json +++ b/apps/edr-passenger-api/package.json @@ -12,7 +12,8 @@ "test:e2e": "jest --config ./test/jest-e2e.json", "type-check": "tsc --noEmit", "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:backfill": "ts-node prisma/backfill-fields.ts", "prisma:verify": "ts-node prisma/verify-backfill.ts" @@ -42,7 +43,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:*", @@ -50,7 +52,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 12b872a0e..618fa4518 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 }); } } @@ -161,7 +161,7 @@ async function seedSchedules(trains: any[], stations: 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) { await prisma.fareRule.deleteMany({ where: { tripId: { in: existingScheduleIds } } }); diff --git a/docker-compose.yaml b/docker-compose.yaml index aa9ec58e2..f6de0d965 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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: 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 + - "3001:3001" + environment: + 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 + - "4000:4000" + 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 diff --git a/infrastructure/docker/Dockerfile.web b/infrastructure/docker/Dockerfile.web new file mode 100644 index 000000000..f51211f7b --- /dev/null +++ b/infrastructure/docker/Dockerfile.web @@ -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;"] 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 28ffde4c1..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: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; - } - } -} 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 30f7e543d..0856ff3f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -305,6 +305,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 @@ -357,9 +360,6 @@ importers: '@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/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': specifier: ^5.0.2 version: 5.0.2 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..09f1eb79f --- /dev/null +++ b/scripts/deploy/sync-env-from-server.sh @@ -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///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}" +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