diff --git a/.woodpecker.yml b/.woodpecker.yml index 3289608..2b0b7c8 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -8,14 +8,40 @@ steps: commands: - apk add --no-cache docker-cli docker-cli-compose - | - YAML_FILES=$(echo "${CI_COMMIT_CHANGED_FILES}" | tr ',' '\n' | grep '\.yaml$' || true) - [ -z "$YAML_FILES" ] && echo "No yaml files changed" && exit 0 - for f in $YAML_FILES; do + # Collect changed stacks — both flat files and folder-based + CHANGED_FILES=$(echo "${CI_COMMIT_CHANGED_FILES}" | tr ',' '\n') + + # Flat: any root-level *.yaml + FLAT=$(echo "$CHANGED_FILES" | grep -E '^[^/]+\.ya?ml$' || true) + + # Folder: any file under a subfolder (e.g. immich/immich.yml) + FOLDERS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+/' | cut -d/ -f1 | sort -u || true) + + [ -z "$FLAT" ] && [ -z "$FOLDERS" ] && echo "No stacks changed" && exit 0 + + for f in $FLAT; do [ -f "$f" ] || continue docker compose -f "$f" --no-interpolate config -q \ && echo " OK $f" || { echo " FAIL $f"; exit 1; } done + for d in $FOLDERS; do + [ -d "$d" ] || continue + # Build -f flags for all compose files in folder, main first + MAIN="" + EXTRAS="" + for ext in yml yaml; do + [ -f "$d/$d.$ext" ] && MAIN="$d/$d.$ext" && break + done + [ -z "$MAIN" ] && echo " SKIP $d (no main compose file)" && continue + for cf in $(find "$d" -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) | sort); do + [ "$cf" = "$MAIN" ] && continue + EXTRAS="$EXTRAS -f $cf" + done + docker compose -f "$MAIN" $EXTRAS --no-interpolate config -q \ + && echo " OK $d/" || { echo " FAIL $d/"; exit 1; } + done + provision-secrets: image: alpine:latest environment: @@ -104,19 +130,25 @@ steps: - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H ${SWARM_MANAGER_IP} >> ~/.ssh/known_hosts 2>/dev/null || true - | - YAML_FILES=$(echo "${CI_COMMIT_CHANGED_FILES}" | tr ',' '\n' | grep '\.yaml$' || true) - [ -z "$YAML_FILES" ] && echo "No stack yaml files changed, skipping" && exit 0 + CHANGED_FILES=$(echo "${CI_COMMIT_CHANGED_FILES}" | tr ',' '\n') + FLAT_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+\.yaml$' | sed 's/\.yaml$//' || true) + FOLDER_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+/' | cut -d/ -f1 | sort -u || true) + ALL_STACKS=$(printf '%s\n%s' "$FLAT_STACKS" "$FOLDER_STACKS" | grep -v '^$' | sort -u) + [ -z "$ALL_STACKS" ] && echo "No stacks changed, skipping" && exit 0 - scp -o StrictHostKeyChecking=no deploy/create-secrets.sh root@${SWARM_MANAGER_IP}:/tmp/cs.sh - | - YAML_FILES=$(echo "${CI_COMMIT_CHANGED_FILES}" | tr ',' '\n' | grep '\.yaml$' || true) - for f in $YAML_FILES; do - STACK=$(basename "$f" .yaml) + CHANGED_FILES=$(echo "${CI_COMMIT_CHANGED_FILES}" | tr ',' '\n') + FLAT_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+\.yaml$' | sed 's/\.yaml$//' || true) + FOLDER_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+/' | cut -d/ -f1 | sort -u || true) + ALL_STACKS=$(printf '%s\n%s' "$FLAT_STACKS" "$FOLDER_STACKS" | grep -v '^$' | sort -u) + + for STACK in $ALL_STACKS; do echo "Provisioning: $STACK" case "$STACK" in - maintenance|media|unifi|guacamole|security|auth|traefik|immich) + maintenance|media|unifi|guacamole|security|auth|traefik|immich|meshcentral|ddm) echo " No Docker secrets for $STACK — secrets in host .env";; woodpecker) - echo " Manual only - skipping";; + echo " Manual only — skipping";; 3dprint) ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} "source /tmp/cs.sh create_or_update_secret '3dprint_db_password' '${3DPRINT_DB_PASSWORD}'";; @@ -125,24 +157,21 @@ steps: create_or_update_secret 'gamma_auth_token' '${GAMMA_AUTH_TOKEN}'";; git) ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} "source /tmp/cs.sh - create_or_update_secret 'git_db_password' '${GIT_DB_PASSWORD}' - create_or_update_secret 'git_runner_token' '${GIT_RUNNER_TOKEN}' - create_or_update_secret 'git_mcp_access_token' '${GIT_MCP_ACCESS_TOKEN}'";; + create_or_update_secret 'git_db_password' '${GIT_DB_PASSWORD}' + create_or_update_secret 'git_runner_token' '${GIT_RUNNER_TOKEN}' + create_or_update_secret 'git_mcp_access_token' '${GIT_MCP_ACCESS_TOKEN}'";; homeassistant) ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} "source /tmp/cs.sh create_or_update_secret 'homeassistant_frigate_rtsp_password' '${HOMEASSISTANT_FRIGATE_RTSP_PASSWORD}' create_or_update_secret 'homeassistant_immich_api_key' '${HOMEASSISTANT_IMMICH_API_KEY}'";; mealie) ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} "source /tmp/cs.sh - create_or_update_secret 'mealie_db_password' '${MEALIE_DB_PASSWORD}' - create_or_update_secret 'mealie_ldap_query_password' '${MEALIE_LDAP_QUERY_PASSWORD}'";; - meshcentral) - ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} "source /tmp/cs.sh - create_or_update_secret 'meshcentral_backup_password' '${MESHCENTRAL_BACKUP_PASSWORD}'";; + create_or_update_secret 'mealie_db_password' '${MEALIE_DB_PASSWORD}' + create_or_update_secret 'mealie_ldap_query_password' '${MEALIE_LDAP_QUERY_PASSWORD}'";; n8n) ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} "source /tmp/cs.sh - create_or_update_secret 'n8n_db_password' '${N8N_DB_PASSWORD}' - create_or_update_secret 'n8n_encryption_key' '${N8N_ENCRYPTION_KEY}'";; + create_or_update_secret 'n8n_db_password' '${N8N_DB_PASSWORD}' + create_or_update_secret 'n8n_encryption_key' '${N8N_ENCRYPTION_KEY}'";; postgresql) ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} "source /tmp/cs.sh create_or_update_secret 'postgresql_password' '${POSTGRESQL_PASSWORD}' @@ -150,11 +179,33 @@ steps: productivity) ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} "source /tmp/cs.sh create_or_update_secret 'productivity_paperless_secret_key' '${PRODUCTIVITY_PAPERLESS_SECRET_KEY}' - create_or_update_secret 'productivity_db_password' '${PRODUCTIVITY_DB_PASSWORD}' - create_or_update_secret 'productivity_oidc_providers' '${PRODUCTIVITY_OIDC_PROVIDERS}'";; + create_or_update_secret 'productivity_db_password' '${PRODUCTIVITY_DB_PASSWORD}' + create_or_update_secret 'productivity_oidc_providers' '${PRODUCTIVITY_OIDC_PROVIDERS}'";; vaultwarden) - printf '%s' "${VAULTWARDEN_ADMIN_TOKEN}" | ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} "docker secret rm vaultwarden_admin_token 2>/dev/null; docker secret create vaultwarden_admin_token -" + printf '%s' "${VAULTWARDEN_ADMIN_TOKEN}" | ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} "docker secret rm vaultwarden_admin_token 2>/dev/null; docker secret create vaultwarden_admin_token -" printf '%s' "${VAULTWARDEN_DATABASE_URL}" | ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} "docker secret rm vaultwarden_database_url 2>/dev/null; docker secret create vaultwarden_database_url -";; + ai) + ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} "source /tmp/cs.sh + create_or_update_secret 'ai_aws_access_key_id' '${AI_AWS_ACCESS_KEY_ID}' + create_or_update_secret 'ai_aws_secret_access_key' '${AI_AWS_SECRET_ACCESS_KEY}' + create_or_update_secret 'ai_litellm_master_key' '${AI_LITELLM_MASTER_KEY}' + create_or_update_secret 'ai_litellm_salt_key' '${AI_LITELLM_SALT_KEY}' + create_or_update_secret 'ai_litellm_db_password' '${AI_LITELLM_DB_PASSWORD}' + create_or_update_secret 'ai_oauth_client_id' '${AI_OAUTH_CLIENT_ID}' + create_or_update_secret 'ai_oauth_client_secret' '${AI_OAUTH_CLIENT_SECRET}' + create_or_update_secret 'ai_webui_secret_key' '${AI_WEBUI_SECRET_KEY}' + create_or_update_secret 'ai_open_webui_db_url' '${AI_OPEN_WEBUI_DB_URL}' + create_or_update_secret 'ai_mcpo_api_key' '${AI_MCPO_API_KEY}'";; + entertainment) + ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} "source /tmp/cs.sh + create_or_update_secret 'entertainment_discord_token' '${ENTERTAINMENT_DISCORD_TOKEN}' + create_or_update_secret 'entertainment_discord_client_secret' '${ENTERTAINMENT_DISCORD_CLIENT_SECRET}' + create_or_update_secret 'entertainment_secret_key_base' '${ENTERTAINMENT_SECRET_KEY_BASE}' + create_or_update_secret 'entertainment_basic_auth_password' '${ENTERTAINMENT_BASIC_AUTH_PASSWORD}' + create_or_update_secret 'entertainment_sparky_db_password' '${ENTERTAINMENT_SPARKY_DB_PASSWORD}' + create_or_update_secret 'entertainment_sparky_app_db_password' '${ENTERTAINMENT_SPARKY_APP_DB_PASSWORD}' + create_or_update_secret 'entertainment_sparky_encryption_key' '${ENTERTAINMENT_SPARKY_ENCRYPTION_KEY}' + create_or_update_secret 'entertainment_better_auth_secret' '${ENTERTAINMENT_BETTER_AUTH_SECRET}'";; *) echo " No secrets case for $STACK";; esac @@ -174,14 +225,26 @@ steps: - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H ${SWARM_MANAGER_IP} >> ~/.ssh/known_hosts 2>/dev/null || true - | - YAML_FILES=$(echo "${CI_COMMIT_CHANGED_FILES}" | tr ',' '\n' | grep '\.yaml$' || true) - [ -z "$YAML_FILES" ] && echo "No yaml files changed" && exit 0 - rsync -av -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" deploy/ root@${SWARM_MANAGER_IP}:/volume1/docker/compose-files/deploy/ - for f in $YAML_FILES; do - [ -f "$f" ] || continue - STACK=$(basename "$f" .yaml) - rsync -av -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \ - "$f" root@${SWARM_MANAGER_IP}:/volume1/docker/compose-files/ + CHANGED_FILES=$(echo "${CI_COMMIT_CHANGED_FILES}" | tr ',' '\n') + FLAT_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+\.yaml$' | sed 's/\.yaml$//' || true) + FOLDER_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+/' | cut -d/ -f1 | sort -u || true) + ALL_STACKS=$(printf '%s\n%s' "$FLAT_STACKS" "$FOLDER_STACKS" | grep -v '^$' | sort -u) + [ -z "$ALL_STACKS" ] && echo "No stacks changed" && exit 0 + + # Always sync deploy/ scripts first + rsync -av -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \ + deploy/ root@${SWARM_MANAGER_IP}:/volume1/docker/compose-files/deploy/ + + for STACK in $ALL_STACKS; do + echo "--- Deploying: $STACK ---" + # Sync flat file or entire folder + if [ -d "$STACK" ]; then + rsync -av -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \ + "$STACK/" root@${SWARM_MANAGER_IP}:/volume1/docker/compose-files/$STACK/ + elif [ -f "${STACK}.yaml" ]; then + rsync -av -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \ + "${STACK}.yaml" root@${SWARM_MANAGER_IP}:/volume1/docker/compose-files/ + fi ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} \ "bash /volume1/docker/compose-files/deploy/stack-deploy.sh $STACK" \ && echo " OK $STACK" || { echo " FAIL $STACK"; exit 1; } @@ -201,11 +264,13 @@ steps: - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H ${SWARM_MANAGER_IP} >> ~/.ssh/known_hosts 2>/dev/null || true - | - YAML_FILES=$(echo "${CI_COMMIT_CHANGED_FILES}" | tr ',' '\n' | grep '\.yaml$' || true) - [ -z "$YAML_FILES" ] && exit 0 + CHANGED_FILES=$(echo "${CI_COMMIT_CHANGED_FILES}" | tr ',' '\n') + FLAT_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+\.yaml$' | sed 's/\.yaml$//' || true) + FOLDER_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+/' | cut -d/ -f1 | sort -u || true) + ALL_STACKS=$(printf '%s\n%s' "$FLAT_STACKS" "$FOLDER_STACKS" | grep -v '^$' | sort -u) + [ -z "$ALL_STACKS" ] && exit 0 sleep 5 - for f in $YAML_FILES; do - STACK=$(basename "$f" .yaml) + for STACK in $ALL_STACKS; do echo "--- $STACK ---" ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} \ "docker stack ps $STACK --filter desired-state=running \ @@ -236,7 +301,7 @@ steps: commands: - apk add --no-cache curl - | - PAYLOAD=$(printf '{"title":"Swarm Deploy FAILED","text":"Commit: %s See: https://${WOODPECKER_HOST}","themeColor":"ff0000"}' "$CI_COMMIT_MESSAGE") + PAYLOAD=$(printf '{"title":"Swarm Deploy FAILED","text":"Commit: %s See: https://%s","themeColor":"ff0000"}' "$CI_COMMIT_MESSAGE" "$WOODPECKER_HOST") curl -sf -X POST -H 'Content-Type: application/json' -d "$PAYLOAD" "$TEAMS_WEBHOOK" || true when: - status: failure