Skip to content

chore(release): v0.3.14 #191

chore(release): v0.3.14

chore(release): v0.3.14 #191

Workflow file for this run

name: Build, push & deploy
# On every push to main, build images for whatever changed (api / admin / app)
# and push to GHCR under ghcr.io/cleanslice/ranch-* tagged with the commit
# SHA only — `:latest` and the rolling deploy are reserved for tag pushes
# (v*), which release-platform.yaml dispatches automatically after a version
# bump. This keeps `:latest` deterministic and avoids the push-vs-tag race
# where a parallel main-branch build would overwrite the tag-run's :latest
# with a 0.0.0-<sha> image and then deploy that stale tag.
#
# Required repo secrets:
# GHCR_PAT — GitHub PAT with write:packages + read:packages
# KUBECONFIG_B64 — base64 of ranch cluster kubeconfig
#
# Required repo variables (Settings → Variables):
# API_URL — e.g. https://api.ranch.cleanslice.org (baked into admin + app Nuxt build)
on:
push:
branches: [main]
paths:
- 'api/**'
- 'admin/**'
- 'app/**'
- 'runtime/**'
- 'rancher/**'
tags:
- 'v*'
workflow_dispatch:
inputs:
target:
description: 'Which app to build (api, admin, app, all)'
required: true
default: 'all'
type: choice
options: [api, admin, app, all]
env:
REGISTRY: ghcr.io
REGISTRY_USER: dmitriyzhuk
IMAGE_PREFIX: ghcr.io/cleanslice/ranch
jobs:
changes:
runs-on: ubuntu-latest
outputs:
api: ${{ steps.tag.outputs.all || steps.filter.outputs.api || steps.manual.outputs.api }}
admin: ${{ steps.tag.outputs.all || steps.filter.outputs.admin || steps.manual.outputs.admin }}
app: ${{ steps.tag.outputs.all || steps.filter.outputs.app || steps.manual.outputs.app }}
version: ${{ steps.version.outputs.value }}
tag: ${{ steps.version.outputs.tag }}
# Release runs are the only ones that publish :latest and roll out
# deployments. Push-on-main builds keep their :sha images on GHCR for
# debugging but do not touch the live cluster.
is_release: ${{ steps.tag.outputs.all || 'false' }}
steps:
- uses: actions/checkout@v4
- id: tag
if: startsWith(github.ref, 'refs/tags/v')
run: echo "all=true" >> $GITHUB_OUTPUT
- id: version
run: |
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
VALUE="${GITHUB_REF_NAME#v}"
echo "value=${VALUE}" >> $GITHUB_OUTPUT
echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
else
echo "value=0.0.0-${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
echo "tag=${GITHUB_SHA}" >> $GITHUB_OUTPUT
fi
- id: filter
if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')
uses: dorny/paths-filter@v3
with:
filters: |
api:
- 'api/**'
- 'runtime/**'
- 'rancher/**'
admin:
- 'admin/**'
app:
- 'app/**'
- id: manual
if: github.event_name == 'workflow_dispatch'
run: |
case "${{ inputs.target }}" in
api) echo "api=true" >> $GITHUB_OUTPUT; echo "admin=false" >> $GITHUB_OUTPUT; echo "app=false" >> $GITHUB_OUTPUT ;;
admin) echo "api=false" >> $GITHUB_OUTPUT; echo "admin=true" >> $GITHUB_OUTPUT; echo "app=false" >> $GITHUB_OUTPUT ;;
app) echo "api=false" >> $GITHUB_OUTPUT; echo "admin=false" >> $GITHUB_OUTPUT; echo "app=true" >> $GITHUB_OUTPUT ;;
all) echo "api=true" >> $GITHUB_OUTPUT; echo "admin=true" >> $GITHUB_OUTPUT; echo "app=true" >> $GITHUB_OUTPUT ;;
esac
build-api:
needs: changes
if: needs.changes.outputs.api == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Bake runtime/ source + deps into the api image so paddock CLI's
# dynamic-import of <repo>/runtime/src/runtime.ts works in prod.
# Runtime lives in a sibling repo, so we clone it into the build
# context root.
- name: Checkout runtime
uses: actions/checkout@v4
with:
repository: CleanSlice/runtime
token: ${{ secrets.GHCR_PAT }}
path: runtime
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.REGISTRY_USER }}
password: ${{ secrets.GHCR_PAT }}
- uses: docker/build-push-action@v6
with:
context: .
file: ./api/Dockerfile
platforms: linux/amd64
push: true
build-args: |
RANCH_VERSION=${{ needs.changes.outputs.version }}
tags: |
${{ env.IMAGE_PREFIX }}-api:${{ needs.changes.outputs.tag }}
${{ needs.changes.outputs.is_release == 'true' && format('{0}-api:latest', env.IMAGE_PREFIX) || '' }}
cache-from: type=gha,scope=ranch-api
cache-to: type=gha,mode=max,scope=ranch-api
build-admin:
needs: changes
if: needs.changes.outputs.admin == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.REGISTRY_USER }}
password: ${{ secrets.GHCR_PAT }}
- uses: docker/build-push-action@v6
with:
context: .
file: ./admin/Dockerfile
platforms: linux/amd64
push: true
build-args: |
API_URL=${{ vars.API_URL }}
RANCH_VERSION=${{ needs.changes.outputs.version }}
tags: |
${{ env.IMAGE_PREFIX }}-admin:${{ needs.changes.outputs.tag }}
${{ needs.changes.outputs.is_release == 'true' && format('{0}-admin:latest', env.IMAGE_PREFIX) || '' }}
cache-from: type=gha,scope=ranch-admin
cache-to: type=gha,mode=max,scope=ranch-admin
build-app:
needs: changes
if: needs.changes.outputs.app == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.REGISTRY_USER }}
password: ${{ secrets.GHCR_PAT }}
- uses: docker/build-push-action@v6
with:
context: .
file: ./app/Dockerfile
platforms: linux/amd64
push: true
build-args: |
API_URL=${{ vars.API_URL }}
RANCH_VERSION=${{ needs.changes.outputs.version }}
tags: |
${{ env.IMAGE_PREFIX }}-app:${{ needs.changes.outputs.tag }}
${{ needs.changes.outputs.is_release == 'true' && format('{0}-app:latest', env.IMAGE_PREFIX) || '' }}
cache-from: type=gha,scope=ranch-app
cache-to: type=gha,mode=max,scope=ranch-app
deploy:
needs: [changes, build-api, build-admin, build-app]
# Deploy only from tag pushes (release runs). Push-on-main builds publish
# their :sha image but never touch the live cluster — this avoids racing
# with the tag-run that should be the authoritative source of :latest.
if: |
always() &&
needs.changes.outputs.is_release == 'true' &&
(needs.build-api.result == 'success' ||
needs.build-admin.result == 'success' ||
needs.build-app.result == 'success')
runs-on: ubuntu-latest
steps:
- name: Configure kubectl
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_B64 }}" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
kubectl version --client=true
- name: Rollout ranch-api
if: needs.changes.outputs.api == 'true' && needs.build-api.result == 'success'
env:
DEPLOY: ranch-api
NS: platform
run: |
kubectl -n "$NS" rollout restart deploy/"$DEPLOY"
if ! kubectl -n "$NS" rollout status deploy/"$DEPLOY" --timeout=10m; then
echo "::group::describe deploy/$DEPLOY"
kubectl -n "$NS" describe deploy/"$DEPLOY" || true
echo "::endgroup::"
echo "::group::pods for $DEPLOY"
{ kubectl -n "$NS" get pods -o wide | head -1; kubectl -n "$NS" get pods -o wide | grep "^$DEPLOY-" || true; }
echo "::endgroup::"
echo "::group::describe pods"
for p in $(kubectl -n "$NS" get pods -o name | grep "/$DEPLOY-" || true); do
kubectl -n "$NS" describe "$p" | tail -80 || true
echo "---"
done
echo "::endgroup::"
echo "::group::recent namespace events"
kubectl -n "$NS" get events --sort-by=.lastTimestamp | tail -50 || true
echo "::endgroup::"
echo "::group::pod logs (current)"
for p in $(kubectl -n "$NS" get pods -o name | grep "/$DEPLOY-" || true); do
echo "=== $p ==="
kubectl -n "$NS" logs "$p" --tail=200 --all-containers --timestamps || true
done
echo "::endgroup::"
echo "::group::pod logs (previous)"
for p in $(kubectl -n "$NS" get pods -o name | grep "/$DEPLOY-" || true); do
echo "=== $p ==="
kubectl -n "$NS" logs "$p" --previous --tail=200 --all-containers --timestamps || true
done
echo "::endgroup::"
exit 1
fi
- name: Rollout ranch-admin
if: needs.changes.outputs.admin == 'true' && needs.build-admin.result == 'success'
env:
DEPLOY: ranch-admin
NS: platform
run: |
kubectl -n "$NS" rollout restart deploy/"$DEPLOY"
if ! kubectl -n "$NS" rollout status deploy/"$DEPLOY" --timeout=10m; then
echo "::group::describe deploy/$DEPLOY"
kubectl -n "$NS" describe deploy/"$DEPLOY" || true
echo "::endgroup::"
echo "::group::pods for $DEPLOY"
{ kubectl -n "$NS" get pods -o wide | head -1; kubectl -n "$NS" get pods -o wide | grep "^$DEPLOY-" || true; }
echo "::endgroup::"
echo "::group::describe pods"
for p in $(kubectl -n "$NS" get pods -o name | grep "/$DEPLOY-" || true); do
kubectl -n "$NS" describe "$p" | tail -80 || true
echo "---"
done
echo "::endgroup::"
echo "::group::recent namespace events"
kubectl -n "$NS" get events --sort-by=.lastTimestamp | tail -50 || true
echo "::endgroup::"
echo "::group::pod logs (current)"
for p in $(kubectl -n "$NS" get pods -o name | grep "/$DEPLOY-" || true); do
echo "=== $p ==="
kubectl -n "$NS" logs "$p" --tail=200 --all-containers --timestamps || true
done
echo "::endgroup::"
echo "::group::pod logs (previous)"
for p in $(kubectl -n "$NS" get pods -o name | grep "/$DEPLOY-" || true); do
echo "=== $p ==="
kubectl -n "$NS" logs "$p" --previous --tail=200 --all-containers --timestamps || true
done
echo "::endgroup::"
exit 1
fi
- name: Rollout ranch-app
if: needs.changes.outputs.app == 'true' && needs.build-app.result == 'success'
env:
DEPLOY: ranch-app
NS: platform
run: |
kubectl -n "$NS" rollout restart deploy/"$DEPLOY"
if ! kubectl -n "$NS" rollout status deploy/"$DEPLOY" --timeout=10m; then
echo "::group::describe deploy/$DEPLOY"
kubectl -n "$NS" describe deploy/"$DEPLOY" || true
echo "::endgroup::"
echo "::group::pods for $DEPLOY"
{ kubectl -n "$NS" get pods -o wide | head -1; kubectl -n "$NS" get pods -o wide | grep "^$DEPLOY-" || true; }
echo "::endgroup::"
echo "::group::describe pods"
for p in $(kubectl -n "$NS" get pods -o name | grep "/$DEPLOY-" || true); do
kubectl -n "$NS" describe "$p" | tail -80 || true
echo "---"
done
echo "::endgroup::"
echo "::group::recent namespace events"
kubectl -n "$NS" get events --sort-by=.lastTimestamp | tail -50 || true
echo "::endgroup::"
echo "::group::pod logs (current)"
for p in $(kubectl -n "$NS" get pods -o name | grep "/$DEPLOY-" || true); do
echo "=== $p ==="
kubectl -n "$NS" logs "$p" --tail=200 --all-containers --timestamps || true
done
echo "::endgroup::"
echo "::group::pod logs (previous)"
for p in $(kubectl -n "$NS" get pods -o name | grep "/$DEPLOY-" || true); do
echo "=== $p ==="
kubectl -n "$NS" logs "$p" --previous --tail=200 --all-containers --timestamps || true
done
echo "::endgroup::"
exit 1
fi
- name: Smoke test
if: |
needs.changes.outputs.api == 'true' ||
needs.changes.outputs.admin == 'true' ||
needs.changes.outputs.app == 'true'
run: |
# `kubectl rollout status` returns as soon as the pod is Ready, but
# Traefik takes a few seconds to swap its upstream — so a curl
# immediately after rollout occasionally hits the old (now-gone)
# endpoint and gets a 503. Retry with a short backoff instead of
# racing the ingress.
probe() {
local name="$1" url="$2"
local code
for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$url" || echo "000")
if [ "$code" = "200" ]; then
echo "$name=$code"
return 0
fi
echo "attempt $i: $name=$code, retrying in 5s..."
sleep 5
done
echo "$name=$code (gave up after 12 attempts)"
return 1
}
probe api https://api.ranch.cleanslice.org/setup/status
probe admin https://admin.ranch.cleanslice.org/login
probe app https://ranch.cleanslice.org/