Files
cloud-host/.gitea/workflows/build-deploy.yaml
T
keyhan 22359be40e fix(platform): apply production hardening from audit plan
Close billing, tenancy, migration, build, and CI/CD gaps identified in the
audit: wallet/gateway guards, full-UUID namespaces, idempotent migrations with
base schema, stateful service stability, safer Dockerfiles/git builds, and
platform chart hardening (Redis auth, RollingUpdate, backups, Swagger off).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 19:35:07 +03:30

289 lines
12 KiB
YAML

name: Build and Deploy Platform
on:
push:
branches: [main]
paths-ignore:
- "**.md"
workflow_dispatch:
# Serialize builds so parallel pushes don't race on the GitOps values update.
concurrency:
group: build-deploy-platform
cancel-in-progress: false
env:
# PULL_REGISTRY: kubelet pulls via k3s mirror → harbor-core (matches registry-pull-secret)
PULL_REGISTRY: registry.abrban.com
# PUSH_REGISTRY: kaniko pushes directly to harbor-registry (internal, no TLS)
PUSH_REGISTRY: harbor-registry.cloudhost.svc.cluster.local:5000
PROJECT: abrban
BUILD_NS: cloudhost-builds
GITEA_HOST: gitea-http.gitea.svc.cluster.local:3000
# PAT of the "ci" user, stored as repo secret CI_TOKEN (names starting with GITEA_ are reserved)
GITEA_TOKEN: ${{ secrets.CI_TOKEN }}
REPO_PATH: abrban/cloud-host.git
GITOPS_REPO_PATH: abrban/cloud-host-gitops.git
jobs:
build-and-deploy:
runs-on: abrban-builder
steps:
- name: Checkout
shell: sh
run: |
git clone --depth=1 --branch main "http://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH}" workspace
cd workspace
echo "Checked out $(git rev-parse --short HEAD)"
- name: Set image tag
shell: sh
run: |
cd workspace
SHA="$(git rev-parse --short HEAD)"
TAG="$(date +%Y%m%d-%H%M)-${SHA}"
echo "IMAGE_TAG=${TAG}" >> "$GITHUB_ENV"
echo "Build tag: ${TAG}"
- name: Define job waiter
shell: sh
run: |
# wait_for_job <name>: exit 0 on Complete, exit 1 (with kaniko logs) on Failed/timeout
cat > wait_for_job.sh <<'ENDSCRIPT'
#!/bin/sh
JOB="$1"
DEADLINE=$(( $(date +%s) + 2400 ))
while :; do
CONDS="$(kubectl -n ${BUILD_NS} get job/${JOB} -o jsonpath='{range .status.conditions[*]}{.type}={.status} {end}' 2>/dev/null)"
case "$CONDS" in
*Complete=True*) echo "Job ${JOB} completed"; exit 0 ;;
*Failed=True*)
echo "Job ${JOB} FAILED — kaniko logs:"
kubectl -n ${BUILD_NS} logs job/${JOB} -c kaniko --tail=100 || true
exit 1 ;;
esac
if [ "$(date +%s)" -gt "$DEADLINE" ]; then
echo "Timed out waiting for job ${JOB} — kaniko logs:"
kubectl -n ${BUILD_NS} logs job/${JOB} -c kaniko --tail=100 || true
exit 1
fi
sleep 15
done
ENDSCRIPT
chmod +x wait_for_job.sh
- name: Run backend tests (Job)
shell: sh
run: |
JOB_NAME="test-be-$(echo $IMAGE_TAG | tr '.:' '-' | cut -c1-50)"
cat <<ENDJOB | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: ${JOB_NAME}
namespace: ${BUILD_NS}
spec:
ttlSecondsAfterFinished: 3600
backoffLimit: 0
template:
spec:
restartPolicy: Never
imagePullSecrets:
- name: registry-pull-secret
containers:
- name: test
image: ${PULL_REGISTRY}/${PROJECT}/node:24-alpine
envFrom:
- secretRef:
name: registry-egress-proxy
command:
- sh
- -c
- |
apk add --no-cache git &&
git clone --depth=1 --branch main http://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH} /workspace &&
cd /workspace/backend &&
npm ci --legacy-peer-deps &&
npm run test -- --ci --runInBand
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { cpu: "2", memory: 3Gi }
ENDJOB
echo "Waiting for backend test job: ${JOB_NAME}"
# Reuse the waiter but read logs from the "test" container on failure
DEADLINE=$(( $(date +%s) + 1800 ))
while :; do
CONDS="$(kubectl -n ${BUILD_NS} get job/${JOB_NAME} -o jsonpath='{range .status.conditions[*]}{.type}={.status} {end}' 2>/dev/null)"
case "$CONDS" in
*Complete=True*) echo "Tests passed"; break ;;
*Failed=True*)
echo "Tests FAILED — logs:"
kubectl -n ${BUILD_NS} logs job/${JOB_NAME} -c test --tail=200 || true
exit 1 ;;
esac
if [ "$(date +%s)" -gt "$DEADLINE" ]; then
echo "Timed out waiting for tests — logs:"
kubectl -n ${BUILD_NS} logs job/${JOB_NAME} -c test --tail=200 || true
exit 1
fi
sleep 15
done
- name: Build backend image (Kaniko Job)
shell: sh
run: |
JOB_NAME="build-be-$(echo $IMAGE_TAG | tr '.:' '-' | cut -c1-50)"
cat <<ENDJOB | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: ${JOB_NAME}
namespace: ${BUILD_NS}
spec:
ttlSecondsAfterFinished: 3600
backoffLimit: 2
template:
spec:
restartPolicy: Never
imagePullSecrets:
- name: registry-pull-secret
initContainers:
- name: clone
# alpine/git ships git — no flaky apk install at build time
image: ${PULL_REGISTRY}/${PROJECT}/alpine-git:2.43.0
command:
- sh
- -c
- git clone --depth=1 --branch main http://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH} /workspace
volumeMounts:
- name: ws
mountPath: /workspace
containers:
- name: kaniko
image: ${PULL_REGISTRY}/${PROJECT}/kaniko-executor:v1.27.6-debug
# Base image (node:24-alpine) is seeded in Harbor abrban/ — avoids
# flaky direct pulls from docker.io through the egress proxy.
envFrom:
- secretRef:
name: registry-egress-proxy
args:
- --dockerfile=/workspace/backend/Dockerfile
- --context=dir:///workspace/backend
- --build-arg=BASE_IMAGE=${PUSH_REGISTRY}/${PROJECT}/node:24-alpine
- --destination=${PUSH_REGISTRY}/${PROJECT}/cloudhost-backend:${IMAGE_TAG}
- --insecure
- --insecure-pull
- --insecure-registry=${PUSH_REGISTRY}
- --skip-tls-verify
- --push-retry=2
volumeMounts:
- name: ws
mountPath: /workspace
- name: docker-config
mountPath: /kaniko/.docker
volumes:
- name: ws
emptyDir: {}
- name: docker-config
secret:
secretName: kaniko-harbor-auth
items:
- key: .dockerconfigjson
path: config.json
ENDJOB
echo "Waiting for backend build job: ${JOB_NAME}"
./wait_for_job.sh "${JOB_NAME}"
echo "Backend build done"
- name: Build frontend image (Kaniko Job)
shell: sh
run: |
JOB_NAME="build-fe-$(echo $IMAGE_TAG | tr '.:' '-' | cut -c1-50)"
cat <<ENDJOB | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: ${JOB_NAME}
namespace: ${BUILD_NS}
spec:
ttlSecondsAfterFinished: 3600
backoffLimit: 2
template:
spec:
restartPolicy: Never
imagePullSecrets:
- name: registry-pull-secret
initContainers:
- name: clone
image: ${PULL_REGISTRY}/${PROJECT}/alpine-git:2.43.0
command:
- sh
- -c
- git clone --depth=1 --branch main http://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH} /workspace
volumeMounts:
- name: ws
mountPath: /workspace
containers:
- name: kaniko
image: ${PULL_REGISTRY}/${PROJECT}/kaniko-executor:v1.27.6-debug
envFrom:
- secretRef:
name: registry-egress-proxy
args:
- --dockerfile=/workspace/frontend/Dockerfile
- --context=dir:///workspace/frontend
- --build-arg=BASE_IMAGE=${PUSH_REGISTRY}/${PROJECT}/node:24-alpine
- --build-arg=NEXT_PUBLIC_API_URL=https://api.abrban.com
- --destination=${PUSH_REGISTRY}/${PROJECT}/cloudhost-frontend:${IMAGE_TAG}
- --insecure
- --insecure-pull
- --insecure-registry=${PUSH_REGISTRY}
- --skip-tls-verify
- --push-retry=2
volumeMounts:
- name: ws
mountPath: /workspace
- name: docker-config
mountPath: /kaniko/.docker
volumes:
- name: ws
emptyDir: {}
- name: docker-config
secret:
secretName: kaniko-harbor-auth
items:
- key: .dockerconfigjson
path: config.json
ENDJOB
echo "Waiting for frontend build job: ${JOB_NAME}"
./wait_for_job.sh "${JOB_NAME}"
echo "Frontend build done"
- name: Update GitOps repo and push
shell: sh
run: |
git clone --depth=1 --branch main "http://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${GITOPS_REPO_PATH}" gitops-repo
cd gitops-repo
VALUES=platform/values-abrban.yaml
if command -v yq >/dev/null 2>&1; then
IMAGE_TAG="${IMAGE_TAG}" yq -i '.images.backend.tag = strenv(IMAGE_TAG) | .images.frontend.tag = strenv(IMAGE_TAG)' "${VALUES}"
else
# Only touch the tag line directly below each cloudhost-* repository line.
sed -i "/repository: .*cloudhost-backend/{n;s|tag: \".*\"|tag: \"${IMAGE_TAG}\"|;}" "${VALUES}"
sed -i "/repository: .*cloudhost-frontend/{n;s|tag: \".*\"|tag: \"${IMAGE_TAG}\"|;}" "${VALUES}"
fi
git config user.email "ci@abrban.com"
git config user.name "Gitea Actions"
git add "${VALUES}"
if ! git diff --cached --quiet; then
git commit -m "ci: deploy platform ${IMAGE_TAG}"
# Retry with rebase — another pipeline may have pushed meanwhile.
for attempt in 1 2 3; do
if git push origin HEAD:main; then
break
fi
echo "Push rejected (attempt ${attempt}) — rebasing on latest main"
git pull --rebase origin main
[ "$attempt" = "3" ] && { echo "Giving up after 3 attempts"; exit 1; }
done
fi