diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..e56c96e --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,134 @@ +# Manual PROD deploy to Portainer. +# +# This does NOT build — build.yml already builds + pushes the api/web images. +# This workflow (re)applies the deploy/*.stack.yml files to the Portainer Swarm. +# Trigger it by hand from the Actions tab ("Run workflow") and choose: +# - tag: which already-published image tag to ship (default: latest) +# - scope: how much to deploy +# app = web + api only (the usual app release) [default] +# full = db + minio + web + api (bring up / update the whole platform) +# +# cssnr/portainer-stack-deploy-action creates each stack on first run and updates +# it on every run, so no manual stack pre-creation in the Portainer UI. On a +# `full` deploy the db + minio stacks are applied BEFORE the app (the API depends +# on them). db + minio are stateful + pinned to node label jorgecuadros_db=true +# (see their stack files) — re-applying them is idempotent and keeps their data. +# +# Prereqs (once): +# - one swarm node labelled jorgecuadros_db=true (db + minio + api pin there). +# - Gitea repo secrets set (Settings > Actions > Secrets): +# # Portainer +# PORTAINER_URL https://192.168.4.212:9443 +# PORTAINER_API_KEY Portainer access token +# PORTAINER_ENDPOINT_ID 2 (the local Swarm endpoint) +# PORTAINER_APP_STACK_NAME e.g. jorgecuadros-prod-app +# PORTAINER_DB_STACK_NAME e.g. jorgecuadros-prod-db (full only) +# PORTAINER_MINIO_STACK_NAME e.g. jorgecuadros-prod-minio (full only) +# # App runtime +# DATABASE_URL mysql://jorgecuadros:@192.168.4.212:3306/jorgecuadros +# SESSION_SECRET 64-hex (openssl rand -hex 32) +# APP_API_ORIGIN http://192.168.4.212:3001 (browser-facing API URL) +# APP_WEB_ORIGIN http://192.168.4.212:3000 (web public origin, API CORS) +# APP_S3_ENDPOINT http://192.168.4.212:9000 (server-side minio URL) +# # Object storage (app + minio stack) +# MINIO_ROOT_USER minio access key +# MINIO_ROOT_PASSWORD minio secret key +# # Database stack (full only) +# MYSQL_PASSWORD app-user password (matches DATABASE_URL) +# MYSQL_ROOT_PASSWORD mysql root password + +name: Deploy to Portainer + +on: + workflow_dispatch: + inputs: + tag: + description: "Image tag to deploy (latest, sha-, or vX.Y.Z)" + required: true + default: "latest" + scope: + description: "What to deploy" + type: choice + required: true + default: "app" + options: + - app + - full + +env: + REGISTRY: git.mancinas.io + +jobs: + deploy: + name: Deploy (${{ github.event.inputs.scope }}) + runs-on: docker + container: + image: node:18-alpine + steps: + - uses: actions/checkout@v4 + + # --- full only: database --------------------------------------------- + - name: Deploy database stack + if: ${{ github.event.inputs.scope == 'full' }} + uses: cssnr/portainer-stack-deploy-action@v1 + with: + url: ${{ secrets.PORTAINER_URL }} + token: ${{ secrets.PORTAINER_API_KEY }} + name: ${{ secrets.PORTAINER_DB_STACK_NAME }} + file: deploy/jorgecuadros-db.stack.yml + type: file + endpoint_id: ${{ secrets.PORTAINER_ENDPOINT_ID }} + env_data: | + { + "MYSQL_SERVER_ID": "1", + "MYSQL_PORT": "3306", + "MYSQL_DATABASE": "jorgecuadros", + "MYSQL_USER": "jorgecuadros", + "MYSQL_PASSWORD": "${{ secrets.MYSQL_PASSWORD }}", + "MYSQL_ROOT_PASSWORD": "${{ secrets.MYSQL_ROOT_PASSWORD }}" + } + + # --- full only: object storage --------------------------------------- + - name: Deploy minio stack + if: ${{ github.event.inputs.scope == 'full' }} + uses: cssnr/portainer-stack-deploy-action@v1 + with: + url: ${{ secrets.PORTAINER_URL }} + token: ${{ secrets.PORTAINER_API_KEY }} + name: ${{ secrets.PORTAINER_MINIO_STACK_NAME }} + file: deploy/jorgecuadros-minio.stack.yml + type: file + endpoint_id: ${{ secrets.PORTAINER_ENDPOINT_ID }} + env_data: | + { + "MINIO_API_PORT": "9000", + "MINIO_CONSOLE_PORT": "9001", + "MINIO_ROOT_USER": "${{ secrets.MINIO_ROOT_USER }}", + "MINIO_ROOT_PASSWORD": "${{ secrets.MINIO_ROOT_PASSWORD }}" + } + + # --- always: the app (web + api) ------------------------------------- + - name: Deploy app stack + uses: cssnr/portainer-stack-deploy-action@v1 + with: + url: ${{ secrets.PORTAINER_URL }} + token: ${{ secrets.PORTAINER_API_KEY }} + name: ${{ secrets.PORTAINER_APP_STACK_NAME }} + file: deploy/jorgecuadros-app.stack.yml + type: file + pull_image: true + endpoint_id: ${{ secrets.PORTAINER_ENDPOINT_ID }} + env_data: | + { + "APP_TAG": "${{ github.event.inputs.tag }}", + "API_PORT": "3001", + "WEB_PORT": "3000", + "S3_BUCKET": "jorgecuadros-documents", + "API_ORIGIN": "${{ secrets.APP_API_ORIGIN }}", + "WEB_ORIGIN": "${{ secrets.APP_WEB_ORIGIN }}", + "S3_ENDPOINT": "${{ secrets.APP_S3_ENDPOINT }}", + "DATABASE_URL": "${{ secrets.DATABASE_URL }}", + "SESSION_SECRET": "${{ secrets.SESSION_SECRET }}", + "MINIO_ROOT_USER": "${{ secrets.MINIO_ROOT_USER }}", + "MINIO_ROOT_PASSWORD": "${{ secrets.MINIO_ROOT_PASSWORD }}" + } diff --git a/apps/web/package.json b/apps/web/package.json index 35b4274..9c4ce38 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev -p 4500", "build": "next build", "start": "next start", "lint": "next lint" diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index fdf48cd..546ca74 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -7,10 +7,29 @@ export const metadata = { "Plataforma interna unificada de clientes, servicios y seguros.", }; +// The browser talks to the API cross-origin, so it needs the API URL at +// runtime. NEXT_PUBLIC_* would bake it at build time (one URL per image); we +// want the URL to come from the deploy .env instead. So read it here on the +// server per request and inject it as window.__API_ORIGIN__ (see lib/api.ts). +// force-dynamic guarantees process.env is read at request time, never baked +// into a static prerender. +export const dynamic = "force-dynamic"; + export default function RootLayout({ children }: { children: ReactNode }) { + const apiOrigin = + process.env.API_ORIGIN ?? + process.env.NEXT_PUBLIC_API_ORIGIN ?? + "http://localhost:3001"; + return ( + {/* Must run before the app bundle so lib/api.ts sees it at import. */} +