From b46766d54416ca1edc9b0b431f076cfe03192be1 Mon Sep 17 00:00:00 2001 From: "ankra-platform-system-test[bot]" <202076971+ankra-platform-system-test[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:46:48 +0000 Subject: [PATCH 1/3] chore: add CI/CD pipeline, Dockerfile, and Kubernetes manifests Generated by Ankra platform --- .ankra/manifests/api-deployment.yaml | 54 ++++++++ .ankra/manifests/api-service.yaml | 15 +++ .ankra/manifests/web-deployment.yaml | 58 ++++++++ .ankra/manifests/web-service.yaml | 15 +++ .github/workflows/api-build-and-publish.yml | 136 +++++++++++++++++++ .github/workflows/web-build-and-publish.yml | 138 ++++++++++++++++++++ apps/api/Dockerfile | 44 +++++++ apps/web/Dockerfile | 48 +++++++ 8 files changed, 508 insertions(+) create mode 100644 .ankra/manifests/api-deployment.yaml create mode 100644 .ankra/manifests/api-service.yaml create mode 100644 .ankra/manifests/web-deployment.yaml create mode 100644 .ankra/manifests/web-service.yaml create mode 100644 .github/workflows/api-build-and-publish.yml create mode 100644 .github/workflows/web-build-and-publish.yml create mode 100644 apps/api/Dockerfile create mode 100644 apps/web/Dockerfile diff --git a/.ankra/manifests/api-deployment.yaml b/.ankra/manifests/api-deployment.yaml new file mode 100644 index 0000000..a07b4a9 --- /dev/null +++ b/.ankra/manifests/api-deployment.yaml @@ -0,0 +1,54 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: api + labels: + app.kubernetes.io/name: api +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: api + template: + metadata: + labels: + app.kubernetes.io/name: api + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 1000 + containers: + - name: api + image: ghcr.io/CodeStaple/musclebuilder/api:${{ ankra.image_tag }} + imagePullPolicy: IfNotPresent + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 1000 + capabilities: + drop: + - ALL + ports: + - name: http + containerPort: 8000 + protocol: TCP + env: + - name: PORT + value: "8000" + livenessProbe: + httpGet: + path: /health + port: http + readinessProbe: + httpGet: + path: /health + port: http + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi diff --git a/.ankra/manifests/api-service.yaml b/.ankra/manifests/api-service.yaml new file mode 100644 index 0000000..27c0811 --- /dev/null +++ b/.ankra/manifests/api-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: api + labels: + app.kubernetes.io/name: api +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: api + ports: + - name: http + port: 80 + targetPort: 8000 + protocol: TCP diff --git a/.ankra/manifests/web-deployment.yaml b/.ankra/manifests/web-deployment.yaml new file mode 100644 index 0000000..5759351 --- /dev/null +++ b/.ankra/manifests/web-deployment.yaml @@ -0,0 +1,58 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: web + labels: + app.kubernetes.io/name: web +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: web + template: + metadata: + labels: + app.kubernetes.io/name: web + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 1000 + containers: + - name: web + image: ghcr.io/CodeStaple/musclebuilder/web:${{ ankra.image_tag }} + imagePullPolicy: IfNotPresent + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 1000 + capabilities: + drop: + - ALL + ports: + - name: http + containerPort: 3000 + protocol: TCP + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3000" + - name: HOSTNAME + value: "0.0.0.0" + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi diff --git a/.ankra/manifests/web-service.yaml b/.ankra/manifests/web-service.yaml new file mode 100644 index 0000000..7e57c85 --- /dev/null +++ b/.ankra/manifests/web-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: web + labels: + app.kubernetes.io/name: web +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: web + ports: + - name: http + port: 80 + targetPort: 3000 + protocol: TCP diff --git a/.github/workflows/api-build-and-publish.yml b/.github/workflows/api-build-and-publish.yml new file mode 100644 index 0000000..b9cf000 --- /dev/null +++ b/.github/workflows/api-build-and-publish.yml @@ -0,0 +1,136 @@ +name: Build and Publish (api) + +on: + push: + branches: [main] + paths: + - apps/api/** + - .github/workflows/api-build-and-publish.yml + - apps/api/uv.lock + - apps/api/pyproject.toml + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/api + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + actions: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Semgrep + run: | + pip install semgrep + semgrep scan --config "p/default" --config "p/javascript" --config "p/docker" --config "p/github-actions" --json --output semgrep-results.json || true + continue-on-error: true + + - name: Upload Semgrep results + uses: actions/upload-artifact@v4 + if: always() && hashFiles('semgrep-results.json') != '' + with: + name: ankra-semgrep-scan + path: semgrep-results.json + retention-days: 90 + + - name: Run Checkov IaC scan + uses: bridgecrewio/checkov-action@v12 + with: + directory: . + output_format: json + output_file_path: checkov-results.json + soft_fail: true + framework: dockerfile,helm,github_actions + continue-on-error: true + + - name: Upload Checkov results + uses: actions/upload-artifact@v4 + if: always() && hashFiles('checkov-results.json') != '' + with: + name: ankra-checkov-scan + path: checkov-results.json + retention-days: 90 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }}/api + tags: | + type=sha + type=ref,event=branch + type=semver,pattern={{version}} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: apps/api + push: true + platforms: linux/amd64,linux/arm64 + provenance: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Get short SHA + id: vars + run: echo "short_sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@v0.35.0 + with: + image-ref: ghcr.io/${{ github.repository }}/api:sha-${{ steps.vars.outputs.short_sha }} + format: json + output: trivy-results.json + severity: CRITICAL,HIGH,MEDIUM,LOW + continue-on-error: true + + - name: Upload Trivy scan results + uses: actions/upload-artifact@v4 + if: always() && hashFiles('trivy-results.json') != '' + with: + name: ankra-security-scan + path: trivy-results.json + retention-days: 90 + + - name: Make packages public + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OWNER: ${{ github.repository_owner }} + IMAGE_PKG: ${{ github.event.repository.name }}%2Fapi + run: | + set +e + for pkg in "$IMAGE_PKG"; do + echo "Setting $pkg to public..." + if gh api -X PATCH -H "Accept: application/vnd.github+json" "/orgs/$OWNER/packages/container/$pkg" -f visibility=public >/dev/null 2>&1; then + echo " done (org)" + continue + fi + if gh api -X PATCH -H "Accept: application/vnd.github+json" "/user/packages/container/$pkg" -f visibility=public >/dev/null 2>&1; then + echo " done (user)" + continue + fi + echo " skipped — flip visibility once in the GHCR UI: https://github.com/orgs/$OWNER/packages/container/$pkg/settings" + done diff --git a/.github/workflows/web-build-and-publish.yml b/.github/workflows/web-build-and-publish.yml new file mode 100644 index 0000000..813d5a9 --- /dev/null +++ b/.github/workflows/web-build-and-publish.yml @@ -0,0 +1,138 @@ +name: Build and Publish (web) + +on: + push: + branches: [main] + paths: + - apps/web/** + - .github/workflows/web-build-and-publish.yml + - pnpm-lock.yaml + - pnpm-workspace.yaml + - package.json + - packages/shared/** + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/web + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + actions: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Semgrep + run: | + pip install semgrep + semgrep scan --config "p/default" --config "p/javascript" --config "p/docker" --config "p/github-actions" --json --output semgrep-results.json || true + continue-on-error: true + + - name: Upload Semgrep results + uses: actions/upload-artifact@v4 + if: always() && hashFiles('semgrep-results.json') != '' + with: + name: ankra-semgrep-scan + path: semgrep-results.json + retention-days: 90 + + - name: Run Checkov IaC scan + uses: bridgecrewio/checkov-action@v12 + with: + directory: . + output_format: json + output_file_path: checkov-results.json + soft_fail: true + framework: dockerfile,helm,github_actions + continue-on-error: true + + - name: Upload Checkov results + uses: actions/upload-artifact@v4 + if: always() && hashFiles('checkov-results.json') != '' + with: + name: ankra-checkov-scan + path: checkov-results.json + retention-days: 90 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }}/web + tags: | + type=sha + type=ref,event=branch + type=semver,pattern={{version}} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: apps/web + push: true + platforms: linux/amd64,linux/arm64 + provenance: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Get short SHA + id: vars + run: echo "short_sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@v0.35.0 + with: + image-ref: ghcr.io/${{ github.repository }}/web:sha-${{ steps.vars.outputs.short_sha }} + format: json + output: trivy-results.json + severity: CRITICAL,HIGH,MEDIUM,LOW + continue-on-error: true + + - name: Upload Trivy scan results + uses: actions/upload-artifact@v4 + if: always() && hashFiles('trivy-results.json') != '' + with: + name: ankra-security-scan + path: trivy-results.json + retention-days: 90 + + - name: Make packages public + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OWNER: ${{ github.repository_owner }} + IMAGE_PKG: ${{ github.event.repository.name }}%2Fweb + run: | + set +e + for pkg in "$IMAGE_PKG"; do + echo "Setting $pkg to public..." + if gh api -X PATCH -H "Accept: application/vnd.github+json" "/orgs/$OWNER/packages/container/$pkg" -f visibility=public >/dev/null 2>&1; then + echo " done (org)" + continue + fi + if gh api -X PATCH -H "Accept: application/vnd.github+json" "/user/packages/container/$pkg" -f visibility=public >/dev/null 2>&1; then + echo " done (user)" + continue + fi + echo " skipped — flip visibility once in the GHCR UI: https://github.com/orgs/$OWNER/packages/container/$pkg/settings" + done diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 0000000..be1e62b --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,44 @@ +# syntax=docker/dockerfile:1 + +# ---- Builder: install deps into a venv with uv ---- +FROM python:3.12-slim AS builder + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +ENV UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ + UV_PYTHON_DOWNLOADS=0 + +WORKDIR /app + +# Install dependencies first (cached layer) using lockfile + project metadata. +COPY pyproject.toml uv.lock ./ +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --no-install-project --no-dev + +# Copy source and install the project itself. +COPY . . +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --no-dev + +# ---- Runtime: slim image, non-root ---- +FROM python:3.12-slim AS runtime + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PATH="/app/.venv/bin:$PATH" + +WORKDIR /app + +# Create non-root user. +RUN groupadd --system --gid 1001 appuser \ + && useradd --system --uid 1001 --gid 1001 --create-home appuser + +# Copy the built virtualenv and application source from the builder. +COPY --from=builder --chown=appuser:appuser /app /app + +USER appuser + +EXPOSE 8000 + +CMD ["uvicorn", "musclebuilder.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..ffd5976 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,48 @@ +# syntax=docker/dockerfile:1 + +# ---- Base with pnpm via corepack ---- +FROM node:22-alpine AS base +RUN corepack enable +WORKDIR /repo + +# ---- Dependencies: install the whole workspace (web + shared) ---- +FROM base AS deps +# Workspace + root manifests first for caching. +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY apps/web/package.json apps/web/package.json +COPY packages/shared/package.json packages/shared/package.json +RUN --mount=type=cache,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile + +# ---- Builder: build the Next.js app (standalone output) ---- +FROM base AS builder +COPY --from=deps /repo/node_modules ./node_modules +COPY --from=deps /repo/apps/web/node_modules ./apps/web/node_modules +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY packages/shared ./packages/shared +COPY apps/web ./apps/web +ENV NEXT_TELEMETRY_DISABLED=1 +RUN pnpm --filter @musclebuilder/web build + +# ---- Runtime: minimal standalone server, non-root ---- +FROM node:22-alpine AS runtime +WORKDIR /app + +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME=0.0.0.0 + +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 nextjs + +# Next.js standalone output bundles a minimal server + traced node_modules. +COPY --from=builder --chown=nextjs:nodejs /repo/apps/web/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /repo/apps/web/.next/static ./apps/web/.next/static +COPY --from=builder --chown=nextjs:nodejs /repo/apps/web/public ./apps/web/public + +USER nextjs + +EXPOSE 3000 + +CMD ["node", "apps/web/server.js"] From 34667676242c367d048bbf85a5317a7d2df2a712 Mon Sep 17 00:00:00 2001 From: "ankra-platform-system-test[bot]" <202076971+ankra-platform-system-test[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:56:03 +0000 Subject: [PATCH 2/3] Update generated files via Ankra --- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/web-build-and-publish.yml | 5 +++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a689b3..78e4c07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: image: postgres:16-alpine env: POSTGRES_USER: musclebuilder - POSTGRES_PASSWORD: musclebuilder + POSTGRES_PASSWORD: [REDACTED:kv_password:8a17e667] POSTGRES_DB: musclebuilder ports: ["5432:5432"] options: >- @@ -59,9 +59,9 @@ jobs: helm lint infra/helm/musclebuilder helm template mb infra/helm/musclebuilder > /dev/null - name: Build API image - run: docker build -f infra/Dockerfile.api -t musclebuilder-api:ci . + run: docker build -f apps/api/Dockerfile -t musclebuilder-api:ci apps/api - name: Build web image - run: docker build -f infra/Dockerfile.web --build-arg NEXT_PUBLIC_API_URL=https://api.example.com -t musclebuilder-web:ci . + run: docker build -f apps/web/Dockerfile --build-arg NEXT_PUBLIC_API_URL=https://api.example.com -t musclebuilder-web:ci . publish-images: # Build + push images to GHCR on main only. @@ -81,16 +81,16 @@ jobs: - name: Build + push API image uses: docker/build-push-action@v6 with: - context: . - file: infra/Dockerfile.api + context: apps/api + file: apps/api/Dockerfile push: true tags: ghcr.io/${{ github.repository }}/api:latest,ghcr.io/${{ github.repository }}/api:${{ github.sha }} - name: Build + push web image uses: docker/build-push-action@v6 with: context: . - file: infra/Dockerfile.web + file: apps/web/Dockerfile push: true build-args: | NEXT_PUBLIC_API_URL=https://api.musclebuilder.example.com - tags: ghcr.io/${{ github.repository }}/web:latest,ghcr.io/${{ github.repository }}/web:${{ github.sha }} + tags: ghcr.io/${{ github.repository }}/web:latest,ghcr.io/${{ github.repository }}/web:${{ github.sha }} \ No newline at end of file diff --git a/.github/workflows/web-build-and-publish.yml b/.github/workflows/web-build-and-publish.yml index 813d5a9..ffe578f 100644 --- a/.github/workflows/web-build-and-publish.yml +++ b/.github/workflows/web-build-and-publish.yml @@ -86,7 +86,8 @@ jobs: - name: Build and push Docker image uses: docker/build-push-action@v6 with: - context: apps/web + context: . + file: apps/web/Dockerfile push: true platforms: linux/amd64,linux/arm64 provenance: false @@ -135,4 +136,4 @@ jobs: continue fi echo " skipped — flip visibility once in the GHCR UI: https://github.com/orgs/$OWNER/packages/container/$pkg/settings" - done + done \ No newline at end of file From a019824d893d598705aabb8c7c611c1dad59191b Mon Sep 17 00:00:00 2001 From: "ankra-platform-system-test[bot]" <202076971+ankra-platform-system-test[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:00:03 +0000 Subject: [PATCH 3/3] Update generated files via Ankra --- .github/workflows/ci.yml | 102 +++++++++++++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78e4c07..648c5f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: branches: [main] jobs: + # ── API: lint, migrate, test ───────────────────────────────────────────── api: runs-on: ubuntu-latest services: @@ -14,7 +15,7 @@ jobs: image: postgres:16-alpine env: POSTGRES_USER: musclebuilder - POSTGRES_PASSWORD: [REDACTED:kv_password:8a17e667] + POSTGRES_PASSWORD: musclebuilder POSTGRES_DB: musclebuilder ports: ["5432:5432"] options: >- @@ -25,16 +26,20 @@ jobs: steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v4 - - run: cd apps/api && uv sync - - run: cd apps/api && uv run ruff check . - - name: Apply migrations (validates alembic against a real DB) + - name: Install dependencies + run: cd apps/api && uv sync --frozen + - name: Lint (ruff) + run: cd apps/api && uv run ruff check . + - name: Apply migrations run: cd apps/api && uv run alembic upgrade head env: DATABASE_URL: postgresql+asyncpg://musclebuilder:musclebuilder@localhost:5432/musclebuilder - - run: cd apps/api && uv run pytest tests -q + - name: Run tests + run: cd apps/api && uv run pytest tests -q env: DATABASE_URL: postgresql+asyncpg://musclebuilder:musclebuilder@localhost:5432/musclebuilder + # ── Web: typecheck + unit tests ────────────────────────────────────────── web: runs-on: ubuntu-latest steps: @@ -46,51 +51,110 @@ jobs: with: node-version: 20 cache: pnpm - - run: pnpm install - - run: pnpm --filter @musclebuilder/web exec vitest run - - run: pnpm --filter @musclebuilder/web typecheck + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Unit tests + run: pnpm --filter @musclebuilder/web exec vitest run + - name: Typecheck + run: pnpm --filter @musclebuilder/web typecheck + # ── Infra: Helm lint only ──────────────────────────────────────────────── infra: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Helm lint + template + - name: Helm lint + dry-run template run: | helm lint infra/helm/musclebuilder - helm template mb infra/helm/musclebuilder > /dev/null - - name: Build API image - run: docker build -f apps/api/Dockerfile -t musclebuilder-api:ci apps/api - - name: Build web image - run: docker build -f apps/web/Dockerfile --build-arg NEXT_PUBLIC_API_URL=https://api.example.com -t musclebuilder-web:ci . + helm template musclebuilder infra/helm/musclebuilder > /dev/null + # ── Docker build smoke-check (no push, just proves images compile) ──────── + docker-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build API image (smoke-check, no push) + uses: docker/build-push-action@v6 + with: + context: apps/api + file: apps/api/Dockerfile + push: false + load: true + tags: musclebuilder-api:ci + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build web image (smoke-check, no push) + uses: docker/build-push-action@v6 + with: + context: . + file: apps/web/Dockerfile + push: false + load: true + build-args: | + NEXT_PUBLIC_API_URL=https://api.example.com + tags: musclebuilder-web:ci + cache-from: type=gha + cache-to: type=gha,mode=max + + # ── Publish to GHCR (main only, after all checks pass) ─────────────────── publish-images: - # Build + push images to GHCR on main only. if: github.ref == 'refs/heads/main' && github.event_name == 'push' - needs: [api, web, infra] + needs: [api, web, infra, docker-build] runs-on: ubuntu-latest permissions: contents: read packages: write + steps: - uses: actions/checkout@v4 - - uses: docker/login-action@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Build + push API image uses: docker/build-push-action@v6 with: context: apps/api file: apps/api/Dockerfile push: true - tags: ghcr.io/${{ github.repository }}/api:latest,ghcr.io/${{ github.repository }}/api:${{ github.sha }} + platforms: linux/amd64,linux/arm64 + provenance: false + tags: | + ghcr.io/${{ github.repository }}/api:latest + ghcr.io/${{ github.repository }}/api:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Build + push web image uses: docker/build-push-action@v6 with: context: . file: apps/web/Dockerfile push: true + platforms: linux/amd64,linux/arm64 + provenance: false build-args: | NEXT_PUBLIC_API_URL=https://api.musclebuilder.example.com - tags: ghcr.io/${{ github.repository }}/web:latest,ghcr.io/${{ github.repository }}/web:${{ github.sha }} \ No newline at end of file + tags: | + ghcr.io/${{ github.repository }}/web:latest + ghcr.io/${{ github.repository }}/web:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file