feat: add feature branch deployments with per-branch isolation
Enable deploying feature branches into isolated environments on the same k3s cluster. Each branch gets its own namespace (cam-<slug>), PostgreSQL schema, and OpenSearch index prefix for data isolation while sharing the underlying infrastructure. - Make OpenSearch index prefix and DB schema configurable via env vars (defaults preserve existing behavior) - Restructure deploy/ into Kustomize base + overlays (main/feature) - Extend CI to build Docker images for all branches, not just main - Add deploy-feature job with namespace creation, secret copying, Traefik Ingress routing (<slug>-api/ui.cameleer.siegeln.net) - Add cleanup-branch job to remove namespace, PG schema, OS indices on branch deletion - Install required tools (git, jq, curl) in CI deploy containers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,15 +2,17 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, 'feature/**', 'fix/**', 'feat/**']
|
||||
tags-ignore:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
delete:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'delete'
|
||||
container:
|
||||
image: maven:3.9-eclipse-temurin-17
|
||||
steps:
|
||||
@@ -60,7 +62,7 @@ jobs:
|
||||
docker:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main'
|
||||
if: github.event_name == 'push'
|
||||
container:
|
||||
image: docker:27
|
||||
steps:
|
||||
@@ -74,15 +76,36 @@ jobs:
|
||||
run: echo "$REGISTRY_TOKEN" | docker login gitea.siegeln.net -u cameleer --password-stdin
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
- name: Compute branch slug
|
||||
run: |
|
||||
sanitize_branch() {
|
||||
echo "$1" | sed -E 's#^(feature|fix|feat|hotfix)/##' \
|
||||
| tr '[:upper:]' '[:lower:]' \
|
||||
| sed 's/[^a-z0-9-]/-/g' \
|
||||
| sed 's/--*/-/g; s/^-//; s/-$//' \
|
||||
| cut -c1-20 \
|
||||
| sed 's/-$//'
|
||||
}
|
||||
if [ "$GITHUB_REF_NAME" = "main" ]; then
|
||||
echo "BRANCH_SLUG=main" >> "$GITHUB_ENV"
|
||||
echo "IMAGE_TAGS=latest" >> "$GITHUB_ENV"
|
||||
else
|
||||
SLUG=$(sanitize_branch "$GITHUB_REF_NAME")
|
||||
echo "BRANCH_SLUG=$SLUG" >> "$GITHUB_ENV"
|
||||
echo "IMAGE_TAGS=branch-$SLUG" >> "$GITHUB_ENV"
|
||||
fi
|
||||
- name: Set up QEMU for cross-platform builds
|
||||
run: docker run --rm --privileged tonistiigi/binfmt --install all
|
||||
- name: Build and push server
|
||||
run: |
|
||||
docker buildx create --use --name cibuilder
|
||||
TAGS="-t gitea.siegeln.net/cameleer/cameleer3-server:${{ github.sha }}"
|
||||
for TAG in $IMAGE_TAGS; do
|
||||
TAGS="$TAGS -t gitea.siegeln.net/cameleer/cameleer3-server:$TAG"
|
||||
done
|
||||
docker buildx build --platform linux/amd64 \
|
||||
--build-arg REGISTRY_TOKEN="$REGISTRY_TOKEN" \
|
||||
-t gitea.siegeln.net/cameleer/cameleer3-server:${{ github.sha }} \
|
||||
-t gitea.siegeln.net/cameleer/cameleer3-server:latest \
|
||||
$TAGS \
|
||||
--cache-from type=registry,ref=gitea.siegeln.net/cameleer/cameleer3-server:buildcache \
|
||||
--cache-to type=registry,ref=gitea.siegeln.net/cameleer/cameleer3-server:buildcache,mode=max \
|
||||
--provenance=false \
|
||||
@@ -91,10 +114,13 @@ jobs:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
- name: Build and push UI
|
||||
run: |
|
||||
TAGS="-t gitea.siegeln.net/cameleer/cameleer3-server-ui:${{ github.sha }}"
|
||||
for TAG in $IMAGE_TAGS; do
|
||||
TAGS="$TAGS -t gitea.siegeln.net/cameleer/cameleer3-server-ui:$TAG"
|
||||
done
|
||||
docker buildx build --platform linux/amd64 \
|
||||
-f ui/Dockerfile \
|
||||
-t gitea.siegeln.net/cameleer/cameleer3-server-ui:${{ github.sha }} \
|
||||
-t gitea.siegeln.net/cameleer/cameleer3-server-ui:latest \
|
||||
$TAGS \
|
||||
--cache-from type=registry,ref=gitea.siegeln.net/cameleer/cameleer3-server-ui:buildcache \
|
||||
--cache-to type=registry,ref=gitea.siegeln.net/cameleer/cameleer3-server-ui:buildcache,mode=max \
|
||||
--provenance=false \
|
||||
@@ -110,13 +136,28 @@ jobs:
|
||||
API="https://gitea.siegeln.net/api/v1"
|
||||
AUTH="Authorization: token ${REGISTRY_TOKEN}"
|
||||
CURRENT_SHA="${{ github.sha }}"
|
||||
# Build list of tags to keep
|
||||
KEEP_TAGS="latest buildcache $CURRENT_SHA"
|
||||
if [ "$BRANCH_SLUG" != "main" ]; then
|
||||
KEEP_TAGS="$KEEP_TAGS branch-$BRANCH_SLUG"
|
||||
fi
|
||||
for PKG in cameleer3-server cameleer3-server-ui; do
|
||||
curl -sf -H "$AUTH" "$API/packages/cameleer/container/$PKG" | \
|
||||
jq -r '.[] | "\(.id) \(.version)"' | \
|
||||
while read id version; do
|
||||
if [ "$version" != "latest" ] && [ "$version" != "$CURRENT_SHA" ]; then
|
||||
echo "Deleting old image tag: $PKG:$version"
|
||||
curl -sf -X DELETE -H "$AUTH" "$API/packages/cameleer/container/$PKG/$version"
|
||||
SHOULD_KEEP=false
|
||||
for KEEP in $KEEP_TAGS; do
|
||||
if [ "$version" = "$KEEP" ]; then
|
||||
SHOULD_KEEP=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$SHOULD_KEEP" = "false" ]; then
|
||||
# Only clean up images for this branch
|
||||
if [ "$BRANCH_SLUG" = "main" ] || echo "$version" | grep -q "branch-$BRANCH_SLUG"; then
|
||||
echo "Deleting old image tag: $PKG:$version"
|
||||
curl -sf -X DELETE -H "$AUTH" "$API/packages/cameleer/container/$PKG/$version"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
@@ -131,6 +172,9 @@ jobs:
|
||||
container:
|
||||
image: bitnami/kubectl:latest
|
||||
steps:
|
||||
- name: Install required tools
|
||||
run: |
|
||||
apt-get update && apt-get install -y --no-install-recommends git ca-certificates
|
||||
- name: Checkout
|
||||
run: |
|
||||
git clone --depth=1 --branch=${GITHUB_REF_NAME} https://cameleer:${REGISTRY_TOKEN}@gitea.siegeln.net/${GITHUB_REPOSITORY}.git .
|
||||
@@ -198,12 +242,11 @@ jobs:
|
||||
kubectl apply -f deploy/authentik.yaml
|
||||
kubectl -n cameleer rollout status deployment/authentik-server --timeout=180s
|
||||
|
||||
kubectl apply -f deploy/server.yaml
|
||||
kubectl apply -k deploy/overlays/main
|
||||
kubectl -n cameleer set image deployment/cameleer3-server \
|
||||
server=gitea.siegeln.net/cameleer/cameleer3-server:${{ github.sha }}
|
||||
kubectl -n cameleer rollout status deployment/cameleer3-server --timeout=120s
|
||||
|
||||
kubectl apply -f deploy/ui.yaml
|
||||
kubectl -n cameleer set image deployment/cameleer3-ui \
|
||||
ui=gitea.siegeln.net/cameleer/cameleer3-server-ui:${{ github.sha }}
|
||||
kubectl -n cameleer rollout status deployment/cameleer3-ui --timeout=120s
|
||||
@@ -225,3 +268,147 @@ jobs:
|
||||
CAMELEER_OIDC_ISSUER: ${{ secrets.CAMELEER_OIDC_ISSUER }}
|
||||
CAMELEER_OIDC_CLIENT_ID: ${{ secrets.CAMELEER_OIDC_CLIENT_ID }}
|
||||
CAMELEER_OIDC_CLIENT_SECRET: ${{ secrets.CAMELEER_OIDC_CLIENT_SECRET }}
|
||||
|
||||
deploy-feature:
|
||||
needs: docker
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref != 'refs/heads/main' && github.event_name == 'push'
|
||||
container:
|
||||
image: bitnami/kubectl:latest
|
||||
steps:
|
||||
- name: Install required tools
|
||||
run: |
|
||||
apt-get update && apt-get install -y --no-install-recommends git jq sed ca-certificates
|
||||
- name: Checkout
|
||||
run: |
|
||||
git clone --depth=1 --branch=${GITHUB_REF_NAME} https://cameleer:${REGISTRY_TOKEN}@gitea.siegeln.net/${GITHUB_REPOSITORY}.git .
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
- name: Configure kubectl
|
||||
run: |
|
||||
mkdir -p ~/.kube
|
||||
echo "$KUBECONFIG_B64" | base64 -d > ~/.kube/config
|
||||
env:
|
||||
KUBECONFIG_B64: ${{ secrets.KUBECONFIG_BASE64 }}
|
||||
- name: Compute branch variables
|
||||
run: |
|
||||
sanitize_branch() {
|
||||
echo "$1" | sed -E 's#^(feature|fix|feat|hotfix)/##' \
|
||||
| tr '[:upper:]' '[:lower:]' \
|
||||
| sed 's/[^a-z0-9-]/-/g' \
|
||||
| sed 's/--*/-/g; s/^-//; s/-$//' \
|
||||
| cut -c1-20 \
|
||||
| sed 's/-$//'
|
||||
}
|
||||
SLUG=$(sanitize_branch "$GITHUB_REF_NAME")
|
||||
NS="cam-${SLUG}"
|
||||
SCHEMA="cam_$(echo $SLUG | tr '-' '_')"
|
||||
echo "BRANCH_SLUG=$SLUG" >> "$GITHUB_ENV"
|
||||
echo "BRANCH_NS=$NS" >> "$GITHUB_ENV"
|
||||
echo "BRANCH_SCHEMA=$SCHEMA" >> "$GITHUB_ENV"
|
||||
- name: Create namespace
|
||||
run: kubectl create namespace "$BRANCH_NS" --dry-run=client -o yaml | kubectl apply -f -
|
||||
- name: Copy secrets from cameleer namespace
|
||||
run: |
|
||||
for SECRET in gitea-registry postgres-credentials opensearch-credentials cameleer-auth cameleer-oidc; do
|
||||
kubectl get secret "$SECRET" -n cameleer -o json \
|
||||
| jq 'del(.metadata.namespace, .metadata.resourceVersion, .metadata.uid, .metadata.creationTimestamp, .metadata.managedFields)' \
|
||||
| kubectl apply -n "$BRANCH_NS" -f -
|
||||
done
|
||||
- name: Substitute placeholders and deploy
|
||||
run: |
|
||||
# Work on a copy so we don't modify the repo
|
||||
cp -r deploy/overlays/feature /tmp/feature-overlay
|
||||
# Substitute all BRANCH_* placeholders
|
||||
for f in /tmp/feature-overlay/*.yaml; do
|
||||
sed -i \
|
||||
-e "s|BRANCH_NAMESPACE|${BRANCH_NS}|g" \
|
||||
-e "s|BRANCH_SCHEMA|${BRANCH_SCHEMA}|g" \
|
||||
-e "s|BRANCH_SLUG|${BRANCH_SLUG}|g" \
|
||||
-e "s|BRANCH_SHA|${{ github.sha }}|g" \
|
||||
"$f"
|
||||
done
|
||||
# Fix kustomization base path (since we moved the overlay)
|
||||
sed -i 's|../../base|'"$(pwd)"'/deploy/base|g' /tmp/feature-overlay/kustomization.yaml
|
||||
kubectl apply -k /tmp/feature-overlay
|
||||
- name: Wait for init-job
|
||||
run: |
|
||||
kubectl -n "$BRANCH_NS" wait --for=condition=complete job/init-schema --timeout=60s || \
|
||||
echo "Warning: init-schema job did not complete in time"
|
||||
- name: Wait for server rollout
|
||||
run: kubectl -n "$BRANCH_NS" rollout status deployment/cameleer3-server --timeout=120s
|
||||
- name: Wait for UI rollout
|
||||
run: kubectl -n "$BRANCH_NS" rollout status deployment/cameleer3-ui --timeout=60s
|
||||
- name: Print deployment URLs
|
||||
run: |
|
||||
echo "===================================="
|
||||
echo "Feature branch deployed!"
|
||||
echo "API: http://${BRANCH_SLUG}-api.cameleer.siegeln.net"
|
||||
echo "UI: http://${BRANCH_SLUG}.cameleer.siegeln.net"
|
||||
echo "===================================="
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
cleanup-branch:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'delete' && github.event.ref_type == 'branch'
|
||||
container:
|
||||
image: bitnami/kubectl:latest
|
||||
steps:
|
||||
- name: Install required tools
|
||||
run: |
|
||||
apt-get update && apt-get install -y --no-install-recommends curl jq ca-certificates
|
||||
- name: Configure kubectl
|
||||
run: |
|
||||
mkdir -p ~/.kube
|
||||
echo "$KUBECONFIG_B64" | base64 -d > ~/.kube/config
|
||||
env:
|
||||
KUBECONFIG_B64: ${{ secrets.KUBECONFIG_BASE64 }}
|
||||
- name: Compute branch variables
|
||||
run: |
|
||||
sanitize_branch() {
|
||||
echo "$1" | sed -E 's#^(feature|fix|feat|hotfix)/##' \
|
||||
| tr '[:upper:]' '[:lower:]' \
|
||||
| sed 's/[^a-z0-9-]/-/g' \
|
||||
| sed 's/--*/-/g; s/^-//; s/-$//' \
|
||||
| cut -c1-20 \
|
||||
| sed 's/-$//'
|
||||
}
|
||||
SLUG=$(sanitize_branch "${{ github.event.ref }}")
|
||||
NS="cam-${SLUG}"
|
||||
SCHEMA="cam_$(echo $SLUG | tr '-' '_')"
|
||||
echo "BRANCH_SLUG=$SLUG" >> "$GITHUB_ENV"
|
||||
echo "BRANCH_NS=$NS" >> "$GITHUB_ENV"
|
||||
echo "BRANCH_SCHEMA=$SCHEMA" >> "$GITHUB_ENV"
|
||||
- name: Delete namespace
|
||||
run: kubectl delete namespace "$BRANCH_NS" --ignore-not-found
|
||||
- name: Drop PostgreSQL schema
|
||||
run: |
|
||||
kubectl run cleanup-schema-${BRANCH_SLUG} \
|
||||
--namespace=cameleer \
|
||||
--image=postgres:16 \
|
||||
--restart=Never \
|
||||
--env="PGPASSWORD=$(kubectl get secret postgres-credentials -n cameleer -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d)" \
|
||||
--command -- sh -c "psql -h postgres -U $(kubectl get secret postgres-credentials -n cameleer -o jsonpath='{.data.POSTGRES_USER}' | base64 -d) -d cameleer3 -c 'DROP SCHEMA IF EXISTS ${BRANCH_SCHEMA} CASCADE'"
|
||||
kubectl wait --for=condition=Ready pod/cleanup-schema-${BRANCH_SLUG} -n cameleer --timeout=30s || true
|
||||
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/cleanup-schema-${BRANCH_SLUG} -n cameleer --timeout=60s || true
|
||||
kubectl delete pod cleanup-schema-${BRANCH_SLUG} -n cameleer --ignore-not-found
|
||||
- name: Delete OpenSearch indices
|
||||
run: |
|
||||
kubectl run cleanup-indices-${BRANCH_SLUG} \
|
||||
--namespace=cameleer \
|
||||
--image=curlimages/curl:latest \
|
||||
--restart=Never \
|
||||
--command -- curl -sf -X DELETE "http://opensearch:9200/cam-${BRANCH_SLUG}-*"
|
||||
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/cleanup-indices-${BRANCH_SLUG} -n cameleer --timeout=60s || true
|
||||
kubectl delete pod cleanup-indices-${BRANCH_SLUG} -n cameleer --ignore-not-found
|
||||
- name: Cleanup Docker images
|
||||
run: |
|
||||
API="https://gitea.siegeln.net/api/v1"
|
||||
AUTH="Authorization: token ${REGISTRY_TOKEN}"
|
||||
for PKG in cameleer3-server cameleer3-server-ui; do
|
||||
# Delete branch-specific tag
|
||||
curl -sf -X DELETE -H "$AUTH" "$API/packages/cameleer/container/$PKG/branch-${BRANCH_SLUG}" || true
|
||||
done
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
Reference in New Issue
Block a user