diff --git a/.github/workflows/admin-web-tests.yml b/.github/workflows/admin-web-tests.yml new file mode 100644 index 0000000..098793e --- /dev/null +++ b/.github/workflows/admin-web-tests.yml @@ -0,0 +1,41 @@ +name: Admin Web Tests + +on: + workflow_call: + inputs: + node-version: + type: string + default: '22' + +jobs: + test: + name: Test + runs-on: ubuntu-latest + timeout-minutes: 10 + defaults: + run: + working-directory: admin-web + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: 'npm' + cache-dependency-path: admin-web/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Type check + run: npx tsc --noEmit + + - name: Run unit tests + run: npx vitest run + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Run E2E tests + run: npx playwright test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1948f33..f50a10e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,34 +38,9 @@ jobs: name: Admin Web CI needs: changes if: needs.changes.outputs.admin-web == 'true' - runs-on: ubuntu-latest - defaults: - run: - working-directory: admin-web - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - cache-dependency-path: admin-web/package-lock.json - - - name: Install dependencies - run: npm ci - - - name: Type check - run: npx tsc --noEmit - - - name: Run unit tests - run: npx vitest run - - - name: Install Playwright browsers - run: npx playwright install --with-deps chromium - - - name: Run E2E tests - run: npx playwright test + uses: ./.github/workflows/admin-web-tests.yml + with: + node-version: '22' # ── YouTube Worker: Node.js microservice ── youtube-worker-check: diff --git a/.github/workflows/docker-admin-web.yml b/.github/workflows/docker-admin-web.yml new file mode 100644 index 0000000..977d2d3 --- /dev/null +++ b/.github/workflows/docker-admin-web.yml @@ -0,0 +1,154 @@ +name: Docker Admin Web + +on: + push: + branches: [main] + paths: + - 'admin-web/**' + - '.github/workflows/docker-admin-web.yml' + workflow_dispatch: + +concurrency: + group: docker-admin-web-${{ github.ref }} + cancel-in-progress: false + +jobs: + admin-web-test: + uses: ./.github/workflows/admin-web-tests.yml + + semantic-release: + name: Semantic Release + needs: admin-web-test + runs-on: ubuntu-latest + timeout-minutes: 5 + defaults: + run: + working-directory: admin-web + outputs: + released: ${{ steps.check-release.outputs.released }} + version: ${{ steps.check-release.outputs.version }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: admin-web/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Record version before release + id: before + run: echo "version=$(node -p "require('./version.json').version")" >> $GITHUB_OUTPUT + + - name: Run semantic-release + env: + GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} + GITEA_URL: https://gitea.1000co.fr + run: npx semantic-release + + - name: Check if released + id: check-release + run: | + VERSION=$(node -p "require('./version.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + if [ "${{ steps.before.outputs.version }}" != "$VERSION" ]; then + echo "released=true" >> $GITHUB_OUTPUT + else + echo "released=false" >> $GITHUB_OUTPUT + fi + + build-and-push: + name: Build & Push Docker Image + needs: semantic-release + if: needs.semantic-release.outputs.released == 'true' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Install Docker CLI + run: apt-get update -qq && apt-get install -y -qq docker.io + + - name: Login to Gitea registry + run: | + echo "${{ secrets.DOCKER_PASSWORD }}" | \ + docker login gitea.1000co.fr \ + -u "${{ secrets.DOCKER_USERNAME }}" \ + --password-stdin + + - name: Build and push + run: | + U="${{ secrets.DOCKER_USERNAME }}" + V="${{ needs.semantic-release.outputs.version }}" + docker build \ + --file ./admin-web/Dockerfile \ + --tag "gitea.1000co.fr/$U/tabatago-admin-web:$V" \ + --tag "gitea.1000co.fr/$U/tabatago-admin-web:latest" \ + ./admin-web + docker push "gitea.1000co.fr/$U/tabatago-admin-web:$V" + docker push "gitea.1000co.fr/$U/tabatago-admin-web:latest" + + deploy: + name: Deploy to Portainer + needs: [semantic-release, build-and-push] + if: needs.semantic-release.outputs.released == 'true' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + + - name: Install deps + run: apt-get update -qq && apt-get install -y -qq gettext-base jq curl + + - name: Deploy stack to Portainer + env: + PORTAINER_TOKEN: ${{ secrets.PORTAINER_ACCESS_TOKEN }} + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + VERSION: ${{ needs.semantic-release.outputs.version }} + run: | + set -e + + # Substitute env vars in compose file + export DOCKER_USERNAME VERSION + COMPOSE=$(envsubst '$DOCKER_USERNAME $VERSION' < admin-web/docker-compose.portainer.yml) + + # Authenticate and discover Docker endpoint (first endpoint = local Docker) + ENDPOINT_ID=$(curl -sSf -H "X-API-Key: $PORTAINER_TOKEN" \ + "https://portainer.1000co.fr/api/endpoints" | jq -r '.[0].Id') + echo "Endpoint ID: $ENDPOINT_ID" + + # Check if "tabatago-admin-web" stack already exists + STACK_ID=$(curl -sSf -H "X-API-Key: $PORTAINER_TOKEN" \ + "https://portainer.1000co.fr/api/stacks" | jq -r '.[] | select(.Name == "tabatago-admin-web") | .Id') + + if [ -n "$STACK_ID" ] && [ "$STACK_ID" != "null" ]; then + echo "Stack tabatago-admin-web exists (ID: $STACK_ID), updating compose content..." + HTTP_RESPONSE=$(curl -s -w '\n%{http_code}' \ + -X PUT \ + -H "X-API-Key: $PORTAINER_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg content "$COMPOSE" '{stackFileContent: $content, prune: false, pullImage: true}')" \ + "https://portainer.1000co.fr/api/stacks/$STACK_ID?endpointId=$ENDPOINT_ID" || true) + HTTP_BODY=$(echo "$HTTP_RESPONSE" | sed '$d') + HTTP_CODE=$(echo "$HTTP_RESPONSE" | tail -1) + + if [ -z "$HTTP_CODE" ] || [ "$HTTP_CODE" -ge 400 ]; then + echo "Portainer API error (HTTP ${HTTP_CODE:-connection failed}):" + echo "$HTTP_BODY" + exit 1 + fi + echo "Stack updated. Portainer will redeploy with new images." + else + echo "⚠️ Stack tabatago-admin-web not found in Portainer." + echo "Create it manually in Portainer first: Stacks → Add stack → paste compose content from admin-web/docker-compose.portainer.yml" + exit 1 + fi diff --git a/admin-web/.dockerignore b/admin-web/.dockerignore new file mode 100644 index 0000000..bf2434d --- /dev/null +++ b/admin-web/.dockerignore @@ -0,0 +1,12 @@ +node_modules +.next +.git +.gitignore +README.md +.npm +.pnp +.DS_Store +*.md +coverage +playwright-report +test-results diff --git a/admin-web/.releaserc.json b/admin-web/.releaserc.json new file mode 100644 index 0000000..a64af31 --- /dev/null +++ b/admin-web/.releaserc.json @@ -0,0 +1,14 @@ +{ + "branches": ["main"], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + ["@semantic-release/exec", { + "prepareCmd": "echo '{\"version\": \"${nextRelease.version}\"}' > version.json" + }], + ["@semantic-release/git", { + "assets": ["version.json"], + "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}" + }] + ] +} diff --git a/admin-web/Dockerfile b/admin-web/Dockerfile new file mode 100644 index 0000000..8f99341 --- /dev/null +++ b/admin-web/Dockerfile @@ -0,0 +1,29 @@ +FROM node:22-bookworm-slim AS build + +WORKDIR /app + +COPY package.json package-lock.json ./ + +RUN npm ci + +COPY . . + +RUN npm run build + +FROM node:22-bookworm-slim AS production + +WORKDIR /app + +ENV NODE_ENV=production + +COPY --from=build /app/public ./public +COPY --from=build /app/.next/standalone ./ +COPY --from=build /app/.next/static ./.next/static + +RUN groupadd -g 1001 appgroup && useradd -u 1001 -g appgroup -s /bin/sh -M nodeuser + +USER nodeuser + +EXPOSE 3000 + +ENTRYPOINT ["node", "server.js"] diff --git a/admin-web/docker-compose.portainer.yml b/admin-web/docker-compose.portainer.yml new file mode 100644 index 0000000..390587d --- /dev/null +++ b/admin-web/docker-compose.portainer.yml @@ -0,0 +1,16 @@ +services: + admin-web: + image: gitea.1000co.fr/${DOCKER_USERNAME}/tabatago-admin-web:${VERSION:-latest} + restart: unless-stopped + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL} + - NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY} + networks: + - supabase_supabase-network + +networks: + supabase_supabase-network: + external: true diff --git a/admin-web/next.config.ts b/admin-web/next.config.ts index e9ffa30..225e495 100644 --- a/admin-web/next.config.ts +++ b/admin-web/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: 'standalone', }; export default nextConfig; diff --git a/admin-web/package.json b/admin-web/package.json index 8dd9913..53d6f56 100644 --- a/admin-web/package.json +++ b/admin-web/package.json @@ -13,7 +13,8 @@ "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:headed": "playwright test --headed", - "test:all": "npm run test && npm run test:e2e" + "test:all": "npm run test && npm run test:e2e", + "semantic-release": "semantic-release" }, "dependencies": { "@google/genai": "^1.47.0", @@ -30,6 +31,10 @@ }, "devDependencies": { "@playwright/test": "^1.58.2", + "@semantic-release/commit-analyzer": "^13.0.0", + "@semantic-release/exec": "^6.0.0", + "@semantic-release/git": "^10.0.0", + "@semantic-release/release-notes-generator": "^14.0.0", "@tailwindcss/postcss": "^4", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", @@ -42,6 +47,7 @@ "eslint": "^9", "eslint-config-next": "16.1.6", "jsdom": "^28.1.0", + "semantic-release": "^24.0.0", "shadcn": "^3.8.5", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", diff --git a/admin-web/version.json b/admin-web/version.json new file mode 100644 index 0000000..c3daccd --- /dev/null +++ b/admin-web/version.json @@ -0,0 +1 @@ +{"version": "0.1.0"} \ No newline at end of file