990bc4400c
CI / Lint & Typecheck (push) Successful in 25s
CI / Test (push) Successful in 27s
CI / Build (push) Successful in 24s
CI / E2E Tests (push) Failing after 46s
CI / Build & Push Docker Images (push) Has been skipped
CI / Update Infra Image Tags (push) Has been skipped
CI / Web E2E (Dev) (push) Has been cancelled
CI / Deploy PR to groombook-dev (push) Has been cancelled
returns immediately after Docker reports containers started, not after services inside those containers are actually listening. This causes Playwright to hit nginx before it's ready. Now: - Start containers with (no --wait) - Poll http://localhost:8080 AND http://localhost:3000/health every 10s, up to 30 attempts (5 minutes total) - Only proceed to E2E tests once both are reachable Co-Authored-By: Paperclip <noreply@paperclip.ing>
419 lines
14 KiB
YAML
419 lines
14 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main, dev]
|
|
pull_request:
|
|
branches: [main, dev]
|
|
workflow_dispatch:
|
|
|
|
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
|
|
timeout-minutes: 5
|
|
|
|
- name: Wait for services to be ready
|
|
run: |
|
|
echo "Waiting for services to become ready..."
|
|
for i in $(seq 1 30); do
|
|
if curl -sf http://localhost:8080 > /dev/null 2>&1 && curl -sf http://localhost:3000/health > /dev/null 2>&1; then
|
|
echo "Services ready after ${i} attempts"
|
|
exit 0
|
|
fi
|
|
echo "Attempt $i/30: services not ready yet, waiting 10s..."
|
|
sleep 10
|
|
done
|
|
echo "Warning: services may not be fully ready after 30 attempts (5m)"
|
|
timeout-minutes: 6
|
|
|
|
- name: Run E2E tests
|
|
run: pnpm --filter @groombook/e2e test
|
|
env:
|
|
PLAYWRIGHT_BASE_URL: http://localhost:8080
|
|
|
|
- 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
|
|
env:
|
|
VITE_API_URL: ""
|
|
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: |
|
|
# 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 Gitea Container Registry
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: git.farh.net
|
|
username: ${{ gitea.actor }}
|
|
password: ${{ secrets.REGISTRY_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: runners-groombook
|
|
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 <<EOF | kubectl apply -n groombook-dev -f -
|
|
apiVersion: batch/v1
|
|
kind: Job
|
|
metadata:
|
|
name: migrate-pr-$PR_NUM
|
|
spec:
|
|
ttlSecondsAfterFinished: 3600
|
|
backoffLimit: 2
|
|
template:
|
|
spec:
|
|
restartPolicy: Never
|
|
containers:
|
|
- name: migrate
|
|
image: git.farh.net/groombook/migrate:$TAG
|
|
env:
|
|
- name: DATABASE_URL
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: groombook-postgres-credentials-dev
|
|
key: uri
|
|
EOF
|
|
kubectl wait --for=condition=complete "job/migrate-pr-$PR_NUM" \
|
|
-n groombook-dev --timeout=120s
|
|
|
|
kubectl set image deployment/api api=git.farh.net/groombook/api:$TAG -n groombook-dev
|
|
kubectl set image deployment/web web=git.farh.net/groombook/web:$TAG -n groombook-dev
|
|
|
|
kubectl rollout status deployment/api -n groombook-dev --timeout=300s
|
|
kubectl rollout status deployment/web -n groombook-dev --timeout=300s
|
|
|
|
echo "Deployment complete."
|
|
|
|
- name: Comment on PR
|
|
env:
|
|
PR_NUM: ${{ github.event.pull_request.number }}
|
|
run: |
|
|
PR_NUM="$PR_NUM"
|
|
BODY=$(cat <<'EOFBODY'
|
|
## Deployed to groombook-dev
|
|
|
|
**Images:** `pr-'"$PR_NUM"'`
|
|
|
|
**URL:** https://dev.groombook.farh.net
|
|
|
|
Ready for UAT validation.
|
|
EOFBODY
|
|
)
|
|
curl -s -X POST "https://git.farh.net/api/v1/repos/groombook/app/issues/${PR_NUM}/comments" \
|
|
-H "Authorization: Bearer ${{ secrets.REGISTRY_TOKEN }}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"body\": $(echo "$BODY" | jq -Rs .)}"
|
|
|
|
web-e2e:
|
|
name: Web E2E (Dev)
|
|
runs-on: ubuntu-latest
|
|
needs: [docker, deploy-dev]
|
|
if: github.event_name == 'pull_request'
|
|
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/web exec playwright install --with-deps chromium
|
|
|
|
- name: Run Web E2E tests
|
|
run: pnpm --filter @groombook/web test:e2e
|
|
timeout-minutes: 10
|
|
|
|
cd:
|
|
name: Update Infra Image Tags
|
|
runs-on: ubuntu-latest
|
|
needs: [docker]
|
|
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push'
|
|
steps:
|
|
- name: Clone groombook/infra
|
|
run: |
|
|
git clone https://oauth2:${{ secrets.REGISTRY_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 dev overlay image tags
|
|
env:
|
|
TAG: ${{ needs.docker.outputs.tag }}
|
|
SHA: ${{ github.sha }}
|
|
run: |
|
|
if [ -z "$TAG" ]; then
|
|
TAG="$(date -u +%Y.%m.%d)-${GITHUB_SHA::7}"
|
|
fi
|
|
export SHORT_SHA="${SHA::7}"
|
|
echo "Updating dev overlay image tags to: $TAG"
|
|
echo "Updating migration/seed Job names with SHA: $SHORT_SHA"
|
|
cd /tmp/infra
|
|
DEV_KUST="apps/overlays/dev/kustomization.yaml"
|
|
yq -i '(.images[] | select(.name == "git.farh.net/groombook/api")).newTag = env(TAG)' "$DEV_KUST"
|
|
yq -i '(.images[] | select(.name == "git.farh.net/groombook/web")).newTag = env(TAG)' "$DEV_KUST"
|
|
yq -i '(.images[] | select(.name == "git.farh.net/groombook/migrate")).newTag = env(TAG)' "$DEV_KUST"
|
|
yq -i '(.images[] | select(.name == "git.farh.net/groombook/seed")).newTag = env(TAG)' "$DEV_KUST"
|
|
yq -i '(.images[] | select(.name == "git.farh.net/groombook/reset")).newTag = env(TAG)' "$DEV_KUST"
|
|
|
|
MIGRATE_JOB="apps/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"
|
|
yq -i '.spec.ttlSecondsAfterFinished = (.spec.ttlSecondsAfterFinished // 86400)' "$MIGRATE_JOB"
|
|
fi
|
|
|
|
SEED_JOB="apps/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"
|
|
yq -i '.spec.ttlSecondsAfterFinished = (.spec.ttlSecondsAfterFinished // 86400)' "$SEED_JOB"
|
|
fi
|
|
|
|
git -C /tmp/infra diff --stat
|
|
|
|
- name: Create PR on groombook/infra
|
|
env:
|
|
TAG: ${{ needs.docker.outputs.tag }}
|
|
run: |
|
|
if [ -z "$TAG" ]; then
|
|
TAG="$(date -u +%Y.%m.%d)-${GITHUB_SHA::7}"
|
|
fi
|
|
|
|
cd /tmp/infra
|
|
git config user.name "groombook-engineer[bot]"
|
|
git config user.email "groombook-engineer@farh.net"
|
|
git checkout -b "chore/update-image-tags-${TAG}"
|
|
git add apps/overlays/dev/ apps/base/migrate-job.yaml apps/base/seed-job.yaml
|
|
git commit -m "chore: update image tags and migration/seed Job names to ${TAG}"
|
|
|
|
git push -u origin "chore/update-image-tags-${TAG}"
|
|
|
|
EXISTING_PR=$(curl -s "https://git.farh.net/api/v1/repos/groombook/infra/pulls?state=open&head=groombook:chore/update-image-tags-${TAG}" \
|
|
-H "Authorization: Bearer ${{ secrets.REGISTRY_TOKEN }}" | jq -r '.[0].number')
|
|
if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ]; then
|
|
echo "PR #$EXISTING_PR already exists for this tag, merging existing PR"
|
|
curl -s -X PUT "https://git.farh.net/api/v1/repos/groombook/infra/pulls/${EXISTING_PR}/merge" \
|
|
-H "Authorization: Bearer ${{ secrets.REGISTRY_TOKEN }}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"do": "merge"}'
|
|
else
|
|
PR_RESPONSE=$(curl -s -X POST "https://git.farh.net/api/v1/repos/groombook/infra/pulls" \
|
|
-H "Authorization: Bearer ${{ secrets.REGISTRY_TOKEN }}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{
|
|
\"base\": \"main\",
|
|
\"head\": \"chore/update-image-tags-${TAG}\",
|
|
\"title\": \"chore: deploy ${TAG} to dev\",
|
|
\"body\": \"[GRO-178](/GRO/issues/GRO-178) — automated image tag update from main merge\"
|
|
}")
|
|
PR_NUM=$(echo "$PR_RESPONSE" | jq -r '.number')
|
|
echo "Created PR #$PR_NUM"
|
|
curl -s -X PUT "https://git.farh.net/api/v1/repos/groombook/infra/pulls/${PR_NUM}/merge" \
|
|
-H "Authorization: Bearer ${{ secrets.REGISTRY_TOKEN }}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"do": "merge"}'
|
|
fi
|