diff --git a/.github/workflows/deploy-login-hanko.yml b/.github/workflows/deploy-login-hanko.yml new file mode 100644 index 000000000..f56807ad4 --- /dev/null +++ b/.github/workflows/deploy-login-hanko.yml @@ -0,0 +1,116 @@ +name: Deploy login-hanko to testlogin.fair.hotosm.org + +on: + push: + branches: + - login_hanko + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_PREFIX: hotosm/fair + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push backend + run: | + docker build -t ghcr.io/${{ env.IMAGE_PREFIX }}/backend:login-hanko \ + -f backend/Dockerfile.API \ + ./backend + docker push ghcr.io/${{ env.IMAGE_PREFIX }}/backend:login-hanko + + - name: Build and push frontend + run: | + docker build -t ghcr.io/${{ env.IMAGE_PREFIX }}/frontend:login-hanko \ + --target prod \ + --build-arg VITE_AUTH_PROVIDER=${{ vars.AUTH_PROVIDER || 'hanko' }} \ + --build-arg VITE_HANKO_URL=https://dev.login.hotosm.org \ + --build-arg VITE_BASE_API_URL=https://testlogin.fair.hotosm.org/api/v1/ \ + -f frontend/Dockerfile.prod \ + ./frontend + docker push ghcr.io/${{ env.IMAGE_PREFIX }}/frontend:login-hanko + + deploy: + needs: build + runs-on: ubuntu-latest + environment: testlogin + steps: + - uses: actions/checkout@v4 + - uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.EC2_SSH_KEY }} + + - name: Add host to known_hosts + run: ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts + + - name: Deploy + env: + EC2_HOST: ${{ secrets.EC2_HOST }} + EC2_USER: ${{ secrets.EC2_USER }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_ACTOR: ${{ github.actor }} + COOKIE_SECRET: ${{ secrets.COOKIE_SECRET }} + AUTH_PROVIDER: ${{ vars.AUTH_PROVIDER || 'hanko' }} + run: | + ssh $EC2_USER@$EC2_HOST << ENDSSH + set -e + + # Ensure Traefik is running + if ! docker ps | grep -q traefik; then + echo "ERROR: Traefik not running. Run setup-test-server.sh first." + exit 1 + fi + + APP_DIR="/opt/fair-test" + + # Setup inicial si no existe + if [ ! -d "\$APP_DIR" ]; then + sudo mkdir -p \$APP_DIR + sudo chown \$USER:\$USER \$APP_DIR + git clone -b login_hanko https://github.com/hotosm/fAIr.git \$APP_DIR + echo "Cloned repository" + fi + + cd \$APP_DIR + + # Pull latest changes + git fetch origin login_hanko + git reset --hard origin/login_hanko + echo "Updated to latest login_hanko" + + # Create .env with secrets + cat > .env << EOF + POSTGRES_USER=fair + POSTGRES_PASSWORD=fair + POSTGRES_DB=fair + SECRET_KEY=test-secret-key-for-testing-only-min-32-chars + COOKIE_SECRET=${COOKIE_SECRET} + AUTH_PROVIDER=${AUTH_PROVIDER} + EOF + echo "Created .env" + + # Login to GHCR + echo "${GH_TOKEN}" | docker login ghcr.io -u ${GH_ACTOR} --password-stdin + + # Pull and deploy + docker compose -f compose.test.yaml pull + docker compose -f compose.test.yaml up -d --force-recreate + + # Cleanup + docker image prune -af + + echo "Deployment complete" + ENDSSH diff --git a/backend/Dockerfile.API b/backend/Dockerfile.API index b9f12517b..efd30b074 100644 --- a/backend/Dockerfile.API +++ b/backend/Dockerfile.API @@ -15,6 +15,7 @@ ENV UV_LINK_MODE=copy ENV UV_PYTHON_DOWNLOADS=0 RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ gdal-bin \ libgdal-dev \ gcc \ diff --git a/compose.test.yaml b/compose.test.yaml new file mode 100644 index 000000000..698a78e90 --- /dev/null +++ b/compose.test.yaml @@ -0,0 +1,117 @@ +# compose.test.yaml - Test environment for login-hanko branch +# Deploy to: testlogin.fair.hotosm.org +# Requires: Traefik running in /opt/traefik with hotosm-test network + +services: + # Frontend syncs built files to a shared volume + frontend: + image: ghcr.io/hotosm/fair/frontend:login-hanko + restart: unless-stopped + volumes: + - frontend-html:/frontend_html + networks: + - internal + + # Nginx serves the static files + nginx: + image: nginx:alpine + restart: unless-stopped + depends_on: + - frontend + volumes: + - frontend-html:/usr/share/nginx/html:ro + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - hotosm-test + labels: + - "traefik.enable=true" + - "traefik.http.routers.fair-frontend.rule=Host(`testlogin.fair.hotosm.org`) && !PathPrefix(`/api`)" + - "traefik.http.routers.fair-frontend.entrypoints=websecure" + - "traefik.http.routers.fair-frontend.tls.certresolver=letsencrypt" + - "traefik.http.services.fair-frontend.loadbalancer.server.port=80" + + backend: + image: ghcr.io/hotosm/fair/backend:login-hanko + restart: unless-stopped + depends_on: + - db + - redis + environment: + - DATABASE_URL=postgis://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} + - AUTH_PROVIDER=${AUTH_PROVIDER:-hanko} + - HANKO_API_URL=https://dev.login.hotosm.org + - JWT_ISSUER=https://dev.login.hotosm.org + - COOKIE_SECRET=${COOKIE_SECRET} + - COOKIE_DOMAIN=.hotosm.org + - SECRET_KEY=${SECRET_KEY} + - REDIS_HOST=redis + - REDIS_PORT=6379 + - CELERY_BROKER_URL=redis://redis:6379/0 + - CELERY_RESULT_BACKEND=redis://redis:6379/0 + - DEBUG=True + - ALLOWED_HOSTS=testlogin.fair.hotosm.org + - ALLOWED_ORIGINS=https://testlogin.fair.hotosm.org,https://dev.login.hotosm.org + - FRONTEND_URL=https://testlogin.fair.hotosm.org + - LOGIN_URL=https://dev.login.hotosm.org + - ADMIN_EMAILS=hernangigena@gmail.com,justina@animus.com.ar,andreatchirillano@hotmail.com,emilio.mariscal@hotosm.org + command: gunicorn --bind 0.0.0.0:8000 --workers 2 fairproject.wsgi:application + networks: + - hotosm-test + - internal + labels: + - "traefik.enable=true" + - "traefik.http.routers.fair-backend.rule=Host(`testlogin.fair.hotosm.org`) && PathPrefix(`/api`)" + - "traefik.http.routers.fair-backend.entrypoints=websecure" + - "traefik.http.routers.fair-backend.tls.certresolver=letsencrypt" + - "traefik.http.services.fair-backend.loadbalancer.server.port=8000" + + db: + image: postgis/postgis:16-3.4 + restart: unless-stopped + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + volumes: + - fair-db:/var/lib/postgresql/data + networks: + - internal + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + restart: unless-stopped + networks: + - internal + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + migrations: + image: ghcr.io/hotosm/fair/backend:login-hanko + command: python manage.py migrate --noinput + depends_on: + db: + condition: service_healthy + environment: + - DATABASE_URL=postgis://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} + - SECRET_KEY=${SECRET_KEY} + - DEBUG=True + - ALLOWED_HOSTS=testlogin.fair.hotosm.org + networks: + - internal + +networks: + hotosm-test: + external: true + internal: + +volumes: + fair-db: + frontend-html: diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev index a1088ec6e..7b41e8b47 100644 --- a/frontend/Dockerfile.dev +++ b/frontend/Dockerfile.dev @@ -11,6 +11,10 @@ RUN echo "Installing dependencies..." && \ COPY . . +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + EXPOSE 3000 -CMD ["pnpm", "run", "dev", "--host", "0.0.0.0"] \ No newline at end of file +ENTRYPOINT ["/entrypoint.sh"] +CMD ["pnpm", "run", "dev", "--port", "3000", "--host", "0.0.0.0"] diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod index 4bc2eaa01..8f2f19981 100644 --- a/frontend/Dockerfile.prod +++ b/frontend/Dockerfile.prod @@ -1,17 +1,33 @@ -## docker build -t fair-frontend:latest -f Dockerfile.prod . && container_id=$(docker create fair-frontend:latest) && docker cp $container_id:/app/dist ./dist && docker rm $container_id +# Build stage +FROM node:22-slim AS build +ARG VITE_AUTH_PROVIDER +ARG VITE_HANKO_URL +ARG VITE_BASE_API_URL + +ENV VITE_AUTH_PROVIDER=${VITE_AUTH_PROVIDER} \ + VITE_HANKO_URL=${VITE_HANKO_URL} \ + VITE_BASE_API_URL=${VITE_BASE_API_URL} -# Build stage -FROM node:22 AS builder WORKDIR /app + COPY package.json pnpm-lock.yaml ./ -# COPY .env ./.env -RUN npm install -g pnpm -RUN pnpm install --force +RUN corepack enable && corepack prepare pnpm@9 --activate +RUN pnpm install --frozen-lockfile + COPY . . RUN pnpm run build -# Export stage -FROM alpine:latest AS exporter + +# Production stage - copies dist to shared volume +FROM docker.io/rclone/rclone:1 AS prod + +VOLUME /frontend_html + +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh + WORKDIR /app -COPY --from=builder /app/dist ./dist +COPY --from=build /app/dist /app + +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/frontend/docker-entrypoint.sh b/frontend/docker-entrypoint.sh new file mode 100644 index 000000000..42510ab3d --- /dev/null +++ b/frontend/docker-entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +set -e + +# Copy frontend to attached volume +echo "Syncing files from /app --> /frontend_html" +rclone sync /app /frontend_html +echo "Updating directory permissions 101:101 (nginx)." +chown -R 101:101 /frontend_html +echo "Sync done." + +# Successful exit (stop container) +exit 0 diff --git a/frontend/entrypoint.sh b/frontend/entrypoint.sh new file mode 100755 index 000000000..0f9462b2f --- /dev/null +++ b/frontend/entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/sh +set -e + +CACHE_FILE="/app/.package-checksum" + +# Calculate checksum of package.json and pnpm-lock.yaml +current_checksum=$(cat /app/package.json /app/pnpm-lock.yaml 2>/dev/null | md5sum | cut -d' ' -f1) + +# Check if we need to reinstall +if [ -f "$CACHE_FILE" ]; then + cached_checksum=$(cat "$CACHE_FILE") + if [ "$current_checksum" != "$cached_checksum" ]; then + echo "📦 package.json or pnpm-lock.yaml changed, reinstalling dependencies..." + pnpm install --force + echo "$current_checksum" > "$CACHE_FILE" + echo "✅ Dependencies updated" + fi +else + # First run, save checksum + echo "$current_checksum" > "$CACHE_FILE" +fi + +exec "$@" diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 000000000..afd888855 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,17 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # SPA routing - serve index.html for all routes + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +}