a5d82903af
ci / tc_operator (push) Successful in 22s
ci / tc_website (push) Successful in 19s
ci / build_booking (push) Successful in 41s
ci / deploy (push) Successful in 8s
ci / build_operator (push) Successful in 37s
ci / build_platform_api (push) Successful in 36s
ci / tc_portal (push) Failing after 26s
ci / build_portal (push) Has been skipped
ci / changes (push) Successful in 4s
ci / tc_booking (push) Successful in 22s
ci / tc_platform_api (push) Successful in 22s
ci / test_platform_api (push) Successful in 32s
When an app's typecheck failed, its build job was SKIPPED — which the
deploy condition tolerates (so other apps still ship) — but the deploy
script keyed on the change flags alone and pinned the never-built image
tag, ImagePullBackOff'ing the app (happened to portal on f6bac10).
Deployable now means changed AND build result == success; otherwise the
app keeps its live image, including in the manifest-apply path.
296 lines
13 KiB
YAML
296 lines
13 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 }}
|
|
# Build results, not just change flags: an app whose typecheck/test
|
|
# failed has result=skipped on its build — its image for this SHA
|
|
# does NOT exist, and pinning it would ImagePullBackOff the app.
|
|
BUILT_PORTAL: ${{ needs.build_portal.result }}
|
|
BUILT_BOOKING: ${{ needs.build_booking.result }}
|
|
BUILT_OPERATOR: ${{ needs.build_operator.result }}
|
|
BUILT_PLATFORM_API: ${{ needs.build_platform_api.result }}
|
|
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 }}
|
|
)
|
|
declare -A BUILT=(
|
|
[portal]=$BUILT_PORTAL
|
|
[booking]=$BUILT_BOOKING
|
|
[operator]=$BUILT_OPERATOR
|
|
[platform-api]=$BUILT_PLATFORM_API
|
|
)
|
|
# Deployable = changed AND its image was actually built this run.
|
|
deployable() { [ "${CHANGED[$1]}" = "true" ] && [ "${BUILT[$1]}" = "success" ]; }
|
|
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 deployable "$app"; 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 deployable "$app"; 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
|