name: Server CI/CD on: push: tags: - "v*" - "latest" workflow_dispatch: {} jobs: docker-image: runs-on: ubuntu-22.04 if: startsWith(github.ref, 'refs/tags/') permissions: contents: read defaults: run: shell: bash steps: - name: Checkout repository uses: https://gitea.waternetwork.cn/actions/checkout@v4 with: fetch-depth: 1 - name: Normalize image metadata env: RAW_REGISTRY_HOST: ${{ vars.REGISTRY_HOST }} RAW_REPOSITORY: ${{ github.repository }} RAW_REF_NAME: ${{ github.ref_name }} run: | RAW_REGISTRY_HOST="$(printf '%s' "${RAW_REGISTRY_HOST}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" if [ -z "${RAW_REGISTRY_HOST}" ]; then echo "Missing required repository variable: REGISTRY_HOST" exit 1 fi REGISTRY_HOST="${RAW_REGISTRY_HOST#http://}" REGISTRY_HOST="${REGISTRY_HOST#https://}" REGISTRY_HOST="${REGISTRY_HOST%/}" if [ -z "${REGISTRY_HOST}" ]; then echo "Repository variable REGISTRY_HOST resolves to an empty host" exit 1 fi REPOSITORY_PATH="${RAW_REPOSITORY#/}" IMAGE_REPOSITORY_PATH="$(printf '%s' "$REPOSITORY_PATH" | tr '[:upper:]' '[:lower:]')" IMAGE_NAME="${REGISTRY_HOST}/${IMAGE_REPOSITORY_PATH}" IMAGE_TAG="${RAW_REF_NAME}" { echo "REGISTRY_HOST=${REGISTRY_HOST}" echo "REPOSITORY_PATH=${REPOSITORY_PATH}" echo "IMAGE_REPOSITORY_PATH=${IMAGE_REPOSITORY_PATH}" echo "IMAGE_NAME=${IMAGE_NAME}" echo "IMAGE_TAG=${IMAGE_TAG}" echo "IMAGE_REF=${IMAGE_NAME}:${IMAGE_TAG}" } >> "$GITHUB_ENV" - name: Login to Gitea Container Registry env: REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} run: | if [ -z "${REGISTRY_HOST:-}" ]; then echo "Missing resolved environment value: REGISTRY_HOST" exit 1 fi if [ -z "${REGISTRY_USERNAME}" ]; then echo "Missing required repository secret: REGISTRY_USERNAME" exit 1 fi if [ -z "${REGISTRY_PASSWORD}" ]; then echo "Missing required repository secret: REGISTRY_PASSWORD" exit 1 fi echo "Logging into registry host: ${REGISTRY_HOST}" echo "${REGISTRY_PASSWORD}" | docker login "$REGISTRY_HOST" \ --username "${REGISTRY_USERNAME}" \ --password-stdin - name: Materialize runtime env file env: TJWATER_SERVER_ENV: ${{ secrets.TJWATER_SERVER_ENV }} run: | if [ -z "${TJWATER_SERVER_ENV}" ]; then echo "Missing required repository secret: TJWATER_SERVER_ENV" echo "Store the backend .env file content as a multiline Gitea repository secret named TJWATER_SERVER_ENV." exit 1 fi printf '%s\n' "${TJWATER_SERVER_ENV}" > .env chmod 600 .env required_env_keys=( ENVIRONMENT NETWORK_NAME DB_NAME DB_HOST DB_PORT DB_USER DB_PASSWORD TIMESCALEDB_DB_NAME TIMESCALEDB_DB_HOST TIMESCALEDB_DB_PORT TIMESCALEDB_DB_USER TIMESCALEDB_DB_PASSWORD METADATA_DB_NAME METADATA_DB_HOST METADATA_DB_PORT METADATA_DB_USER METADATA_DB_PASSWORD DATABASE_ENCRYPTION_KEY ) missing_keys=() for key in "${required_env_keys[@]}"; do if ! grep -Eq "^[[:space:]]*(export[[:space:]]+)?${key}[[:space:]]*=" .env; then missing_keys+=("$key") fi done if [ "${#missing_keys[@]}" -gt 0 ]; then echo "TJWATER_SERVER_ENV is missing required keys: ${missing_keys[*]}" exit 1 fi - name: Validate workspace run: | if [ ! -f ./Dockerfile ]; then echo "Dockerfile not found in workspace. Repository checkout may have failed or produced an unexpected workspace." exit 1 fi - name: Build and Push Image run: | if [ -z "${IMAGE_NAME:-}" ] || [ -z "${IMAGE_TAG:-}" ]; then echo "Missing resolved image metadata: IMAGE_NAME or IMAGE_TAG" exit 1 fi push_with_retry() { image_ref="$1" attempt=1 max_attempts=3 while [ "$attempt" -le "$max_attempts" ]; do if docker push "$image_ref"; then return 0 fi if [ "$attempt" -eq "$max_attempts" ]; then return 1 fi echo "Push failed for $image_ref (attempt $attempt/$max_attempts); retrying in 10s..." attempt=$((attempt + 1)) sleep 10 done } if [ "${IMAGE_TAG}" = "latest" ]; then docker build \ -f ./Dockerfile \ -t "${IMAGE_NAME}:latest" \ . push_with_retry "${IMAGE_NAME}:latest" else docker build \ -f ./Dockerfile \ -t "${IMAGE_NAME}:${IMAGE_TAG}" \ -t "${IMAGE_NAME}:latest" \ . push_with_retry "${IMAGE_NAME}:${IMAGE_TAG}" push_with_retry "${IMAGE_NAME}:latest" fi - name: Notify Deploy Server run: | post_deploy_webhook() { label="$1" payload="$2" webhook_url="${{ vars.DEPLOY_WEBHOOK_URL }}" token="${{ secrets.DEPLOY_WEBHOOK_TOKEN }}" webhook_url=$(echo "$webhook_url" | xargs) if [ -z "$webhook_url" ]; then echo "Missing required repository variable: DEPLOY_WEBHOOK_URL" return 1 fi if [ -z "$token" ]; then echo "Missing required repository secret: DEPLOY_WEBHOOK_TOKEN" return 1 fi echo "[$label] Calling webhook: $webhook_url" http_code=$(curl -sS -D /tmp/deploy_headers.txt -o /tmp/deploy_response.txt -w "%{http_code}" -X POST "$webhook_url" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $token" \ -d "$payload") echo "[$label] webhook HTTP status: ${http_code}" if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then return 0 fi echo "[$label] response headers:" cat /tmp/deploy_headers.txt echo "[$label] response body:" cat /tmp/deploy_response.txt return 1 } PRIMARY_PAYLOAD="{\"image\":\"${IMAGE_REF}\",\"tag\":\"${IMAGE_TAG}\",\"repo\":\"${REPOSITORY_PATH}\"}" FALLBACK_PAYLOAD="{\"image\":\"${IMAGE_REF}\",\"tag\":\"${IMAGE_TAG}\",\"repo\":\"${IMAGE_REPOSITORY_PATH}\"}" echo "Deploy webhook target: ${{ vars.DEPLOY_WEBHOOK_URL }}" echo "Deploy payload(primary): image=${IMAGE_REF}, tag=${IMAGE_TAG}, repo=${REPOSITORY_PATH}" if post_deploy_webhook "primary" "$PRIMARY_PAYLOAD"; then exit 0 fi echo "Primary webhook request failed, retrying with lowercase repo path..." echo "Deploy payload(fallback): image=${IMAGE_REF}, tag=${IMAGE_TAG}, repo=${IMAGE_REPOSITORY_PATH}" if post_deploy_webhook "fallback" "$FALLBACK_PAYLOAD"; then exit 0 fi echo "Deploy webhook failed after primary and fallback attempts." exit 1 deploy-fallback-log: runs-on: ubuntu-22.04 needs: docker-image if: failure() steps: - name: Deployment not triggered run: echo "Image build/push failed, deployment webhook was not called."