241 lines
9.9 KiB
YAML
241 lines
9.9 KiB
YAML
name: Deploy (Clouderized)
|
|
|
|
on:
|
|
push:
|
|
branches: [main, master]
|
|
workflow_dispatch:
|
|
inputs:
|
|
no_cache:
|
|
description: 'Build without Docker layer cache'
|
|
required: false
|
|
default: 'false'
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: clouderized-runner
|
|
concurrency:
|
|
group: clouderized-build # global — only 1 build at a time, limited host resources
|
|
cancel-in-progress: false # queue, don't cancel — a new push from any customer must not kill another's build
|
|
|
|
steps:
|
|
- name: Cancel stale same-repo runs
|
|
run: |
|
|
curl -sf -X POST http://194.233.75.123:5200/apps/${{ gitea.repository_owner }}/${REPO#*/}/cancel-stale-runs \
|
|
-H "X-Internal-Token: ${{ secrets.INTERNAL_TOKEN }}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"run_id\": ${{ gitea.run_id }}}" \
|
|
-o /dev/null || true
|
|
env:
|
|
REPO: ${{ gitea.repository }}
|
|
|
|
- name: Checkout
|
|
run: |
|
|
git clone "https://x-access-token:${{ gitea.token }}@git.clouderized.com/${{ gitea.repository }}.git" .
|
|
|
|
- name: Check app config
|
|
run: |
|
|
CUSTOMER="${{ gitea.repository_owner }}"
|
|
REPO="${{ gitea.repository }}"
|
|
APP="${REPO#*/}"
|
|
YAML="/home/cldrzd/customers/$CUSTOMER/$APP/clouderized.yaml"
|
|
if [ ! -f "$YAML" ]; then
|
|
echo "ERROR: clouderized.yaml not found — app not provisioned yet"
|
|
exit 1
|
|
fi
|
|
echo "App provisioned — proceeding with build"
|
|
|
|
- name: Build image
|
|
run: |
|
|
CUSTOMER="${{ gitea.repository_owner }}"
|
|
REPO="${{ gitea.repository }}"
|
|
APP="${REPO#*/}"
|
|
CFG="/home/cldrzd/customers/$CUSTOMER/$APP/clouderized.yaml"
|
|
DOCKERFILE=$(python3 -c "import yaml; c=yaml.safe_load(open('$CFG')); print(c.get('dockerfile','Dockerfile'))" 2>/dev/null || echo "Dockerfile")
|
|
[ -f "$DOCKERFILE" ] || { echo "ERROR: Backend Dockerfile not found at $DOCKERFILE"; exit 1; }
|
|
NO_CACHE_FLAG=""
|
|
if [ "${{ inputs.no_cache }}" = "true" ]; then
|
|
NO_CACHE_FLAG="--no-cache"
|
|
fi
|
|
BUILD_CTX=$(dirname "$DOCKERFILE")
|
|
podman build --memory=4g $NO_CACHE_FLAG \
|
|
-f "$DOCKERFILE" \
|
|
-t $CUSTOMER-$APP:latest \
|
|
-t $CUSTOMER-$APP:${{ gitea.sha }} \
|
|
"$BUILD_CTX"
|
|
|
|
- name: Log image pull
|
|
run: |
|
|
CUSTOMER="${{ gitea.repository_owner }}"
|
|
REPO="${{ gitea.repository }}"
|
|
APP="${REPO#*/}"
|
|
CFG="/home/cldrzd/customers/$CUSTOMER/$APP/clouderized.yaml"
|
|
DOCKERFILE=$(python3 -c "import yaml; c=yaml.safe_load(open('$CFG')); print(c.get('dockerfile','Dockerfile'))" 2>/dev/null || echo "Dockerfile")
|
|
[ -f "$DOCKERFILE" ] || { echo "ERROR: Backend Dockerfile not found at $DOCKERFILE"; exit 1; }
|
|
BASE_IMAGE=$(grep '^FROM ' "$DOCKERFILE" | tail -1 | awk '{print $2}')
|
|
IMAGE_ID=$(podman inspect --format='{{.Id}}' $CUSTOMER-$APP:latest)
|
|
curl -s -X POST http://194.233.75.123:5200/apps/$CUSTOMER/$APP/image-pull \
|
|
-H 'Content-Type: application/json' \
|
|
-H "X-Internal-Token: ${{ secrets.INTERNAL_TOKEN }}" \
|
|
-d "{\"base_image\":\"$BASE_IMAGE\",\"image_id\":\"$IMAGE_ID\",\"git_sha\":\"${{ gitea.sha }}\"}" \
|
|
|| true
|
|
|
|
- name: Generate compose
|
|
run: |
|
|
CUSTOMER="${{ gitea.repository_owner }}"
|
|
REPO="${{ gitea.repository }}"
|
|
APP="${REPO#*/}"
|
|
python3 /home/cldrzd/clouderized/scripts/generate_compose.py \
|
|
/home/cldrzd/customers/$CUSTOMER/$APP/clouderized.yaml \
|
|
--output /home/cldrzd/customers/$CUSTOMER/$APP/docker-compose.yml
|
|
|
|
- name: Write env file
|
|
env:
|
|
APP_ENV: ${{ secrets.APP_ENV }}
|
|
run: |
|
|
CUSTOMER="${{ gitea.repository_owner }}"
|
|
REPO="${{ gitea.repository }}"
|
|
APP="${REPO#*/}"
|
|
ENV_FILE="/home/cldrzd/customers/$CUSTOMER/$APP/app.env"
|
|
> "$ENV_FILE"
|
|
if [ -n "$APP_ENV" ]; then
|
|
printf '%s\n' "$APP_ENV" >> "$ENV_FILE"
|
|
fi
|
|
echo "Wrote env file: $(wc -l < "$ENV_FILE") var(s)"
|
|
|
|
- name: Build frontend
|
|
run: |
|
|
CUSTOMER="${{ gitea.repository_owner }}"
|
|
REPO="${{ gitea.repository }}"
|
|
APP="${REPO#*/}"
|
|
CFG="/home/cldrzd/customers/$CUSTOMER/$APP/clouderized.yaml"
|
|
ENABLED=$(python3 -c "import yaml; c=yaml.safe_load(open('$CFG')); print(str(c.get('frontend',{}).get('enabled',False)).lower())" 2>/dev/null || echo "false")
|
|
if [ "$ENABLED" = "true" ]; then
|
|
BUILD_DIR=$(python3 -c "import yaml; c=yaml.safe_load(open('$CFG')); print(c.get('frontend',{}).get('build_dir','frontend'))")
|
|
DIST_DIR=$(python3 -c "import yaml; c=yaml.safe_load(open('$CFG')); print(c.get('frontend',{}).get('dist_dir','dist'))")
|
|
ABS_BUILD="$(pwd)/$BUILD_DIR"
|
|
[ -d "$ABS_BUILD" ] || { echo "ERROR: Frontend directory '$BUILD_DIR' not found in repo. Check 'Frontend directory' in portal settings."; exit 1; }
|
|
DEST="/home/cldrzd/customers/$CUSTOMER/$APP/frontend"
|
|
mkdir -p "$DEST"
|
|
podman run --rm \
|
|
-v "$ABS_BUILD:/app:z" \
|
|
-w /app \
|
|
node:lts-alpine \
|
|
sh -c "if [ -f package-lock.json ]; then npm ci; else npm install; fi && npm run build"
|
|
rsync -a --delete "$ABS_BUILD/$DIST_DIR/" "$DEST/"
|
|
echo "Frontend built and synced to $DEST"
|
|
else
|
|
echo "Frontend companion not enabled — skipping"
|
|
fi
|
|
|
|
- name: Seed data
|
|
run: |
|
|
CUSTOMER="${{ gitea.repository_owner }}"
|
|
REPO="${{ gitea.repository }}"
|
|
APP="${REPO#*/}"
|
|
if [ -d "data" ]; then
|
|
DEST="/home/cldrzd/customers/$CUSTOMER/$APP/data/$APP/app-data"
|
|
mkdir -p "$DEST"
|
|
if [ -z "$(ls -A "$DEST" 2>/dev/null)" ]; then
|
|
cp -r data/. "$DEST/"
|
|
echo "Seeded /app/data: $(find data -type f | wc -l) file(s)"
|
|
else
|
|
echo "Skipped: /app/data already has data"
|
|
fi
|
|
fi
|
|
|
|
- name: Ensure internal network
|
|
run: |
|
|
CUSTOMER="${{ gitea.repository_owner }}"
|
|
podman network inspect $CUSTOMER-internal >/dev/null 2>&1 \
|
|
|| podman network create --driver bridge --internal \
|
|
$CUSTOMER-internal
|
|
|
|
- name: Deploy
|
|
run: |
|
|
CUSTOMER="${{ gitea.repository_owner }}"
|
|
REPO="${{ gitea.repository }}"
|
|
APP="${REPO#*/}"
|
|
cd /home/cldrzd/customers/$CUSTOMER/$APP
|
|
podman-compose up -d --force-recreate
|
|
podman images --format '{{.Repository}}:{{.Tag}}' \
|
|
| grep "^localhost/$CUSTOMER-$APP:" | grep -v ':latest' \
|
|
| xargs -r podman rmi --force 2>/dev/null || true
|
|
|
|
- name: Health check
|
|
run: |
|
|
REPO="${{ gitea.repository }}"
|
|
APP="${REPO#*/}"
|
|
python3 /home/cldrzd/clouderized/scripts/healthcheck.py \
|
|
--customer ${{ gitea.repository_owner }} \
|
|
--app $APP
|
|
|
|
- name: Ping orchestrator
|
|
run: |
|
|
CUSTOMER="${{ gitea.repository_owner }}"
|
|
REPO="${{ gitea.repository }}"
|
|
APP="${REPO#*/}"
|
|
curl -sf -X POST http://194.233.75.123:5200/apps/$CUSTOMER/$APP/ping \
|
|
-H "X-Internal-Token: ${{ secrets.INTERNAL_TOKEN }}" || true
|
|
|
|
- name: Release deploy lock
|
|
if: always()
|
|
run: |
|
|
CUSTOMER="${{ gitea.repository_owner }}"
|
|
REPO="${{ gitea.repository }}"
|
|
APP="${REPO#*/}"
|
|
curl -sf -X POST http://194.233.75.123:5200/apps/$CUSTOMER/$APP/redeploy/release \
|
|
-H "X-Internal-Token: ${{ secrets.INTERNAL_TOKEN }}" || true
|
|
|
|
- name: Report deploy failure
|
|
if: failure()
|
|
run: |
|
|
CUSTOMER="${{ gitea.repository_owner }}"
|
|
REPO="${{ gitea.repository }}"
|
|
APP="${REPO#*/}"
|
|
|
|
# Collect container logs
|
|
APP_DIR="/home/cldrzd/customers/$CUSTOMER/$APP"
|
|
if [ -d "$APP_DIR" ] && [ -f "$APP_DIR/docker-compose.yml" ]; then
|
|
LOGS=$(cd "$APP_DIR" && podman-compose logs --tail=50 2>&1 || true)
|
|
[ -z "$LOGS" ] && LOGS="No container logs — container may not have started."
|
|
else
|
|
LOGS="Container not started — build or compose step may have failed."
|
|
fi
|
|
|
|
printf '%s' "Deploy failed — $APP (run #${{ gitea.run_number }})" > /tmp/_ci_title
|
|
cat > /tmp/_ci_body <<ENDBODY
|
|
## Deploy failed
|
|
|
|
A deploy workflow failed for \`$APP\`.
|
|
|
|
**Run:** [#${{ gitea.run_number }}](https://git.clouderized.com/$CUSTOMER/$APP/actions/runs/${{ gitea.run_id }}) | **Commit:** \`${{ gitea.sha }}\`
|
|
|
|
### Container logs (last 50 lines)
|
|
\`\`\`
|
|
$LOGS
|
|
\`\`\`
|
|
|
|
### What to do next
|
|
- Review the failing step in the workflow run linked above.
|
|
- Fix the issue in your code and push again.
|
|
- If your Dockerfile needs fixing, see: https://docs.clouderized.com/sample-dockerfiles
|
|
ENDBODY
|
|
|
|
python3 - "${{ gitea.token }}" "$CUSTOMER/$APP" <<'PYEOF'
|
|
import sys, json, urllib.request
|
|
token, repo = sys.argv[1], sys.argv[2]
|
|
title = open('/tmp/_ci_title').read()
|
|
body = open('/tmp/_ci_body').read()
|
|
url = f"https://git.clouderized.com/api/v1/repos/{repo}/issues"
|
|
data = json.dumps({"title": title, "body": body}).encode()
|
|
req = urllib.request.Request(url, data=data, method="POST")
|
|
req.add_header("Authorization", f"token {token}")
|
|
req.add_header("Content-Type", "application/json")
|
|
try:
|
|
resp = urllib.request.urlopen(req)
|
|
print("Issue created:", json.loads(resp.read())["html_url"])
|
|
except Exception as e:
|
|
print(f"Warning: could not create issue: {e}")
|
|
PYEOF
|
|
|