From 777c3b34bc5a10b785531ccb8960ac7a46a2f17a Mon Sep 17 00:00:00 2001 From: ianshaloom Date: Sun, 8 Mar 2026 16:35:10 +0300 Subject: [PATCH] fix(ci): replace dynamic matrix with explicit per-app jobs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gitea Actions does not evaluate fromJson() in matrix strategy — matrix.app was always empty, breaking turbo prune. Replaced with two explicit jobs (build-storefront, build-admin) each with a plain if: condition. The deploy job uses always() + !contains(needs.*.result, 'failure') so it runs when either build succeeded and skips when a build was cancelled/failed. Parallelism is preserved — both apps still build simultaneously when both change. Co-Authored-By: Claude Sonnet 4.6 --- .gitea/workflows/deploy-staging.yml | 187 +++++++++++++--------------- 1 file changed, 90 insertions(+), 97 deletions(-) diff --git a/.gitea/workflows/deploy-staging.yml b/.gitea/workflows/deploy-staging.yml index 3033ee1..d960030 100644 --- a/.gitea/workflows/deploy-staging.yml +++ b/.gitea/workflows/deploy-staging.yml @@ -8,7 +8,6 @@ on: # Gitea Actions has no environment-level secrets (unlike GitHub Actions). # Staging and production secrets live at repo level, distinguished by prefix. # Production workflow uses the same names with PROD_ prefix. -# (see: troubleshooting #8 — REGISTRY must include the owner segment) # # Required secrets (repo → Settings → Secrets and Variables → Actions): # STAGING_REGISTRY — host:port/owner @@ -31,18 +30,13 @@ on: jobs: # ── 0. Detect changes ──────────────────────────────────────────────────────── - # Determines which apps have changed so downstream jobs skip unchanged ones. - # Rules: - # apps/storefront/** → storefront - # apps/admin/** → admin - # packages/**, convex/** → both (shared code) - # package.json, package-lock.json, turbo.json → both (root config) + # Determines which apps need to be rebuilt on this push. + # Shared paths (packages/, convex/, root config) mark both apps as changed. changes: name: Detect changed apps runs-on: ubuntu-latest outputs: - matrix: ${{ steps.detect.outputs.matrix }} storefront: ${{ steps.detect.outputs.storefront }} admin: ${{ steps.detect.outputs.admin }} steps: @@ -53,7 +47,6 @@ jobs: - name: Determine affected apps id: detect run: | - # Fall back to diffing against an empty tree on the very first commit BASE=$(git rev-parse HEAD~1 2>/dev/null || git hash-object -t tree /dev/null) CHANGED=$(git diff --name-only "$BASE" HEAD) @@ -66,24 +59,11 @@ jobs: ADMIN=true fi - echo "$CHANGED" | grep -q '^apps/storefront/' && STOREFRONT=true - echo "$CHANGED" | grep -q '^apps/admin/' && ADMIN=true + echo "$CHANGED" | grep -q '^apps/storefront/' && STOREFRONT=true || true + echo "$CHANGED" | grep -q '^apps/admin/' && ADMIN=true || true - # Build JSON matrix for the build job - APPS=() - [ "$STOREFRONT" = "true" ] && APPS+=("storefront") - [ "$ADMIN" = "true" ] && APPS+=("admin") - - if [ ${#APPS[@]} -eq 0 ]; then - MATRIX="[]" - else - MATRIX=$(printf '"%s",' "${APPS[@]}" | sed 's/,$//') - MATRIX="[${MATRIX}]" - fi - - echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT" echo "storefront=${STOREFRONT}" >> "$GITHUB_OUTPUT" - echo "admin=${ADMIN}" >> "$GITHUB_OUTPUT" + echo "admin=${ADMIN}" >> "$GITHUB_OUTPUT" # ── 1. CI ─────────────────────────────────────────────────────────────────── @@ -93,116 +73,131 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/setup-node@v4 with: node-version: 20 cache: npm - - name: Install dependencies - run: npm ci + - run: npm ci + - run: npm run lint + - run: npm run type-check + - run: npm run test:once - - name: Lint - run: npm run lint + # ── 2a. Build storefront ───────────────────────────────────────────────────── - - name: Typecheck - run: npm run type-check - - - name: Test - run: npm run test:once - - # ── 2. Build & push ───────────────────────────────────────────────────────── - # Matrix is restricted to apps that actually changed — unchanged apps are - # skipped entirely so their build minutes are not wasted. - - build: - name: Build & push — ${{ matrix.app }} + build-storefront: + name: Build & push — storefront needs: [ci, changes] - if: needs.changes.outputs.matrix != '[]' + if: needs.changes.outputs.storefront == 'true' runs-on: ubuntu-latest - strategy: - matrix: - app: ${{ fromJson(needs.changes.outputs.matrix) }} steps: - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/setup-node@v4 with: node-version: 20 cache: npm - - name: Install dependencies - run: npm ci + - run: npm ci - - name: Prune workspace for ${{ matrix.app }} + - name: Prune workspace run: | - npx turbo prune ${{ matrix.app }} --docker - # convex/ is not an npm workspace so turbo prune excludes it — copy manually + npx turbo prune storefront --docker cp -r convex out/full/convex - # turbo prune can't fully parse the npm 11 lockfile; replace with full lockfile - # so non-hoisted packages (e.g. apps/storefront/node_modules/@heroui/react) - # are present when npm ci runs inside the Docker build. cp package-lock.json out/package-lock.json - name: Authenticate with registry - # docker login fails for HTTP-only registries — pre-populate config.json instead - # (see: troubleshooting #7) run: | mkdir -p ~/.docker AUTH=$(echo -n "${{ secrets.STAGING_REGISTRY_USER }}:${{ secrets.STAGING_REGISTRY_TOKEN }}" | base64 -w 0) REGISTRY_HOST=$(echo "${{ secrets.STAGING_REGISTRY }}" | cut -d'/' -f1) echo "{\"auths\":{\"${REGISTRY_HOST}\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json - - name: Build & push ${{ matrix.app }} - # --push uses buildkit HTTPS internally, which fails for HTTP registries. - # --load + docker push goes through the Podman daemon (insecure=true). (#7, #12) + - name: Build & push env: - STOREFRONT_CLERK_KEY: ${{ secrets.STAGING_STOREFRONT_NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} - ADMIN_CLERK_KEY: ${{ secrets.STAGING_ADMIN_NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.STAGING_STOREFRONT_NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} NEXT_PUBLIC_CONVEX_URL: ${{ secrets.STAGING_NEXT_PUBLIC_CONVEX_URL }} - NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME: ${{ secrets.STAGING_NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME }} - NEXT_PUBLIC_CLOUDINARY_API_KEY: ${{ secrets.STAGING_NEXT_PUBLIC_CLOUDINARY_API_KEY }} - NEXT_PUBLIC_IMAGE_PROCESSING_API_URL: ${{ secrets.STAGING_NEXT_PUBLIC_IMAGE_PROCESSING_API_URL }} NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${{ secrets.STAGING_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }} run: | SHORT_SHA="${GITHUB_SHA::7}" - IMAGE="${{ secrets.STAGING_REGISTRY }}/${{ matrix.app }}" - - if [ "${{ matrix.app }}" = "admin" ]; then - docker build \ - -f apps/admin/Dockerfile \ - --build-arg NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="$ADMIN_CLERK_KEY" \ - --build-arg NEXT_PUBLIC_CONVEX_URL="$NEXT_PUBLIC_CONVEX_URL" \ - --build-arg NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="$NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME" \ - --build-arg NEXT_PUBLIC_CLOUDINARY_API_KEY="$NEXT_PUBLIC_CLOUDINARY_API_KEY" \ - --build-arg NEXT_PUBLIC_IMAGE_PROCESSING_API_URL="$NEXT_PUBLIC_IMAGE_PROCESSING_API_URL" \ - --load \ - -t "${IMAGE}:staging" \ - ./out - else - docker build \ - -f apps/storefront/Dockerfile \ - --build-arg NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="$STOREFRONT_CLERK_KEY" \ - --build-arg NEXT_PUBLIC_CONVEX_URL="$NEXT_PUBLIC_CONVEX_URL" \ - --build-arg NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" \ - --load \ - -t "${IMAGE}:staging" \ - ./out - fi - + IMAGE="${{ secrets.STAGING_REGISTRY }}/storefront" + docker build \ + -f apps/storefront/Dockerfile \ + --build-arg NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY" \ + --build-arg NEXT_PUBLIC_CONVEX_URL="$NEXT_PUBLIC_CONVEX_URL" \ + --build-arg NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" \ + --load -t "${IMAGE}:staging" ./out docker tag "${IMAGE}:staging" "${IMAGE}:sha-${SHORT_SHA}" docker push "${IMAGE}:staging" docker push "${IMAGE}:sha-${SHORT_SHA}" - # ── 3. Deploy ─────────────────────────────────────────────────────────────── - # Only pulls and restarts the containers whose images were just rebuilt. + # ── 2b. Build admin ────────────────────────────────────────────────────────── + + build-admin: + name: Build & push — admin + needs: [ci, changes] + if: needs.changes.outputs.admin == 'true' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + - name: Prune workspace + run: | + npx turbo prune admin --docker + cp -r convex out/full/convex + cp package-lock.json out/package-lock.json + + - name: Authenticate with registry + run: | + mkdir -p ~/.docker + AUTH=$(echo -n "${{ secrets.STAGING_REGISTRY_USER }}:${{ secrets.STAGING_REGISTRY_TOKEN }}" | base64 -w 0) + REGISTRY_HOST=$(echo "${{ secrets.STAGING_REGISTRY }}" | cut -d'/' -f1) + echo "{\"auths\":{\"${REGISTRY_HOST}\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json + + - name: Build & push + env: + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.STAGING_ADMIN_NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} + NEXT_PUBLIC_CONVEX_URL: ${{ secrets.STAGING_NEXT_PUBLIC_CONVEX_URL }} + NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME: ${{ secrets.STAGING_NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME }} + NEXT_PUBLIC_CLOUDINARY_API_KEY: ${{ secrets.STAGING_NEXT_PUBLIC_CLOUDINARY_API_KEY }} + NEXT_PUBLIC_IMAGE_PROCESSING_API_URL: ${{ secrets.STAGING_NEXT_PUBLIC_IMAGE_PROCESSING_API_URL }} + run: | + SHORT_SHA="${GITHUB_SHA::7}" + IMAGE="${{ secrets.STAGING_REGISTRY }}/admin" + docker build \ + -f apps/admin/Dockerfile \ + --build-arg NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY" \ + --build-arg NEXT_PUBLIC_CONVEX_URL="$NEXT_PUBLIC_CONVEX_URL" \ + --build-arg NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="$NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME" \ + --build-arg NEXT_PUBLIC_CLOUDINARY_API_KEY="$NEXT_PUBLIC_CLOUDINARY_API_KEY" \ + --build-arg NEXT_PUBLIC_IMAGE_PROCESSING_API_URL="$NEXT_PUBLIC_IMAGE_PROCESSING_API_URL" \ + --load -t "${IMAGE}:staging" ./out + docker tag "${IMAGE}:staging" "${IMAGE}:sha-${SHORT_SHA}" + docker push "${IMAGE}:staging" + docker push "${IMAGE}:sha-${SHORT_SHA}" + + # ── 3. Deploy ──────────────────────────────────────────────────────────────── + # Runs when at least one app changed and no build failed. + # `always()` is required so the job isn't auto-skipped when one build job + # was skipped (Gitea/GitHub skip downstream jobs of skipped jobs by default). deploy: name: Deploy to staging VPS - needs: [build, changes] - if: needs.changes.outputs.matrix != '[]' + needs: [build-storefront, build-admin, changes] + if: | + always() && + (needs.changes.outputs.storefront == 'true' || needs.changes.outputs.admin == 'true') && + !contains(needs.*.result, 'failure') && + !contains(needs.*.result, 'cancelled') runs-on: ubuntu-latest steps: @@ -241,7 +236,6 @@ jobs: | podman login "${REGISTRY_HOST}" \ -u "${REGISTRY_USER}" --password-stdin --tls-verify=false - # Pull only the images that were just rebuilt [ "${STOREFRONT_CHANGED}" = "true" ] && podman pull --tls-verify=false "${REGISTRY}/storefront:staging" [ "${ADMIN_CHANGED}" = "true" ] && podman pull --tls-verify=false "${REGISTRY}/admin:staging" @@ -253,7 +247,6 @@ jobs: > /opt/staging/.env chmod 600 /opt/staging/.env - # Restart only the containers whose images changed SERVICES="" [ "${STOREFRONT_CHANGED}" = "true" ] && SERVICES="\${SERVICES} storefront" [ "${ADMIN_CHANGED}" = "true" ] && SERVICES="\${SERVICES} admin"