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: http://portainer.lab/ 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 PORTAINER_BASE_URL=$(printf '%s' "${PORTAINER_URL}" | sed -E 's/[[:space:]]+$//; s#/*$##') 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("^(?[A-Za-z_][A-Za-z0-9_]*)=(?.*)$")) | map({name: .name, value: .value}) ') echo "Loaded $(printf '%s' "${ENV_JSON}" | jq 'length') env entries from ENV_VARS" 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}}" fi fi STACKS_BODY=$(mktemp) STACKS_ERR=$(mktemp) STACKS_HTTP_CODE=$(curl -sS \ --noproxy "*" \ -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=$? if [ "${STACKS_CURL_EXIT}" -eq 6 ] && [ -n "${PORTAINER_IP:-}" ]; then STACKS_HTTP_CODE=$(curl -sS \ --noproxy "*" \ -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 fi if [ "${STACKS_CURL_EXIT}" -ne 0 ]; then cat "${STACKS_ERR}" || true exit "${STACKS_CURL_EXIT}" fi if [ "${STACKS_HTTP_CODE}" -lt 200 ] || [ "${STACKS_HTTP_CODE}" -ge 300 ]; then 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_ERR=$(mktemp) if [ -n "${STACK_ID}" ]; then PAYLOAD=$(jq -n \ --rawfile stack_file docker-compose.prod.yml \ --argjson env_vars "${ENV_JSON}" \ '{StackFileContent: $stack_file, Env: $env_vars, Prune: false, PullImage: true}') APPLY_HTTP_CODE=$(curl -sS -X PUT \ --noproxy "*" \ -o "${APPLY_BODY}" \ -w "%{http_code}" \ "${ACTIVE_PORTAINER_BASE_URL}/api/stacks/${STACK_ID}?endpointId=${PORTAINER_ENDPOINT_ID}" \ -H "X-API-Key: ${PORTAINER_API_KEY}" \ -H "Content-Type: application/json" \ -d "${PAYLOAD}" \ 2>"${APPLY_ERR}") APPLY_CURL_EXIT=$? else 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}') APPLY_HTTP_CODE=$(curl -sS -X POST \ --noproxy "*" \ -o "${APPLY_BODY}" \ -w "%{http_code}" \ "${ACTIVE_PORTAINER_BASE_URL}/api/stacks/create/standalone/string?endpointId=${PORTAINER_ENDPOINT_ID}" \ -H "X-API-Key: ${PORTAINER_API_KEY}" \ -H "Content-Type: application/json" \ -d "${PAYLOAD}" \ 2>"${APPLY_ERR}") APPLY_CURL_EXIT=$? fi if [ "${APPLY_CURL_EXIT}" -ne 0 ]; then cat "${APPLY_ERR}" || true exit "${APPLY_CURL_EXIT}" fi if [ "${APPLY_HTTP_CODE}" -lt 200 ] || [ "${APPLY_HTTP_CODE}" -ge 300 ]; then cat "${APPLY_BODY}" || true exit 1 fi echo "Portainer deploy completed successfully"