Files
dezky/.gitea/workflows/ci.yml
T
Ronni Baslund 4907d0a856
ci / changes (push) Successful in 3s
ci / tc_booking (push) Successful in 22s
ci / tc_portal (push) Successful in 23s
ci / tc_platform_api (push) Successful in 21s
ci / tc_operator (push) Successful in 24s
ci / tc_website (push) Successful in 22s
ci / test_platform_api (push) Successful in 33s
ci / build_booking (push) Successful in 12s
ci / build_portal (push) Successful in 5s
ci / build_operator (push) Successful in 5s
ci / build_platform_api (push) Successful in 4s
ci / deploy (push) Successful in 41s
feat(ci): change-gated pipeline — only test/build/deploy what changed
A 'changes' job diffs the push range (github.event.before..sha; falls back
to everything on first/force pushes and when this workflow file itself
changes) and gates per-app typecheck/test/build jobs. Deploy is asymmetric
on purpose: app-only changes roll just the changed Deployments via
kubectl set image; manifest changes (fleet/apps/**) apply the kustomization
with every app pinned to its live image (or this push's sha) so an apply
never resets unchanged apps to :latest. Docs-only pushes run nothing.
2026-06-10 19:57:50 +02:00

281 lines
12 KiB
YAML

# CI/CD for the dezky monorepo (Gitea Actions). Installs deps and typechecks
# each app/service independently — the repo is NOT a single pnpm workspace yet,
# so every app has its own lockfile and is built from its own directory.
#
# Change-gated: the `changes` job diffs the push range and every later job
# only runs for the apps that actually changed. A docs-only push runs nothing;
# an operator-only push typechecks, builds and deploys just operator. Editing
# this workflow file counts as "everything changed".
#
# Deploys are intentionally asymmetric:
# - app change → `kubectl set image deploy/<app> …:<sha>` (only that app
# rolls; nothing else is touched)
# - manifest change (fleet/apps/**) → `kubectl apply -k` with every app's
# image pinned to what is LIVE right now (or the fresh sha
# for apps built in the same push) — a plain apply would
# reset unchanged apps to :latest.
name: ci
on:
push:
branches: [main]
pull_request:
jobs:
changes:
runs-on: ubuntu-latest
outputs:
portal: ${{ steps.diff.outputs.portal }}
booking: ${{ steps.diff.outputs.booking }}
operator: ${{ steps.diff.outputs.operator }}
website: ${{ steps.diff.outputs.website }}
platform_api: ${{ steps.diff.outputs.platform_api }}
infra: ${{ steps.diff.outputs.infra }}
deploy_any: ${{ steps.diff.outputs.deploy_any }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect changed paths
id: diff
run: |
# Push range: event.before..sha. On the first push to a branch (or a
# force-push where `before` is gone) fall back to "everything".
BEFORE="${{ github.event.before }}"
if [ -z "$BEFORE" ] || ! git cat-file -e "$BEFORE" 2>/dev/null || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
FILES="ALL"
else
FILES=$(git diff --name-only "$BEFORE" "${{ github.sha }}")
fi
# PRs: diff against the target branch instead.
if [ "${{ github.event_name }}" = "pull_request" ]; then
git fetch origin "${{ github.base_ref }}" --depth=1
FILES=$(git diff --name-only "origin/${{ github.base_ref }}" "${{ github.sha }}")
fi
echo "changed files:"; echo "$FILES"
hit() { [ "$FILES" = "ALL" ] || echo "$FILES" | grep -qE "$1"; }
# The workflow itself changing re-runs everything.
if hit '^\.gitea/workflows/ci\.yml$'; then FILES="ALL"; fi
out() { echo "$1=$2" >> "$GITHUB_OUTPUT"; echo " $1=$2"; }
hit '^apps/portal/' && out portal true || out portal false
hit '^apps/booking/' && out booking true || out booking false
hit '^apps/operator/' && out operator true || out operator false
hit '^apps/website/' && out website true || out website false
hit '^services/platform-api/' && out platform_api true || out platform_api false
hit '^infrastructure/production/fleet/apps/' && out infra true || out infra false
if hit '^apps/(portal|booking|operator)/|^services/platform-api/|^infrastructure/production/fleet/apps/'; then
out deploy_any true
else
out deploy_any false
fi
# ── Typecheck (one job per app so each is individually skippable) ─────────
# Node comes from the runner image (catthehacker ships node 24) — NOT
# actions/setup-node, whose shared tool-cache races across concurrent jobs
# ("node: Text file busy"). corepack (bundled with node) reads each app's
# own packageManager — what this per-app monorepo (no root package.json) needs.
tc_portal:
runs-on: ubuntu-latest
needs: [changes]
if: needs.changes.outputs.portal == 'true'
defaults: { run: { working-directory: apps/portal } }
steps:
- uses: actions/checkout@v4
- name: Install + typecheck
run: |
corepack enable
pnpm install --frozen-lockfile
pnpm typecheck
tc_booking:
runs-on: ubuntu-latest
needs: [changes]
if: needs.changes.outputs.booking == 'true'
defaults: { run: { working-directory: apps/booking } }
steps:
- uses: actions/checkout@v4
- name: Install + typecheck
run: |
corepack enable
pnpm install --frozen-lockfile
pnpm typecheck
tc_operator:
runs-on: ubuntu-latest
needs: [changes]
if: needs.changes.outputs.operator == 'true'
defaults: { run: { working-directory: apps/operator } }
steps:
- uses: actions/checkout@v4
- name: Install + typecheck
run: |
corepack enable
pnpm install --frozen-lockfile
pnpm typecheck
tc_website:
runs-on: ubuntu-latest
needs: [changes]
if: needs.changes.outputs.website == 'true'
defaults: { run: { working-directory: apps/website } }
steps:
- uses: actions/checkout@v4
- name: Install + typecheck
run: |
corepack enable
pnpm install --frozen-lockfile
pnpm typecheck
tc_platform_api:
runs-on: ubuntu-latest
needs: [changes]
if: needs.changes.outputs.platform_api == 'true'
defaults: { run: { working-directory: services/platform-api } }
steps:
- uses: actions/checkout@v4
- name: Install + typecheck
run: |
corepack enable
pnpm install --frozen-lockfile
pnpm typecheck
test_platform_api:
runs-on: ubuntu-latest
needs: [changes]
if: needs.changes.outputs.platform_api == 'true'
defaults: { run: { working-directory: services/platform-api } }
steps:
- uses: actions/checkout@v4
- name: Install + test
run: |
corepack enable
pnpm install --frozen-lockfile
pnpm test
# ── Build + push images (only on main, only for changed apps) ─────────────
# Uses REGISTRY_TOKEN (PAT with package read+write) — the per-job
# GITHUB_TOKEN is not accepted by the registry — and the dind sidecar for
# docker build. Tags: :latest for humans, :<sha> for the deploy pin.
build_portal:
runs-on: ubuntu-latest
needs: [changes, tc_portal]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changes.outputs.portal == 'true'
steps:
- uses: actions/checkout@v4
- name: Build + push
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.lastcloud.io -u "${{ github.actor }}" --password-stdin
IMG=git.lastcloud.io/ronnibaslund/dezky/portal
docker build -t "$IMG:latest" -t "$IMG:${{ github.sha }}" apps/portal
docker push "$IMG:latest"
docker push "$IMG:${{ github.sha }}"
build_booking:
runs-on: ubuntu-latest
needs: [changes, tc_booking]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changes.outputs.booking == 'true'
steps:
- uses: actions/checkout@v4
- name: Build + push
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.lastcloud.io -u "${{ github.actor }}" --password-stdin
IMG=git.lastcloud.io/ronnibaslund/dezky/booking
docker build -t "$IMG:latest" -t "$IMG:${{ github.sha }}" apps/booking
docker push "$IMG:latest"
docker push "$IMG:${{ github.sha }}"
build_operator:
runs-on: ubuntu-latest
needs: [changes, tc_operator]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changes.outputs.operator == 'true'
steps:
- uses: actions/checkout@v4
- name: Build + push
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.lastcloud.io -u "${{ github.actor }}" --password-stdin
IMG=git.lastcloud.io/ronnibaslund/dezky/operator
docker build -t "$IMG:latest" -t "$IMG:${{ github.sha }}" apps/operator
docker push "$IMG:latest"
docker push "$IMG:${{ github.sha }}"
build_platform_api:
runs-on: ubuntu-latest
needs: [changes, tc_platform_api, test_platform_api]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changes.outputs.platform_api == 'true'
steps:
- uses: actions/checkout@v4
- name: Build + push
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.lastcloud.io -u "${{ github.actor }}" --password-stdin
IMG=git.lastcloud.io/ronnibaslund/dezky/platform-api
docker build -t "$IMG:latest" -t "$IMG:${{ github.sha }}" services/platform-api
docker push "$IMG:latest"
docker push "$IMG:${{ github.sha }}"
# ── Deploy (only what changed) ─────────────────────────────────────────────
# always() so skipped builds don't block it; the explicit result checks make
# any FAILED build (or its typecheck/test, transitively) abort the deploy.
deploy:
runs-on: ubuntu-latest
needs: [changes, build_portal, build_booking, build_operator, build_platform_api]
if: >-
always() &&
github.event_name == 'push' && github.ref == 'refs/heads/main' &&
needs.changes.outputs.deploy_any == 'true' &&
needs.build_portal.result != 'failure' && needs.build_portal.result != 'cancelled' &&
needs.build_booking.result != 'failure' && needs.build_booking.result != 'cancelled' &&
needs.build_operator.result != 'failure' && needs.build_operator.result != 'cancelled' &&
needs.build_platform_api.result != 'failure' && needs.build_platform_api.result != 'cancelled'
steps:
- uses: actions/checkout@v4
- name: Install kubectl + kustomize
run: |
curl -fsSLo /usr/local/bin/kubectl https://dl.k8s.io/release/v1.33.4/bin/linux/amd64/kubectl
chmod +x /usr/local/bin/kubectl
curl -fsSL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.6.0/kustomize_v5.6.0_linux_amd64.tar.gz \
| tar -xz -C /usr/local/bin kustomize
- name: Deploy to k3s
env:
KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }}
run: |
export KUBECONFIG=/tmp/kubeconfig
echo "$KUBECONFIG_B64" | base64 -d > "$KUBECONFIG"
REG=git.lastcloud.io/ronnibaslund/dezky
SHA=${{ github.sha }}
declare -A CHANGED=(
[portal]=${{ needs.changes.outputs.portal }}
[booking]=${{ needs.changes.outputs.booking }}
[operator]=${{ needs.changes.outputs.operator }}
[platform-api]=${{ needs.changes.outputs.platform_api }}
)
ROLL=""
if [ "${{ needs.changes.outputs.infra }}" = "true" ]; then
# Manifest change: apply the whole kustomization, but pin every
# app to its CURRENT live image (or this push's sha if it was
# rebuilt) so the apply never resets anything to :latest.
cd infrastructure/production/fleet/apps
for app in portal booking platform-api operator; do
if [ "${CHANGED[$app]}" = "true" ]; then
IMG="$REG/$app:$SHA"
else
IMG=$(kubectl -n dezky-apps get deploy "$app" -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null || echo "$REG/$app:latest")
fi
kustomize edit set image "$REG/$app=$IMG"
done
kubectl apply -k .
ROLL="portal booking platform-api operator"
else
# App-only change: roll just the changed apps; nothing else moves.
for app in portal booking platform-api operator; do
if [ "${CHANGED[$app]}" = "true" ]; then
kubectl -n dezky-apps set image "deploy/$app" "$app=$REG/$app:$SHA"
ROLL="$ROLL $app"
fi
done
fi
for app in $ROLL; do
kubectl -n dezky-apps rollout status "deploy/$app" --timeout=180s
done