name: CI on: push: 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: gitea.siegeln.net/cameleer/cameleer-build:1 credentials: username: cameleer password: ${{ secrets.REGISTRY_TOKEN }} steps: - uses: actions/checkout@v4 - name: Configure Gitea Maven Registry run: | mkdir -p ~/.m2 cat > ~/.m2/settings.xml << 'SETTINGS' gitea cameleer ${env.REGISTRY_TOKEN} SETTINGS env: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - name: Cache Maven dependencies uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-maven- - name: Build UI working-directory: ui run: | echo '//gitea.siegeln.net/api/packages/cameleer/npm/:_authToken=${REGISTRY_TOKEN}' >> .npmrc npm ci npm run build env: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} VITE_APP_VERSION: ${{ github.sha }} - name: Build and Test run: mvn clean verify -DskipITs -U --batch-mode docker: needs: build runs-on: ubuntu-latest if: github.event_name == 'push' container: image: gitea.siegeln.net/cameleer/cameleer-docker-builder:1 credentials: username: cameleer password: ${{ secrets.REGISTRY_TOKEN }} steps: - 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: Login to registry run: echo "$REGISTRY_TOKEN" | docker login gitea.siegeln.net -u cameleer --password-stdin env: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - name: Compute branch slug run: | . .gitea/sanitize-branch.sh 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 gitea.siegeln.net/cameleer/binfmt:1 --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" \ $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 \ --push . env: 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 SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) docker buildx build --platform linux/amd64 \ -f ui/Dockerfile \ --build-arg REGISTRY_TOKEN="$REGISTRY_TOKEN" \ --build-arg VITE_APP_VERSION="$SHORT_SHA" \ $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 \ --push ui/ env: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - name: Cleanup local Docker run: docker system prune -af --filter "until=24h" if: always() - name: Cleanup old container images run: | 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 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 env: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} if: always() deploy: needs: docker runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' container: image: alpine/k8s:1.32.3 steps: - 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: Deploy run: | kubectl create namespace cameleer --dry-run=client -o yaml | kubectl apply -f - kubectl create secret docker-registry gitea-registry \ --namespace=cameleer \ --docker-server=gitea.siegeln.net \ --docker-username=cameleer \ --docker-password="$REGISTRY_TOKEN" \ --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic cameleer-auth \ --namespace=cameleer \ --from-literal=CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN="$CAMELEER_AUTH_TOKEN" \ --from-literal=CAMELEER_SERVER_SECURITY_UIUSER="${CAMELEER_UI_USER:-admin}" \ --from-literal=CAMELEER_SERVER_SECURITY_UIPASSWORD="${CAMELEER_UI_PASSWORD:-admin}" \ --from-literal=CAMELEER_SERVER_SECURITY_JWTSECRET="${CAMELEER_JWT_SECRET}" \ --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic postgres-credentials \ --namespace=cameleer \ --from-literal=POSTGRES_USER="$POSTGRES_USER" \ --from-literal=POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \ --from-literal=POSTGRES_DB="${POSTGRES_DB:-cameleer}" \ --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic logto-credentials \ --namespace=cameleer \ --from-literal=PG_USER="${LOGTO_PG_USER:-logto}" \ --from-literal=PG_PASSWORD="${LOGTO_PG_PASSWORD}" \ --from-literal=ENDPOINT="${LOGTO_ENDPOINT}" \ --from-literal=ADMIN_ENDPOINT="${LOGTO_ADMIN_ENDPOINT}" \ --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic clickhouse-credentials \ --namespace=cameleer \ --from-literal=CLICKHOUSE_USER="${CLICKHOUSE_USER:-default}" \ --from-literal=CLICKHOUSE_PASSWORD="$CLICKHOUSE_PASSWORD" \ --dry-run=client -o yaml | kubectl apply -f - kubectl apply -f deploy/postgres.yaml kubectl -n cameleer rollout status statefulset/postgres --timeout=120s kubectl apply -f deploy/clickhouse.yaml kubectl -n cameleer rollout status statefulset/clickhouse --timeout=180s kubectl apply -f deploy/logto.yaml kubectl -n cameleer rollout status deployment/logto --timeout=180s 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 -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 env: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} CAMELEER_AUTH_TOKEN: ${{ secrets.CAMELEER_AUTH_TOKEN }} CAMELEER_JWT_SECRET: ${{ secrets.CAMELEER_JWT_SECRET }} CAMELEER_UI_USER: ${{ secrets.CAMELEER_UI_USER }} CAMELEER_UI_PASSWORD: ${{ secrets.CAMELEER_UI_PASSWORD }} POSTGRES_USER: ${{ secrets.POSTGRES_USER }} POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} POSTGRES_DB: ${{ secrets.POSTGRES_DB }} LOGTO_PG_USER: ${{ secrets.LOGTO_PG_USER }} LOGTO_PG_PASSWORD: ${{ secrets.LOGTO_PG_PASSWORD }} LOGTO_ENDPOINT: ${{ secrets.LOGTO_ENDPOINT }} LOGTO_ADMIN_ENDPOINT: ${{ secrets.LOGTO_ADMIN_ENDPOINT }} CLICKHOUSE_USER: ${{ secrets.CLICKHOUSE_USER }} CLICKHOUSE_PASSWORD: ${{ secrets.CLICKHOUSE_PASSWORD }} deploy-feature: needs: docker runs-on: ubuntu-latest if: github.ref != 'refs/heads/main' && github.event_name == 'push' container: image: alpine/k8s:1.32.3 steps: - 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: | . .gitea/sanitize-branch.sh 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 clickhouse-credentials cameleer-auth; 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 preserving the directory structure so ../../base resolves mkdir -p /tmp/feature-deploy/deploy/overlays cp -r deploy/base /tmp/feature-deploy/deploy/base cp -r deploy/overlays/feature /tmp/feature-deploy/deploy/overlays/feature # Substitute all BRANCH_* placeholders for f in /tmp/feature-deploy/deploy/overlays/feature/*.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 kubectl apply -k /tmp/feature-deploy/deploy/overlays/feature - 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: alpine/k8s:1.32.3 steps: - 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: 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 }}