Files
condado-newsletter/.gitea/workflows/deploy.yml
Gabriel Sancho 9b554644bc
All checks were successful
Build And Publish Production Image / Build And Publish Production Image (push) Successful in 7s
fix: change PullImage option to false in deploy workflow
2026-03-28 16:15:13 -03:00

331 lines
13 KiB
YAML

name: Deploy Production Stack
on:
workflow_run:
workflows: ["Build And Publish Production Image"]
types: [completed]
workflow_dispatch:
jobs:
deploy:
name: Deploy Stack Via Portainer
if: ${{ gitea.event_name == 'workflow_dispatch' || gitea.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
env:
STACK_NAME: condado-newsletter-stack
PORTAINER_URL: ${{ secrets.PORTAINER_URL }}
PORTAINER_API_KEY: ${{ secrets.PORTAINER_API_KEY }}
PORTAINER_ENDPOINT_ID: ${{ secrets.PORTAINER_ENDPOINT_ID }}
ENV_VARS: ${{ secrets.ENV_VARS }}
steps:
- uses: actions/checkout@v4
with:
github-server-url: http://gitea.lab
- name: Validate ENV_VARS secret
run: |
set -eu
if [ -z "${ENV_VARS}" ]; then
echo "ENV_VARS secret is empty."
exit 1
fi
- name: Deploy stack via Portainer API
run: |
set -u
set +e
if ! command -v curl >/dev/null 2>&1; then
echo "curl is not available in this runner image"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "jq is not available in this runner image"
exit 1
fi
PORTAINER_BASE_URL=$(printf '%s' "${PORTAINER_URL:-http://portainer.lab/}" | sed -E 's/[[:space:]]+$//; s#/*$##')
echo "Portainer deploy debug"
echo "PORTAINER_URL=${PORTAINER_URL:-http://portainer.lab/}"
echo "PORTAINER_BASE_URL=${PORTAINER_BASE_URL}"
echo "STACK_NAME=${STACK_NAME}"
echo "PORTAINER_ENDPOINT_ID=${PORTAINER_ENDPOINT_ID}"
echo "HTTP_PROXY=${HTTP_PROXY:-<empty>}"
echo "HTTPS_PROXY=${HTTPS_PROXY:-<empty>}"
echo "NO_PROXY=${NO_PROXY:-<empty>}"
echo "Current runner network info:"
if command -v ip >/dev/null 2>&1; then
ip -4 addr show || true
ip route || true
else
hostname -I || true
fi
ENV_JSON=$(printf '%s\n' "${ENV_VARS}" | jq -R -s '
split("\n")
| map(gsub("\r$"; ""))
| map(select(length > 0))
| map(select(startswith("#") | not))
| map(select(test("^[A-Za-z_][A-Za-z0-9_]*=.*$")))
| map(capture("^(?<name>[A-Za-z_][A-Za-z0-9_]*)=(?<value>.*)$"))
| map({name: .name, value: .value})
')
echo "Loaded $(printf '%s' "${ENV_JSON}" | jq 'length') env entries from ENV_VARS"
echo "ENV names preview:"
printf '%s' "${ENV_JSON}" | jq -r '.[0:10][]?.name' || true
REQUIRED_ENV_KEYS=(
APP_PASSWORD
JWT_SECRET
SPRING_DATASOURCE_USERNAME
SPRING_DATASOURCE_PASSWORD
APP_RECIPIENTS
)
MISSING_KEYS=()
for REQUIRED_KEY in "${REQUIRED_ENV_KEYS[@]}"; do
if ! printf '%s' "${ENV_JSON}" | jq -e --arg required_key "${REQUIRED_KEY}" 'map(.name) | index($required_key) != null' >/dev/null; then
MISSING_KEYS+=("${REQUIRED_KEY}")
fi
done
if [ "${#MISSING_KEYS[@]}" -gt 0 ]; then
echo "ENV_VARS is missing required keys: ${MISSING_KEYS[*]}"
exit 1
fi
echo "Portainer base URL: ${PORTAINER_BASE_URL}"
echo "Target stack: ${STACK_NAME}"
echo "Endpoint id set: $([ -n "${PORTAINER_ENDPOINT_ID}" ] && echo yes || echo no)"
PORTAINER_HOST=$(printf '%s' "${PORTAINER_BASE_URL}" | sed -E 's#^[a-zA-Z]+://##; s#/.*$##; s/:.*$//')
PORTAINER_IP=""
ACTIVE_PORTAINER_BASE_URL="${PORTAINER_BASE_URL}"
if command -v getent >/dev/null 2>&1; then
PORTAINER_IP=$(getent hosts "${PORTAINER_HOST}" | awk 'NR==1{print $1}')
if [ -n "${PORTAINER_IP}" ]; then
PORTAINER_IP_BASE_URL="${PORTAINER_BASE_URL/${PORTAINER_HOST}/${PORTAINER_IP}}"
echo "Portainer DNS resolved ${PORTAINER_HOST} -> ${PORTAINER_IP}"
echo "IP fallback URL: ${PORTAINER_IP_BASE_URL}"
else
echo "DNS lookup returned no IP for ${PORTAINER_HOST}"
fi
else
echo "getent not available; skipping DNS pre-check"
fi
STACKS_BODY=$(mktemp)
STACKS_HEADERS=$(mktemp)
STACKS_ERR=$(mktemp)
STACKS_HTTP_CODE=$(curl -sS \
--noproxy "*" \
-D "${STACKS_HEADERS}" \
-o "${STACKS_BODY}" \
-w "%{http_code}" \
"${ACTIVE_PORTAINER_BASE_URL}/api/stacks" \
-H "X-API-Key: ${PORTAINER_API_KEY}" \
2>"${STACKS_ERR}")
STACKS_CURL_EXIT=$?
echo "GET /api/stacks curl exit: ${STACKS_CURL_EXIT}"
echo "GET /api/stacks http code: ${STACKS_HTTP_CODE}"
echo "GET /api/stacks headers:"
cat "${STACKS_HEADERS}" || true
if [ "${STACKS_CURL_EXIT}" -eq 6 ] && [ -n "${PORTAINER_IP:-}" ]; then
echo "Retrying stack list with IP fallback due to DNS failure"
STACKS_HTTP_CODE=$(curl -sS \
--noproxy "*" \
-D "${STACKS_HEADERS}" \
-o "${STACKS_BODY}" \
-w "%{http_code}" \
"${PORTAINER_IP_BASE_URL}/api/stacks" \
-H "X-API-Key: ${PORTAINER_API_KEY}" \
2>"${STACKS_ERR}")
STACKS_CURL_EXIT=$?
if [ "${STACKS_CURL_EXIT}" -eq 0 ]; then
ACTIVE_PORTAINER_BASE_URL="${PORTAINER_IP_BASE_URL}"
fi
echo "Retry GET /api/stacks curl exit: ${STACKS_CURL_EXIT}"
echo "Retry GET /api/stacks http code: ${STACKS_HTTP_CODE}"
fi
if [ "${STACKS_CURL_EXIT}" -ne 0 ]; then
echo "GET /api/stacks stderr:"
cat "${STACKS_ERR}" || true
exit "${STACKS_CURL_EXIT}"
fi
if [ "${STACKS_HTTP_CODE}" -lt 200 ] || [ "${STACKS_HTTP_CODE}" -ge 300 ]; then
echo "GET /api/stacks body:"
cat "${STACKS_BODY}" || true
exit 1
fi
STACK_ID=$(jq -r --arg stack_name "${STACK_NAME}" '.[] | select(.Name == $stack_name) | .Id' "${STACKS_BODY}" | head -n 1)
APPLY_BODY=$(mktemp)
APPLY_HEADERS=$(mktemp)
APPLY_ERR=$(mktemp)
# If the stack does not exist yet, remove orphan containers with names defined in compose.
# This enables an idempotent create-or-recreate flow when old standalone containers exist.
if [ -z "${STACK_ID}" ]; then
echo "Stack not found in Portainer; checking for orphan containers with conflicting names"
mapfile -t CONTAINER_NAMES < <(awk '/container_name:/{print $2}' docker-compose.prod.yml | tr -d '"' | sed '/^$/d')
for CONTAINER_NAME in "${CONTAINER_NAMES[@]}"; do
FILTERS=$(jq -cn --arg n "^/${CONTAINER_NAME}$" '{name: [$n]}')
FILTERS_URLENC=$(printf '%s' "${FILTERS}" | jq -sRr @uri)
LIST_URL="${ACTIVE_PORTAINER_BASE_URL}/api/endpoints/${PORTAINER_ENDPOINT_ID}/docker/containers/json?all=1&filters=${FILTERS_URLENC}"
LIST_BODY=$(mktemp)
LIST_ERR=$(mktemp)
LIST_HTTP_CODE=$(curl -sS \
--noproxy "*" \
-o "${LIST_BODY}" \
-w "%{http_code}" \
"${LIST_URL}" \
-H "X-API-Key: ${PORTAINER_API_KEY}" \
2>"${LIST_ERR}")
LIST_CURL_EXIT=$?
echo "Container pre-check [${CONTAINER_NAME}] curl=${LIST_CURL_EXIT} http=${LIST_HTTP_CODE}"
if [ "${LIST_CURL_EXIT}" -ne 0 ]; then
echo "Container pre-check stderr for ${CONTAINER_NAME}:"
cat "${LIST_ERR}" || true
continue
fi
if [ "${LIST_HTTP_CODE}" -lt 200 ] || [ "${LIST_HTTP_CODE}" -ge 300 ]; then
echo "Container pre-check non-success response for ${CONTAINER_NAME}:"
cat "${LIST_BODY}" || true
continue
fi
mapfile -t MATCHING_IDS < <(jq -r '.[].Id' "${LIST_BODY}")
if [ "${#MATCHING_IDS[@]}" -eq 0 ]; then
echo "No conflicting container found for ${CONTAINER_NAME}"
continue
fi
for CONTAINER_ID in "${MATCHING_IDS[@]}"; do
DELETE_URL="${ACTIVE_PORTAINER_BASE_URL}/api/endpoints/${PORTAINER_ENDPOINT_ID}/docker/containers/${CONTAINER_ID}?force=1"
DELETE_BODY=$(mktemp)
DELETE_ERR=$(mktemp)
DELETE_HTTP_CODE=$(curl -sS -X DELETE \
--noproxy "*" \
-o "${DELETE_BODY}" \
-w "%{http_code}" \
"${DELETE_URL}" \
-H "X-API-Key: ${PORTAINER_API_KEY}" \
2>"${DELETE_ERR}")
DELETE_CURL_EXIT=$?
echo "Removed conflicting container ${CONTAINER_NAME} (${CONTAINER_ID}) curl=${DELETE_CURL_EXIT} http=${DELETE_HTTP_CODE}"
if [ "${DELETE_CURL_EXIT}" -ne 0 ]; then
echo "Delete stderr:"
cat "${DELETE_ERR}" || true
fi
if [ "${DELETE_HTTP_CODE}" -lt 200 ] || [ "${DELETE_HTTP_CODE}" -ge 300 ]; then
echo "Delete response body:"
cat "${DELETE_BODY}" || true
fi
done
done
fi
if [ -n "${STACK_ID}" ]; then
echo "Updating existing stack id=${STACK_ID}"
REQUEST_URL="${ACTIVE_PORTAINER_BASE_URL}/api/stacks/${STACK_ID}?endpointId=${PORTAINER_ENDPOINT_ID}"
PAYLOAD=$(jq -n \
--rawfile stack_file docker-compose.prod.yml \
--argjson env_vars "${ENV_JSON}" \
'{StackFileContent: $stack_file, Env: $env_vars, Prune: false, PullImage: false}')
echo "Apply request URL: ${REQUEST_URL}"
echo "Apply payload summary:"
printf '%s' "${PAYLOAD}" | jq -r '{stackFileLength: (.StackFileContent | length), envCount: (.Env | length), prune: .Prune, pullImage: .PullImage}' || true
APPLY_HTTP_CODE=$(curl -sS -X PUT \
--noproxy "*" \
-D "${APPLY_HEADERS}" \
-o "${APPLY_BODY}" \
-w "%{http_code}" \
"${REQUEST_URL}" \
-H "X-API-Key: ${PORTAINER_API_KEY}" \
-H "Content-Type: application/json" \
-d "${PAYLOAD}" \
2>"${APPLY_ERR}")
APPLY_CURL_EXIT=$?
else
echo "Creating new stack ${STACK_NAME}"
REQUEST_URL="${ACTIVE_PORTAINER_BASE_URL}/api/stacks/create/standalone/string?endpointId=${PORTAINER_ENDPOINT_ID}"
PAYLOAD=$(jq -n \
--arg name "${STACK_NAME}" \
--rawfile stack_file docker-compose.prod.yml \
--argjson env_vars "${ENV_JSON}" \
'{Name: $name, StackFileContent: $stack_file, Env: $env_vars, FromAppTemplate: false}')
echo "Apply request URL: ${REQUEST_URL}"
echo "Apply payload summary:"
printf '%s' "${PAYLOAD}" | jq -r '{name: .Name, stackFileLength: (.StackFileContent | length), envCount: (.Env | length), fromAppTemplate: .FromAppTemplate}' || true
APPLY_HTTP_CODE=$(curl -sS -X POST \
--noproxy "*" \
-D "${APPLY_HEADERS}" \
-o "${APPLY_BODY}" \
-w "%{http_code}" \
"${REQUEST_URL}" \
-H "X-API-Key: ${PORTAINER_API_KEY}" \
-H "Content-Type: application/json" \
-d "${PAYLOAD}" \
2>"${APPLY_ERR}")
APPLY_CURL_EXIT=$?
fi
echo "Apply curl exit: ${APPLY_CURL_EXIT}"
echo "Apply http code: ${APPLY_HTTP_CODE}"
echo "Apply response headers:"
cat "${APPLY_HEADERS}" || true
if [ "${APPLY_CURL_EXIT}" -ne 0 ]; then
echo "Apply stderr:"
cat "${APPLY_ERR}" || true
exit "${APPLY_CURL_EXIT}"
fi
if [ "${APPLY_HTTP_CODE}" -lt 200 ] || [ "${APPLY_HTTP_CODE}" -ge 300 ]; then
echo "Apply response body:"
cat "${APPLY_BODY}" || true
echo "Apply response parsed as JSON (if possible):"
jq -r '.' "${APPLY_BODY}" 2>/dev/null || echo "<non-json or empty body>"
if [ ! -s "${APPLY_BODY}" ]; then
echo "Apply body is empty; retrying once with verbose curl for diagnostics"
curl -v -X "$( [ -n "${STACK_ID}" ] && echo PUT || echo POST )" \
--noproxy "*" \
-o /tmp/portainer-debug-body.txt \
"${REQUEST_URL}" \
-H "X-API-Key: ${PORTAINER_API_KEY}" \
-H "Content-Type: application/json" \
-d "${PAYLOAD}" \
2>/tmp/portainer-debug-stderr.txt || true
echo "Verbose retry stderr:"
cat /tmp/portainer-debug-stderr.txt || true
echo "Verbose retry body:"
cat /tmp/portainer-debug-body.txt || true
fi
exit 1
fi
echo "Portainer deploy completed successfully"