diff --git a/.forgejo/workflows/addin.yml b/.forgejo/workflows/addin.yml index 713a135..42a8492 100644 --- a/.forgejo/workflows/addin.yml +++ b/.forgejo/workflows/addin.yml @@ -5,15 +5,17 @@ on: branches: - main - master + tags: + - "addin-*.*.*" paths: - - 'addin/**' - - 'docker-compose.yml' - - '.forgejo/workflows/addin.yml' + - "addin/**" + - "docker-compose.yml" + - ".forgejo/workflows/addin.yml" pull_request: paths: - - 'addin/**' - - 'docker-compose.yml' - - '.forgejo/workflows/addin.yml' + - "addin/**" + - "docker-compose.yml" + - ".forgejo/workflows/addin.yml" permissions: contents: read @@ -33,7 +35,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: "20" - name: Install dependencies run: npm install --no-fund --no-audit diff --git a/.forgejo/workflows/backend.yml b/.forgejo/workflows/backend.yml index 3d6df29..23b044d 100644 --- a/.forgejo/workflows/backend.yml +++ b/.forgejo/workflows/backend.yml @@ -1,36 +1,40 @@ -name: Deploy DocEdit Backend CI +name: Backend CI + Docker Deploy on: push: + branches: + - main + - master tags: - "backend-*.*.*" + paths: + - "backend/**" + - "docker-compose.yml" + - ".forgejo/workflows/backend.yml" + pull_request: + paths: + - "backend/**" + - "docker-compose.yml" + - ".forgejo/workflows/backend.yml" workflow_dispatch: - env: - # Container Registry - REGISTRY_URL: ${{ vars.CONTAINER_REGISTRY }} - REGISTRY_USERNAME: ${{ vars.CONTAINER_REGISTRY_USERNAME }} - REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} +permissions: + contents: read - # Docker Image - WEB_IMAGE_NAME: chamomind/ecourt-note - IMAGE_TAG: ${{ github.ref_name }} - - # SSH Deployment +env: + REGISTRY: ${{ vars.CONTAINER_REGISTRY || 'ghcr.io' }} + IMAGE_NAME: ${{ vars.IMAGE_NAMESPACE }}/word-versioning-backend + IMAGE_TAG: ${{ github.ref_name || 'latest' }} DEPLOY_HOST: ${{ vars.SSH_DEPLOY_HOST }} DEPLOY_USER: ${{ vars.SSH_DEPLOY_USER }} - # Notifications - DISCORD_WEBHOOK_URL: ${{ vars.DISCORD_WEBHOOK_URL }} - NEXT_PUBLIC_APP_URL: ${{ vars.NEXT_PUBLIC_APP_URL }} - jobs: build-and-test: name: Build and test backend runs-on: ubuntu-latest timeout-minutes: 30 env: - DOTNET_CLI_TELEMETRY_OPTOUT: '1' + DOTNET_CLI_TELEMETRY_OPTOUT: "1" steps: - name: Checkout repository uses: actions/checkout@v4 @@ -38,7 +42,7 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: "8.0.x" - name: Restore solution run: dotnet restore backend/WordVersioning.slnx @@ -48,3 +52,51 @@ jobs: - name: Run tests run: dotnet test backend/WordVersioning.slnx --configuration Release --no-build --logger "trx;LogFileName=backend-tests.trx" + + docker-build: + name: Build and push backend image + needs: build-and-test + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ vars.CONTAINER_REGISTRY_USERNAME }} + password: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + + - name: Build and push backend image + uses: docker/build-push-action@v6 + with: + context: . + file: ./backend/WordVersioning.Api/Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + deploy: + name: Deploy backend container + needs: docker-build + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - name: Deploy over SSH + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ env.DEPLOY_HOST }} + username: ${{ env.DEPLOY_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + set -e + echo "${{ secrets.CONTAINER_REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u "${{ vars.CONTAINER_REGISTRY_USERNAME }}" --password-stdin + docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + docker compose -f /opt/word-versioning/docker-compose.prod.yml up -d api + docker image prune -f diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..dcb6163 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,99 @@ +name: Deploy Containers + +on: + push: + tags: + - "addin-*.*.*" + - "backend-*.*.*" + - "web-*.*.*" + workflow_dispatch: + +permissions: + contents: read + +env: + REGISTRY: ${{ vars.CONTAINER_REGISTRY || 'ghcr.io' }} + BACKEND_IMAGE: ${{ vars.IMAGE_NAMESPACE }}/word-versioning-backend + WEB_IMAGE: ${{ vars.IMAGE_NAMESPACE }}/word-versioning-web + BACKEND_TAG: ${{ github.ref_name || 'latest' }} + WEB_TAG: ${{ github.ref_name || 'latest' }} + DEPLOY_HOST: ${{ vars.SSH_DEPLOY_HOST }} + DEPLOY_USER: ${{ vars.SSH_DEPLOY_USER }} + +jobs: + build-and-push-backend: + name: Build and push backend image + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ vars.CONTAINER_REGISTRY_USERNAME }} + password: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + + - name: Build backend image + uses: docker/build-push-action@v6 + with: + context: . + file: ./backend/WordVersioning.Api/Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:${{ env.BACKEND_TAG }} + ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:latest + + build-and-push-web: + name: Build and push web image + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ vars.CONTAINER_REGISTRY_USERNAME }} + password: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + + - name: Build web image + uses: docker/build-push-action@v6 + with: + context: ./web + file: ./web/Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.WEB_IMAGE }}:${{ env.WEB_TAG }} + ${{ env.REGISTRY }}/${{ env.WEB_IMAGE }}:latest + + deploy: + name: Deploy to remote host + needs: + - build-and-push-backend + - build-and-push-web + runs-on: ubuntu-latest + steps: + - name: Deploy via SSH + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ env.DEPLOY_HOST }} + username: ${{ env.DEPLOY_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + set -e + echo "${{ secrets.CONTAINER_REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u "${{ vars.CONTAINER_REGISTRY_USERNAME }}" --password-stdin + export BACKEND_IMAGE="${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:${{ env.BACKEND_TAG }}" + export WEB_IMAGE="${{ env.REGISTRY }}/${{ env.WEB_IMAGE }}:${{ env.WEB_TAG }}" + cd /opt/word-versioning + docker compose -f docker-compose.prod.yml pull + docker compose -f docker-compose.prod.yml up -d --remove-orphans + docker image prune -f diff --git a/.forgejo/workflows/web.yml b/.forgejo/workflows/web.yml index fa94fd2..e7dce29 100644 --- a/.forgejo/workflows/web.yml +++ b/.forgejo/workflows/web.yml @@ -5,15 +5,17 @@ on: branches: - main - master + tags: + - "web-*.*.*" paths: - - 'web/**' - - 'docker-compose.yml' - - '.forgejo/workflows/web.yml' + - "web/**" + - "docker-compose.yml" + - ".forgejo/workflows/web.yml" pull_request: paths: - - 'web/**' - - 'docker-compose.yml' - - '.forgejo/workflows/web.yml' + - "web/**" + - "docker-compose.yml" + - ".forgejo/workflows/web.yml" permissions: contents: read @@ -33,7 +35,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: "20" - name: Install dependencies run: npm install --no-fund --no-audit diff --git a/backend/WordVersioning.Api/Dockerfile b/backend/WordVersioning.Api/Dockerfile new file mode 100644 index 0000000..d22e800 --- /dev/null +++ b/backend/WordVersioning.Api/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +COPY backend/WordVersioning.Api/WordVersioning.Api.csproj ./backend/WordVersioning.Api/ +COPY backend/WordVersioning.Api.IntegrationTests/WordVersioning.Api.IntegrationTests.csproj ./backend/WordVersioning.Api.IntegrationTests/ +COPY backend/WordVersioning.slnx ./backend/ + +RUN dotnet restore ./backend/WordVersioning.slnx + +COPY backend ./backend +RUN dotnet publish ./backend/WordVersioning.Api/WordVersioning.Api.csproj -c Release -o /app/publish --no-restore + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final +WORKDIR /app + +ENV ASPNETCORE_URLS=http://+:8080 +EXPOSE 8080 + +COPY --from=build /app/publish . +ENTRYPOINT ["dotnet", "WordVersioning.Api.dll"] diff --git a/backend/WordVersioning.Api/build-x86.sh b/backend/WordVersioning.Api/build-x86.sh new file mode 100644 index 0000000..3149f65 --- /dev/null +++ b/backend/WordVersioning.Api/build-x86.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Build x86 Docker Image on Apple Silicon +# This script builds Docker image for x86/amd64 platform + +set -e # Exit on any error + +echo "๐Ÿš€ Building x86 Docker image for KML Games..." + +# Configuration +IMAGE_NAME="kml-games-admin" +TAG="${1:-latest}" +PLATFORM="linux/amd64" + +echo "๐Ÿ“‹ Build Configuration:" +echo " Image Name: $IMAGE_NAME" +echo " Tag: $TAG" +echo " Platform: $PLATFORM" +echo "" + +# Check if buildx is available +if ! docker buildx version > /dev/null 2>&1; then + echo "โŒ Docker Buildx is required but not available." + echo " Please update Docker Desktop to latest version." + exit 1 +fi + +# Create builder if not exists +BUILDER_NAME="multiarch-builder" +if ! docker buildx inspect $BUILDER_NAME > /dev/null 2>&1; then + echo "๐Ÿ”ง Creating multiarch builder..." + docker buildx create --name $BUILDER_NAME --driver docker-container --bootstrap + docker buildx use $BUILDER_NAME +else + echo "โœ… Using existing multiarch builder: $BUILDER_NAME" + docker buildx use $BUILDER_NAME +fi + +# Build the image for x86 platform +echo "๐Ÿ”จ Building Docker image for $PLATFORM..." +docker buildx build \ + --platform $PLATFORM \ + --tag $IMAGE_NAME:$TAG \ + --load \ + . + +echo "" +echo "โœ… Build completed successfully!" +echo "๐Ÿ“ฆ Image: $IMAGE_NAME:$TAG (x86/amd64)" + +# Show image info +echo "" +echo "๐Ÿ” Image Information:" +docker image ls | grep $IMAGE_NAME + +echo "" +echo "๐ŸŽ‰ Ready to use! You can now:" +echo " โ€ข Run locally: docker run -p 5000:8080 $IMAGE_NAME:$TAG" +echo " โ€ข Push to registry: docker push your-registry/$IMAGE_NAME:$TAG" +echo " โ€ข Use in production on x86 servers" \ No newline at end of file diff --git a/backend/WordVersioning.Api/dual-tag-push.sh b/backend/WordVersioning.Api/dual-tag-push.sh new file mode 100644 index 0000000..65e2e7e --- /dev/null +++ b/backend/WordVersioning.Api/dual-tag-push.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +# Dual-Tag Harbor Push +# Push to IP but tag with domain name for external access + +set -e + +IMAGE_NAME="${1:-kml-games-admin}" +TAG="${2:-latest}" +HARBOR_IP="192.168.4.11:9088" +HARBOR_DOMAIN="docker.code-extreme.dev" +PROJECT="kml-games" + +echo "๐Ÿš€ Dual-Tag Harbor Push (IP + Domain)" +echo "====================================" +echo " Local Image: $IMAGE_NAME:$TAG" +echo " Harbor IP: $HARBOR_IP (for push)" +echo " Harbor Domain: $HARBOR_DOMAIN (for external)" +echo " Project: $PROJECT" +echo "" +echo " Internal Path: $HARBOR_IP/$PROJECT/$IMAGE_NAME:$TAG" +echo " External Path: $HARBOR_DOMAIN/$PROJECT/$IMAGE_NAME:$TAG" +echo "" + +# Check if image exists +if ! docker image inspect $IMAGE_NAME:$TAG > /dev/null 2>&1; then + echo "โŒ Image $IMAGE_NAME:$TAG not found!" + echo "" + echo "Available images:" + docker image ls | grep $IMAGE_NAME || echo "No $IMAGE_NAME images found" + exit 1 +fi + +# Login to Harbor IP +echo "๐Ÿ” Logging in to Harbor IP..." +if ! docker login $HARBOR_IP; then + echo "โŒ Login failed!" + echo "๐Ÿ’ก Please login manually: docker login $HARBOR_IP" + exit 1 +fi + +echo "" +echo "๐Ÿท๏ธ Tagging images..." + +# Tag with domain name (for external access) +echo " โ†’ $HARBOR_DOMAIN/$PROJECT/$IMAGE_NAME:$TAG (external)" +docker tag $IMAGE_NAME:$TAG $HARBOR_DOMAIN/$PROJECT/$IMAGE_NAME:$TAG + +# Tag with IP (for push) +echo " โ†’ $HARBOR_IP/$PROJECT/$IMAGE_NAME:$TAG (push)" +docker tag $IMAGE_NAME:$TAG $HARBOR_IP/$PROJECT/$IMAGE_NAME:$TAG + +echo "" +echo "๐Ÿ“ฆ Pushing to Harbor via IP..." +if docker push $HARBOR_IP/$PROJECT/$IMAGE_NAME:$TAG; then + echo "" + echo "โœ… Push successful!" + echo "" + echo "๐ŸŽฏ Image Usage:" + echo "" + echo "๐Ÿ“ Internal Network (same LAN):" + echo " docker login $HARBOR_IP" + echo " docker pull $HARBOR_IP/$PROJECT/$IMAGE_NAME:$TAG" + echo " docker run -p 8080:8080 $HARBOR_IP/$PROJECT/$IMAGE_NAME:$TAG" + echo "" + echo "๐ŸŒ External Network (via domain):" + echo " docker login $HARBOR_DOMAIN" + echo " docker pull $HARBOR_DOMAIN/$PROJECT/$IMAGE_NAME:$TAG" + echo " docker run -p 8080:8080 $HARBOR_DOMAIN/$PROJECT/$IMAGE_NAME:$TAG" + echo "" + echo "๐Ÿ“ Docker Compose (recommended for external):" + echo " services:" + echo " app:" + echo " image: $HARBOR_DOMAIN/$PROJECT/$IMAGE_NAME:$TAG" + echo "" + echo "๐Ÿ” Harbor Web UI:" + echo " URL: http://192.168.4.11:9088/harbor/projects/$PROJECT/repositories" + echo "" + echo "๐Ÿ’ก Both image names point to the same image in Harbor!" +else + echo "" + echo "โŒ Push failed!" + echo "" + echo "๐Ÿ”ง Troubleshooting:" + echo " 1. Ensure project '$PROJECT' exists in Harbor" + echo " 2. Check Harbor permissions" + echo " 3. Verify Harbor connectivity: curl http://192.168.4.11:9088" + echo " 4. Create project via Harbor UI: http://192.168.4.11:9088" + exit 1 +fi + +# Clean up local IP-tagged image (optional) +echo "" +read -p "๐Ÿงน Remove local IP-tagged image? (y/N): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + docker rmi $HARBOR_IP/$PROJECT/$IMAGE_NAME:$TAG > /dev/null 2>&1 || true + echo "โœ… Cleaned up local IP-tagged image" +fi + +echo "" +echo "๐ŸŽ‰ Dual-tag push completed!" +echo " โ†’ Use domain name ($HARBOR_DOMAIN) for external access" +echo " โ†’ Use IP ($HARBOR_IP) for internal/faster access" \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..4f43416 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,78 @@ +version: "3.9" + +services: + api: + image: ${BACKEND_IMAGE:-ghcr.io/your-namespace/word-versioning-backend:latest} + environment: + ASPNETCORE_ENVIRONMENT: Production + ConnectionStrings__Default: Host=${DB_HOST:-db};Port=${DB_PORT:-5432};Database=${DB_NAME:-wordversioning};Username=${DB_USER:-wordversioning};Password=${DB_PASSWORD} + MinIO__Endpoint: ${MINIO_ENDPOINT:-minio:39000} + MinIO__AccessKey: ${MINIO_ROOT_USER} + MinIO__SecretKey: ${MINIO_ROOT_PASSWORD} + MinIO__UseSSL: "false" + Authentication__Authority: ${KEYCLOAK_AUTHORITY:-https://keycloak:8443/realms/word-versioning} + Authentication__ValidAudiences__0: ${AUTH_AUDIENCE_0:-word-addin-officejs} + Authentication__ValidAudiences__1: ${AUTH_AUDIENCE_1:-word-versioning-web} + Cors__AllowedOrigins__0: ${WEB_URL:-http://localhost:5173} + Cors__AllowedOrigins__1: ${ADDIN_URL:-https://localhost:3000} + OpenLinks__Secret: ${OPENLINKS_SECRET} + OpenLinks__BaseUrl: ${OPENLINKS_BASE_URL} + OpenLinks__ExpirySeconds: ${OPENLINKS_EXPIRY_SECONDS:-120} + DevHttps__Enabled: "false" + ports: + - "8080:8080" + depends_on: + - db + - minio + - keycloak + + web: + image: ${WEB_IMAGE:-ghcr.io/your-namespace/word-versioning-web:latest} + environment: + NUXT_PUBLIC_API_BASE_URL: ${NUXT_PUBLIC_API_BASE_URL:-http://api:8080} + NUXT_PUBLIC_KEYCLOAK_AUTHORITY: ${NUXT_PUBLIC_KEYCLOAK_AUTHORITY:-https://keycloak:8443/realms/word-versioning} + NUXT_PUBLIC_KEYCLOAK_CLIENT_ID: ${NUXT_PUBLIC_KEYCLOAK_CLIENT_ID:-word-versioning-web} + NUXT_PUBLIC_OPEN_IN_WORD: ${NUXT_PUBLIC_OPEN_IN_WORD:-on} + ports: + - "5173:3000" + depends_on: + - api + + db: + image: postgres:16 + environment: + POSTGRES_USER: ${DB_USER:-wordversioning} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME:-wordversioning} + volumes: + - postgres_data:/var/lib/postgresql/data + + minio: + image: quay.io/minio/minio:latest + command: server /data --address ":39000" --console-address ":39001" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + volumes: + - minio_data:/data + ports: + - "39000:39000" + - "39001:39001" + + keycloak: + image: quay.io/keycloak/keycloak:26.2 + command: start-dev --import-realm + environment: + KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_BOOTSTRAP_ADMIN_USERNAME} + KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_BOOTSTRAP_ADMIN_PASSWORD} + KC_HTTPS_CERTIFICATE_FILE: /certs/localhost.crt + KC_HTTPS_CERTIFICATE_KEY_FILE: /certs/localhost.key + volumes: + - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json:ro + - ${HOME:-${USERPROFILE}}/.office-addin-dev-certs:/certs:ro + ports: + - "8443:8443" + +volumes: + postgres_data: + minio_data: diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..5886f89 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,18 @@ +FROM node:20-alpine AS build +WORKDIR /app + +COPY package*.json ./ +RUN npm install --no-fund --no-audit + +COPY . . +RUN npm run build + +FROM node:20-alpine AS final +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=3000 +EXPOSE 3000 + +COPY --from=build /app/.output ./.output +CMD ["node", ".output/server/index.mjs"]