From b0d1a4def4298d2e8a0672bf1e4bdb56f5fd08f0 Mon Sep 17 00:00:00 2001 From: Scrubs McBarkley <18+gb_scrubs@noreply.git.farh.net> Date: Wed, 20 May 2026 01:29:50 +0000 Subject: [PATCH 1/8] chore: migrate workflows to .gitea/ --- .gitea/workflows/ci.yml | 409 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 .gitea/workflows/ci.yml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..b41e002 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,409 @@ +name: CI + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + workflow_dispatch: + inputs: + ref: + description: "Branch or ref to run CI against" + required: false + default: "main" + +jobs: + lint-typecheck: + name: Lint & Typecheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: '9.15.4' + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: pnpm typecheck + + - name: Lint + run: pnpm lint + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: '9.15.4' + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests + run: pnpm test + + e2e: + name: E2E Tests + runs-on: ubuntu-latest + needs: [lint-typecheck, test] + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: '9.15.4' + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Playwright browsers + run: pnpm --filter @groombook/e2e exec playwright install --with-deps chromium + + - name: Start Docker Compose stack + run: docker compose up -d --wait + timeout-minutes: 5 + + - name: Run E2E tests + run: pnpm --filter @groombook/e2e test + + - name: Upload Playwright report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: apps/e2e/playwright-report/ + retention-days: 7 + + - name: Stop Docker Compose stack + if: always() + run: docker compose down + + build: + name: Build + runs-on: ubuntu-latest + needs: [lint-typecheck, test] + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: '9.15.4' + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build all packages + run: pnpm build + + docker: + name: Build & Push Docker Images + runs-on: ubuntu-latest + needs: [build, e2e] + outputs: + tag: ${{ steps.version.outputs.tag }} + steps: + - uses: actions/checkout@v4 + + - name: Generate image tag + id: version + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + TAG="pr-${{ github.event.pull_request.number }}-${GITHUB_SHA::7}" + else + TAG="$(date -u +%Y.%m.%d)-${GITHUB_SHA::7}" + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "Image tag: $TAG" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: git.farh.net + username: ${{ gitea.actor }} + password: ${{ gitea.token }} + + - name: Build and push API image + uses: docker/build-push-action@v6 + with: + context: . + file: apps/api/Dockerfile + target: runner + push: true + tags: | + git.farh.net/groombook/api:${{ steps.version.outputs.tag }} + ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/api:latest' || '' }} + cache-from: type=registry,ref=git.farh.net/groombook/cache:api + cache-to: type=registry,ref=git.farh.net/groombook/cache:api,mode=max + + - name: Build and push Migrate image + uses: docker/build-push-action@v6 + with: + context: . + file: apps/api/Dockerfile + target: migrate + push: true + tags: | + git.farh.net/groombook/migrate:${{ steps.version.outputs.tag }} + ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/migrate:latest' || '' }} + cache-from: type=registry,ref=git.farh.net/groombook/cache:migrate + cache-to: type=registry,ref=git.farh.net/groombook/cache:migrate,mode=max + + - name: Build and push Seed image + uses: docker/build-push-action@v6 + with: + context: . + file: apps/api/Dockerfile + target: seed + push: true + tags: | + git.farh.net/groombook/seed:${{ steps.version.outputs.tag }} + ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/seed:latest' || '' }} + cache-from: type=registry,ref=git.farh.net/groombook/cache:seed + cache-to: type=registry,ref=git.farh.net/groombook/cache:seed,mode=max + + - name: Build and push Reset image + uses: docker/build-push-action@v6 + with: + context: . + file: apps/api/Dockerfile + target: reset + push: true + tags: | + git.farh.net/groombook/reset:${{ steps.version.outputs.tag }} + ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/reset:latest' || '' }} + cache-from: type=registry,ref=git.farh.net/groombook/cache:reset + cache-to: type=registry,ref=git.farh.net/groombook/cache:reset,mode=max + + - name: Build and push Web image + uses: docker/build-push-action@v6 + with: + context: . + file: apps/web/Dockerfile + push: true + tags: | + git.farh.net/groombook/web:${{ steps.version.outputs.tag }} + ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/web:latest' || '' }} + cache-from: type=registry,ref=git.farh.net/groombook/cache:web + cache-to: type=registry,ref=git.farh.net/groombook/cache:web,mode=max + + deploy-dev: + name: Deploy PR to groombook-dev + runs-on: ubuntu-latest + needs: [docker] + if: github.event_name == 'pull_request' + steps: + - name: Install kubectl + run: | + curl -sLO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + sudo mv kubectl /usr/local/bin/ + kubectl version --client + + - name: Deploy to groombook-dev + env: + PR_NUM: ${{ github.event.pull_request.number }} + SHA: ${{ github.sha }} + run: | + TAG="pr-$PR_NUM-${SHA::7}" + echo "Deploying images tagged $TAG to groombook-dev..." + + kubectl delete job "migrate-pr-$PR_NUM" -n groombook-dev --ignore-not-found + cat < Date: Wed, 20 May 2026 01:29:52 +0000 Subject: [PATCH 2/8] chore: migrate workflows to .gitea/ --- .gitea/workflows/helm-release.yml | 54 +++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .gitea/workflows/helm-release.yml diff --git a/.gitea/workflows/helm-release.yml b/.gitea/workflows/helm-release.yml new file mode 100644 index 0000000..63438a6 --- /dev/null +++ b/.gitea/workflows/helm-release.yml @@ -0,0 +1,54 @@ +name: Release Helm Chart + +on: + push: + branches: [main] + paths: + - 'charts/**' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout groombook + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Checkout groombook.dev (Helm chart host) + uses: actions/checkout@v4 + with: + repository: groombook/groombook.dev + path: gitea-pages + token: ${{ gitea.token }} + + - name: Install Helm + uses: azure/setup-helm@v4 + + - name: Update Helm dependencies + run: helm dependency update charts/groombook + + - name: Package chart + run: | + mkdir -p gitea-pages/charts + helm package charts/groombook -d gitea-pages/charts + + - name: Update repo index + run: | + # TODO: update URL once Gitea Pages hosting is confirmed + CHART_URL="${HELM_CHART_URL:-https://groombook.farh.net/charts}" + if [ -f gitea-pages/charts/index.yaml ]; then + helm repo index gitea-pages/charts --merge gitea-pages/charts/index.yaml --url "$CHART_URL" + else + helm repo index gitea-pages/charts --url "$CHART_URL" + fi + + - name: Push to groombook.dev + run: | + cd gitea-pages + git config user.name "groombook-engineer[bot]" + git config user.email "groombook-engineer[bot]@git.farh.net" + git add charts/ + git diff --staged --quiet && echo 'No chart changes' && exit 0 + git commit -m "Update Helm chart repository" + git push From 7836511baaae2e2f3931a3ac89d6ff20515a56b6 Mon Sep 17 00:00:00 2001 From: Scrubs McBarkley <18+gb_scrubs@noreply.git.farh.net> Date: Wed, 20 May 2026 01:29:54 +0000 Subject: [PATCH 3/8] chore: migrate workflows to .gitea/ --- .gitea/workflows/promote-prod.yml | 108 ++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 .gitea/workflows/promote-prod.yml diff --git a/.gitea/workflows/promote-prod.yml b/.gitea/workflows/promote-prod.yml new file mode 100644 index 0000000..f6ea70a --- /dev/null +++ b/.gitea/workflows/promote-prod.yml @@ -0,0 +1,108 @@ +name: Promote to Production + +on: + workflow_dispatch: + inputs: + tag: + description: "Image tag to promote (e.g. 2026.03.28-f1b85bf)" + required: true + type: string + +jobs: + promote: + name: Promote to Production + runs-on: ubuntu-latest + steps: + - name: Validate tag format + run: | + TAG="${{ inputs.tag }}" + if ! echo "$TAG" | grep -qE '^[0-9]{4}\.[0-9]{2}\.[0-9]{2}-[a-f0-9]{7}$'; then + echo "::error::Invalid tag format: '$TAG'. Expected format: YYYY.MM.DD-sha7 (e.g. 2026.03.28-f1b85bf)" + exit 1 + fi + echo "Tag format valid: $TAG" + + - name: Verify image exists in Gitea Container Registry + env: + GITEA_TOKEN: ${{ gitea.token }} + run: | + TAG="${{ inputs.tag }}" + if ! curl -sf \ + -H "Authorization: token $GITEA_TOKEN" \ + "https://git.farh.net/api/v1/packages/groombook?type=container&limit=50" \ + | jq -e --arg t "$TAG" '[.[] | select(.name == "api" and .version == $t)] | length > 0' > /dev/null 2>&1; then + echo "::warning::Could not verify git.farh.net/groombook/api:$TAG via package API — verify manually if needed." + else + echo "Image verified: git.farh.net/groombook/api:$TAG exists" + fi + + - name: Clone groombook/infra + env: + GITEA_TOKEN: ${{ gitea.token }} + run: | + git clone https://oauth2:$GITEA_TOKEN@git.farh.net/groombook/infra.git /tmp/infra + + - name: Install yq + run: | + sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + + - name: Update prod overlay image tags and base Job names + env: + TAG: ${{ inputs.tag }} + run: | + cd /tmp/infra + PROD_KUST="apps/groombook/overlays/prod/kustomization.yaml" + + SHORT_SHA="${TAG##*-}" + export SHORT_SHA + export TAG + + yq -i '(.images[] | select(.name == "git.farh.net/groombook/api")).newTag = env(TAG)' "$PROD_KUST" + yq -i '(.images[] | select(.name == "git.farh.net/groombook/web")).newTag = env(TAG)' "$PROD_KUST" + yq -i '(.images[] | select(.name == "git.farh.net/groombook/migrate")).newTag = env(TAG)' "$PROD_KUST" + yq -i '(.images[] | select(.name == "git.farh.net/groombook/seed")).newTag = env(TAG)' "$PROD_KUST" + + MIGRATE_JOB="apps/groombook/base/migrate-job.yaml" + if [ -f "$MIGRATE_JOB" ]; then + yq -i '.metadata.name = "migrate-schema-" + env(SHORT_SHA)' "$MIGRATE_JOB" + yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$MIGRATE_JOB" + fi + + SEED_JOB="apps/groombook/base/seed-job.yaml" + if [ -f "$SEED_JOB" ]; then + yq -i '.metadata.name = "seed-test-data-" + env(SHORT_SHA)' "$SEED_JOB" + yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$SEED_JOB" + fi + + git -C /tmp/infra diff --stat + + - name: Create PR on groombook/infra + env: + TAG: ${{ inputs.tag }} + GITEA_TOKEN: ${{ gitea.token }} + run: | + cd /tmp/infra + git config user.name "groombook-engineer[bot]" + git config user.email "groombook-engineer[bot]@git.farh.net" + git checkout -b "release/promote-prod-${TAG}" + git add apps/groombook/overlays/prod/ apps/groombook/base/migrate-job.yaml apps/groombook/base/seed-job.yaml + git commit -m "release: promote ${TAG} to production" + git push -u origin "release/promote-prod-${TAG}" + curl -s -X POST \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + "https://git.farh.net/api/v1/repos/groombook/infra/pulls" \ + -d "{\"head\":\"release/promote-prod-${TAG}\",\"base\":\"main\",\"title\":\"release: promote ${TAG} to production\",\"body\":\"Promote image tag ${TAG} to production after UAT sign-off. cc @cpfarhood\"}" + + - name: Notify on failure + if: failure() + env: + GITEA_TOKEN: ${{ gitea.token }} + RUN_ID: ${{ github.run_id }} + run: | + curl -s -X POST \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + "https://git.farh.net/api/v1/repos/groombook/app/issues/$RUN_ID/comments" \ + -d '{"body": "## Production Promotion Failed\n\nThe `promote-prod` workflow failed. Check the workflow run logs for details."}' From db3bcf8094328c188f94c1fa2747968c7916d035 Mon Sep 17 00:00:00 2001 From: Scrubs McBarkley <18+gb_scrubs@noreply.git.farh.net> Date: Wed, 20 May 2026 01:29:57 +0000 Subject: [PATCH 4/8] chore: migrate workflows to .gitea/ --- .gitea/workflows/promote-to-uat.yml | 98 +++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .gitea/workflows/promote-to-uat.yml diff --git a/.gitea/workflows/promote-to-uat.yml b/.gitea/workflows/promote-to-uat.yml new file mode 100644 index 0000000..35c4f80 --- /dev/null +++ b/.gitea/workflows/promote-to-uat.yml @@ -0,0 +1,98 @@ +name: Promote to UAT + +on: + workflow_dispatch: + inputs: + image_tag: + description: "Image tag to deploy to UAT (e.g. 2026.04.03-abc1234)" + required: true + type: string + +jobs: + promote-to-uat: + name: Promote to groombook-uat + runs-on: ubuntu-latest + steps: + - name: Clone groombook/infra + env: + GITEA_TOKEN: ${{ gitea.token }} + run: | + git clone https://oauth2:$GITEA_TOKEN@git.farh.net/groombook/infra.git /tmp/infra + + - name: Install yq + run: | + sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + + - name: Update UAT overlay image tags and base Job names + env: + TAG: ${{ inputs.image_tag }} + run: | + echo "Updating UAT overlay image tags to: $TAG" + cd /tmp/infra + UAT_KUST="apps/groombook/overlays/uat/kustomization.yaml" + + if [ ! -f "$UAT_KUST" ]; then + echo "ERROR: UAT overlay not found at $UAT_KUST. Ensure GRO-427 has been completed." + exit 1 + fi + + SHORT_SHA="${TAG##*-}" + export SHORT_SHA + export TAG + + yq -i '(.images[] | select(.name == "git.farh.net/groombook/api")).newTag = env(TAG)' "$UAT_KUST" + yq -i '(.images[] | select(.name == "git.farh.net/groombook/web")).newTag = env(TAG)' "$UAT_KUST" + yq -i '(.images[] | select(.name == "git.farh.net/groombook/migrate")).newTag = env(TAG)' "$UAT_KUST" + yq -i '(.images[] | select(.name == "git.farh.net/groombook/seed")).newTag = env(TAG)' "$UAT_KUST" + + MIGRATE_JOB="apps/groombook/base/migrate-job.yaml" + if [ -f "$MIGRATE_JOB" ]; then + yq -i '.metadata.name = "migrate-schema-" + env(SHORT_SHA)' "$MIGRATE_JOB" + yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$MIGRATE_JOB" + fi + + SEED_JOB="apps/groombook/base/seed-job.yaml" + if [ -f "$SEED_JOB" ]; then + yq -i '.metadata.name = "seed-test-data-" + env(SHORT_SHA)' "$SEED_JOB" + yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$SEED_JOB" + fi + + git -C /tmp/infra diff --stat + + - name: Create PR on groombook/infra + env: + TAG: ${{ inputs.image_tag }} + GITEA_TOKEN: ${{ gitea.token }} + run: | + cd /tmp/infra + git config user.name "groombook-engineer[bot]" + git config user.email "groombook-engineer[bot]@git.farh.net" + git checkout -b "chore/update-uat-image-tags-${TAG}" + git add apps/groombook/overlays/uat/ apps/groombook/base/migrate-job.yaml apps/groombook/base/seed-job.yaml + git commit -m "chore: promote ${TAG} to UAT" + git push -u origin "chore/update-uat-image-tags-${TAG}" + + PR_NUM=$(curl -s -X POST \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + "https://git.farh.net/api/v1/repos/groombook/infra/pulls" \ + -d "{\"head\":\"chore/update-uat-image-tags-${TAG}\",\"base\":\"main\",\"title\":\"chore: promote ${TAG} to UAT\",\"body\":\"[GRO-429](/GRO/issues/GRO-429) — UAT promotion triggered by CTO\"}" \ + | jq '.number') + curl -s -X POST \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + "https://git.farh.net/api/v1/repos/groombook/infra/pulls/$PR_NUM/merge" \ + -d '{"Do":"merge"}' + + - name: Notify on failure + if: failure() + env: + GITEA_TOKEN: ${{ gitea.token }} + RUN_ID: ${{ github.run_id }} + run: | + curl -s -X POST \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + "https://git.farh.net/api/v1/repos/groombook/app/issues/$RUN_ID/comments" \ + -d '{"body": "## UAT Promotion Failed\n\nThe `promote-to-uat` workflow failed. Check the workflow run logs for details.\n\nCommon issues:\n- UAT overlay not found (ensure GRO-427 is complete)\n- GITEA_TOKEN permissions"}' From c67f731f698f46e4e5737646ccd28b3cfd50be6e Mon Sep 17 00:00:00 2001 From: Scrubs McBarkley <18+gb_scrubs@noreply.git.farh.net> Date: Wed, 20 May 2026 01:30:06 +0000 Subject: [PATCH 5/8] chore: remove legacy .github/workflows --- .github/workflows/ci.yml | 434 --------------------------------------- 1 file changed, 434 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 6a8c173..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,434 +0,0 @@ -name: CI - -on: - push: - branches: [main, dev] - pull_request: - branches: [main, dev] - workflow_dispatch: - inputs: - ref: - description: "Branch or ref to run CI against" - required: false - default: "main" - -jobs: - lint-typecheck: - name: Lint & Typecheck - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - version: '9.15.4' - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Typecheck - run: pnpm typecheck - - - name: Lint - run: pnpm lint - - test: - name: Test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - version: '9.15.4' - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Run tests - run: pnpm test - - e2e: - name: E2E Tests - runs-on: ubuntu-latest - needs: [lint-typecheck, test] - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - version: '9.15.4' - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Install Playwright browsers - run: pnpm --filter @groombook/e2e exec playwright install --with-deps chromium - - - name: Start Docker Compose stack - run: docker compose up -d --wait - timeout-minutes: 5 - - - name: Run E2E tests - run: pnpm --filter @groombook/e2e test - - - name: Upload Playwright report - if: failure() - uses: actions/upload-artifact@v4 - with: - name: playwright-report - path: apps/e2e/playwright-report/ - retention-days: 7 - - - name: Stop Docker Compose stack - if: always() - run: docker compose down - - build: - name: Build - runs-on: ubuntu-latest - needs: [lint-typecheck, test] - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - version: '9.15.4' - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build all packages - run: pnpm build - - docker: - name: Build & Push Docker Images - runs-on: ubuntu-latest - needs: [build, e2e] - outputs: - tag: ${{ steps.version.outputs.tag }} - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v4 - - - name: Generate image tag - id: version - run: | - # Always include short SHA so each build is immutable and cache-from can never - # cross-contaminate between commits. For PRs the format is pr-N-sha7; for main - # it is YYYY.MM.DD-sha7. - if [ "${{ github.event_name }}" = "pull_request" ]; then - TAG="pr-${{ github.event.pull_request.number }}-${GITHUB_SHA::7}" - else - TAG="$(date -u +%Y.%m.%d)-${GITHUB_SHA::7}" - fi - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "Image tag: $TAG" - - - 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 and push API image - uses: docker/build-push-action@v6 - with: - context: . - file: apps/api/Dockerfile - target: runner - push: true - tags: | - ghcr.io/groombook/api:${{ steps.version.outputs.tag }} - ${{ github.ref == 'refs/heads/main' && 'ghcr.io/groombook/api:latest' || '' }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Build and push Migrate image - uses: docker/build-push-action@v6 - with: - context: . - file: apps/api/Dockerfile - target: migrate - push: true - tags: | - ghcr.io/groombook/migrate:${{ steps.version.outputs.tag }} - ${{ github.ref == 'refs/heads/main' && 'ghcr.io/groombook/migrate:latest' || '' }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Build and push Seed image - uses: docker/build-push-action@v6 - with: - context: . - file: apps/api/Dockerfile - target: seed - push: true - tags: | - ghcr.io/groombook/seed:${{ steps.version.outputs.tag }} - ${{ github.ref == 'refs/heads/main' && 'ghcr.io/groombook/seed:latest' || '' }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Build and push Reset image - uses: docker/build-push-action@v6 - with: - context: . - file: apps/api/Dockerfile - target: reset - push: true - tags: | - ghcr.io/groombook/reset:${{ steps.version.outputs.tag }} - ${{ github.ref == 'refs/heads/main' && 'ghcr.io/groombook/reset:latest' || '' }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Build and push Web image - uses: docker/build-push-action@v6 - with: - context: . - file: apps/web/Dockerfile - push: true - tags: | - ghcr.io/groombook/web:${{ steps.version.outputs.tag }} - ${{ github.ref == 'refs/heads/main' && 'ghcr.io/groombook/web:latest' || '' }} - cache-from: type=gha - cache-to: type=gha,mode=max - - deploy-dev: - name: Deploy PR to groombook-dev - runs-on: runners-groombook - needs: [docker] - if: github.event_name == 'pull_request' - permissions: - contents: read - pull-requests: write - steps: - - name: Install kubectl - run: | - curl -sLO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" - chmod +x kubectl - sudo mv kubectl /usr/local/bin/ - kubectl version --client - - - name: Deploy to groombook-dev - env: - PR_NUM: ${{ github.event.pull_request.number }} - SHA: ${{ github.sha }} - run: | - TAG="pr-$PR_NUM-${SHA::7}" - echo "Deploying images tagged $TAG to groombook-dev..." - - # Run migration with PR image - kubectl delete job "migrate-pr-$PR_NUM" -n groombook-dev --ignore-not-found - cat < Date: Wed, 20 May 2026 01:30:08 +0000 Subject: [PATCH 6/8] chore: remove legacy .github/workflows --- .github/workflows/helm-release.yml | 54 ------------------------------ 1 file changed, 54 deletions(-) delete mode 100644 .github/workflows/helm-release.yml diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml deleted file mode 100644 index 5f91899..0000000 --- a/.github/workflows/helm-release.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Release Helm Chart - -on: - push: - branches: [main] - paths: - - 'charts/**' - -jobs: - release: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout groombook - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Checkout groombook.github.io - uses: actions/checkout@v4 - with: - repository: groombook/groombook.github.io - path: gh-pages - token: ${{ secrets.CHART_REPO_TOKEN }} - - - name: Install Helm - uses: azure/setup-helm@v4 - - - name: Update Helm dependencies - run: helm dependency update charts/groombook - - - name: Package chart - run: | - mkdir -p gh-pages/charts - helm package charts/groombook -d gh-pages/charts - - - name: Update repo index - run: | - if [ -f gh-pages/charts/index.yaml ]; then - helm repo index gh-pages/charts --merge gh-pages/charts/index.yaml --url https://groombook.github.io/charts - else - helm repo index gh-pages/charts --url https://groombook.github.io/charts - fi - - - name: Push to groombook.github.io - run: | - cd gh-pages - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add charts/ - git diff --staged --quiet && echo 'No chart changes' && exit 0 - git commit -m "Update Helm chart repository" - git push From 604e79bab4bdf5bed965512bb1cea8ce78dc2e81 Mon Sep 17 00:00:00 2001 From: Scrubs McBarkley <18+gb_scrubs@noreply.git.farh.net> Date: Wed, 20 May 2026 01:30:09 +0000 Subject: [PATCH 7/8] chore: remove legacy .github/workflows --- .github/workflows/promote-prod.yml | 117 ----------------------------- 1 file changed, 117 deletions(-) delete mode 100644 .github/workflows/promote-prod.yml diff --git a/.github/workflows/promote-prod.yml b/.github/workflows/promote-prod.yml deleted file mode 100644 index 110d1a3..0000000 --- a/.github/workflows/promote-prod.yml +++ /dev/null @@ -1,117 +0,0 @@ -name: Promote to Production - -on: - workflow_dispatch: - inputs: - tag: - description: "Image tag to promote (e.g. 2026.03.28-f1b85bf)" - required: true - type: string - -jobs: - promote: - name: Promote to Production - runs-on: ubuntu-latest - permissions: - contents: read - packages: read - steps: - - name: Validate tag format - run: | - TAG="${{ inputs.tag }}" - if ! echo "$TAG" | grep -qE '^[0-9]{4}\.[0-9]{2}\.[0-9]{2}-[a-f0-9]{7}$'; then - echo "::error::Invalid tag format: '$TAG'. Expected format: YYYY.MM.DD-sha7 (e.g. 2026.03.28-f1b85bf)" - exit 1 - fi - echo "Tag format valid: $TAG" - - - name: Verify image exists in GHCR - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - TAG="${{ inputs.tag }}" - # Check that the API image exists — if API was pushed, web/migrate were too - if ! gh api "/orgs/groombook/packages/container/api/versions" --jq ".[].metadata.container.tags[]" 2>/dev/null | grep -qF "$TAG"; then - echo "::error::Image ghcr.io/groombook/api:$TAG not found in GHCR. Verify the tag was built and pushed." - exit 1 - fi - echo "Image verified: ghcr.io/groombook/api:$TAG exists" - - - name: Generate infra repo token - id: infra-token - uses: tibdex/github-app-token@v2 - with: - app_id: ${{ vars.GH_APP_ID }} - private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - - name: Clone groombook/infra - run: | - git clone https://x-access-token:${{ steps.infra-token.outputs.token }}@github.com/groombook/infra.git /tmp/infra - - - name: Install yq - run: | - sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 - sudo chmod +x /usr/local/bin/yq - - - name: Update prod overlay image tags and base Job names - env: - TAG: ${{ inputs.tag }} - run: | - cd /tmp/infra - PROD_KUST="apps/groombook/overlays/prod/kustomization.yaml" - - SHORT_SHA="${TAG##*-}" - export SHORT_SHA - export TAG - - yq -i '(.images[] | select(.name == "ghcr.io/groombook/api")).newTag = env(TAG)' "$PROD_KUST" - yq -i '(.images[] | select(.name == "ghcr.io/groombook/web")).newTag = env(TAG)' "$PROD_KUST" - yq -i '(.images[] | select(.name == "ghcr.io/groombook/migrate")).newTag = env(TAG)' "$PROD_KUST" - yq -i '(.images[] | select(.name == "ghcr.io/groombook/seed")).newTag = env(TAG)' "$PROD_KUST" - - # Update migrate Job name to include short SHA (immutable template fix) - MIGRATE_JOB="apps/groombook/base/migrate-job.yaml" - if [ -f "$MIGRATE_JOB" ]; then - yq -i '.metadata.name = "migrate-schema-" + env(SHORT_SHA)' "$MIGRATE_JOB" - yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$MIGRATE_JOB" - fi - - # Update seed Job name to include short SHA (immutable template fix) - SEED_JOB="apps/groombook/base/seed-job.yaml" - if [ -f "$SEED_JOB" ]; then - yq -i '.metadata.name = "seed-test-data-" + env(SHORT_SHA)' "$SEED_JOB" - yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$SEED_JOB" - fi - - git -C /tmp/infra diff --stat - - - name: Create PR on groombook/infra - env: - TAG: ${{ inputs.tag }} - GH_TOKEN: ${{ steps.infra-token.outputs.token }} - run: | - cd /tmp/infra - git config user.name "groombook-engineer[bot]" - git config user.email "3141748+groombook-engineer[bot]@users.noreply.github.com" - git checkout -b "release/promote-prod-${TAG}" - git add apps/groombook/overlays/prod/ apps/groombook/base/migrate-job.yaml apps/groombook/base/seed-job.yaml - git commit -m "release: promote ${TAG} to production" - git push -u origin "release/promote-prod-${TAG}" - gh pr create \ - --repo groombook/infra \ - --base main \ - --head "release/promote-prod-${TAG}" \ - --title "release: promote ${TAG} to production" \ - --body "Promote image tag ${TAG} to production after UAT sign-off. cc @cpfarhood" - - - name: Notify on failure - if: failure() - uses: actions/github-script@v7 - with: - script: | - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: '## Production Promotion Failed\n\nThe `promote-prod` workflow failed. Check the workflow run logs for details.' - }); From b3517bf7469a3b0ae399edfae64723e9b71ad8f2 Mon Sep 17 00:00:00 2001 From: Scrubs McBarkley <18+gb_scrubs@noreply.git.farh.net> Date: Wed, 20 May 2026 01:30:11 +0000 Subject: [PATCH 8/8] chore: remove legacy .github/workflows --- .github/workflows/promote-to-uat.yml | 108 --------------------------- 1 file changed, 108 deletions(-) delete mode 100644 .github/workflows/promote-to-uat.yml diff --git a/.github/workflows/promote-to-uat.yml b/.github/workflows/promote-to-uat.yml deleted file mode 100644 index 083e013..0000000 --- a/.github/workflows/promote-to-uat.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: Promote to UAT - -on: - workflow_dispatch: - inputs: - image_tag: - description: "Image tag to deploy to UAT (e.g. 2026.04.03-abc1234)" - required: true - type: string - -jobs: - promote-to-uat: - name: Promote to groombook-uat - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Generate infra repo token - id: infra-token - uses: tibdex/github-app-token@v2 - with: - app_id: ${{ vars.GH_APP_ID }} - private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - - name: Clone groombook/infra - run: | - git clone https://x-access-token:${{ steps.infra-token.outputs.token }}@github.com/groombook/infra.git /tmp/infra - - - name: Install yq - run: | - sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 - sudo chmod +x /usr/local/bin/yq - - - name: Update UAT overlay image tags and base Job names - env: - TAG: ${{ inputs.image_tag }} - run: | - echo "Updating UAT overlay image tags to: $TAG" - cd /tmp/infra - UAT_KUST="apps/groombook/overlays/uat/kustomization.yaml" - - if [ ! -f "$UAT_KUST" ]; then - echo "ERROR: UAT overlay not found at $UAT_KUST. Ensure GRO-427 has been completed." - exit 1 - fi - - SHORT_SHA="${TAG##*-}" - export SHORT_SHA - export TAG - - yq -i '(.images[] | select(.name == "ghcr.io/groombook/api")).newTag = env(TAG)' "$UAT_KUST" - yq -i '(.images[] | select(.name == "ghcr.io/groombook/web")).newTag = env(TAG)' "$UAT_KUST" - yq -i '(.images[] | select(.name == "ghcr.io/groombook/migrate")).newTag = env(TAG)' "$UAT_KUST" - yq -i '(.images[] | select(.name == "ghcr.io/groombook/seed")).newTag = env(TAG)' "$UAT_KUST" - - # Update migrate Job name to include short SHA (immutable template fix) - MIGRATE_JOB="apps/groombook/base/migrate-job.yaml" - if [ -f "$MIGRATE_JOB" ]; then - yq -i '.metadata.name = "migrate-schema-" + env(SHORT_SHA)' "$MIGRATE_JOB" - yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$MIGRATE_JOB" - fi - - # Update seed Job name to include short SHA (immutable template fix) - # NOTE: Do NOT update the image tag here — let the Kustomize images transformer - # in the UAT overlay handle it via newTag. This avoids the immutable template issue. - SEED_JOB="apps/groombook/base/seed-job.yaml" - if [ -f "$SEED_JOB" ]; then - yq -i '.metadata.name = "seed-test-data-" + env(SHORT_SHA)' "$SEED_JOB" - yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$SEED_JOB" - fi - - git -C /tmp/infra diff --stat - - - name: Create PR on groombook/infra - env: - TAG: ${{ inputs.image_tag }} - GH_TOKEN: ${{ steps.infra-token.outputs.token }} - run: | - cd /tmp/infra - git config user.name "groombook-engineer[bot]" - git config user.email "3141748+groombook-engineer[bot]@users.noreply.github.com" - git checkout -b "chore/update-uat-image-tags-${TAG}" - git add apps/groombook/overlays/uat/ apps/groombook/base/migrate-job.yaml apps/groombook/base/seed-job.yaml - git commit -m "chore: promote ${TAG} to UAT" - - git push -u origin "chore/update-uat-image-tags-${TAG}" - - # Create PR and merge immediately (no required checks on groombook/infra) - PR_URL=$(gh pr create \ - --repo groombook/infra \ - --base main \ - --head "chore/update-uat-image-tags-${TAG}" \ - --title "chore: promote ${TAG} to UAT" \ - --body "[GRO-429](/GRO/issues/GRO-429) — UAT promotion triggered by CTO") - gh pr merge "$PR_URL" --merge - - - name: Notify on failure - if: failure() - uses: actions/github-script@v7 - with: - script: | - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: '## UAT Promotion Failed\n\nThe `promote-to-uat` workflow failed. Check the workflow run logs for details.\n\nCommon issues:\n- UAT overlay not found (ensure GRO-427 is complete)\n- Infra repo access token expired' - });