trips) portainer API deploy + preflight checks (#5)
All checks were successful
Build and Push Image / docker-build-and-push (push) Successful in 2m25s

- Switch CI deploy step from Portainer webhook to Portainer API (CE-compatible)
- Add manual workflow trigger (`workflow_dispatch`)
- Add preflight checks for Portainer auth, endpoint ID, and stack ID before redeploy
- Update README with required secrets and deploy flow

Reviewed-on: #5
Co-authored-by: Shaun Campbell <shaun@campbellwireless.net>
Co-committed-by: Shaun Campbell <shaun@campbellwireless.net>
This commit was merged in pull request #5.
This commit is contained in:
2026-02-21 06:20:51 +00:00
committed by shaun
parent 5101129f23
commit 4b3af5e947
2 changed files with 160 additions and 3 deletions

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- main
workflow_dispatch:
env:
REGISTRY_HOST: registry.campbellwireless.net
@@ -50,3 +51,107 @@ jobs:
tags: |
${{ env.REGISTRY_HOST }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY_HOST }}/${{ env.IMAGE_NAME }}:${{ steps.tags.outputs.sha_short }}
- name: Preflight Portainer deploy config
env:
PORTAINER_URL: ${{ secrets.PORTAINER_URL }}
PORTAINER_API_KEY: ${{ secrets.PORTAINER_API_KEY }}
PORTAINER_STACK_ID: ${{ secrets.PORTAINER_STACK_ID }}
PORTAINER_ENDPOINT_ID: ${{ secrets.PORTAINER_ENDPOINT_ID }}
PORTAINER_INSECURE_TLS: ${{ secrets.PORTAINER_INSECURE_TLS }}
run: |
set -eu
for required in PORTAINER_URL PORTAINER_API_KEY PORTAINER_STACK_ID PORTAINER_ENDPOINT_ID; do
if [ -z "$(eval "printf '%s' \"\${$required:-}\"")" ]; then
echo "Missing required secret: $required" >&2
exit 1
fi
done
CURL_ARGS=(--fail --show-error --silent --retry 3 --retry-all-errors)
if [ "${PORTAINER_INSECURE_TLS:-false}" = "true" ]; then
CURL_ARGS+=(--insecure)
fi
API_BASE="${PORTAINER_URL%/}/api"
ENDPOINT_JSON="$(
curl "${CURL_ARGS[@]}" \
--header "X-API-Key: ${PORTAINER_API_KEY}" \
"${API_BASE}/endpoints/${PORTAINER_ENDPOINT_ID}"
)"
echo "${ENDPOINT_JSON}" | jq -e --arg id "${PORTAINER_ENDPOINT_ID}" \
'((.Id // .id) | tostring) == $id' >/dev/null
STACK_JSON="$(
curl "${CURL_ARGS[@]}" \
--header "X-API-Key: ${PORTAINER_API_KEY}" \
"${API_BASE}/stacks/${PORTAINER_STACK_ID}?endpointId=${PORTAINER_ENDPOINT_ID}"
)"
echo "${STACK_JSON}" | jq -e --arg id "${PORTAINER_STACK_ID}" \
'((.Id // .id) | tostring) == $id' >/dev/null
echo "Portainer preflight checks passed."
- name: Trigger Portainer stack redeploy
env:
PORTAINER_URL: ${{ secrets.PORTAINER_URL }}
PORTAINER_API_KEY: ${{ secrets.PORTAINER_API_KEY }}
PORTAINER_STACK_ID: ${{ secrets.PORTAINER_STACK_ID }}
PORTAINER_ENDPOINT_ID: ${{ secrets.PORTAINER_ENDPOINT_ID }}
PORTAINER_INSECURE_TLS: ${{ secrets.PORTAINER_INSECURE_TLS }}
run: |
set -eu
for required in PORTAINER_URL PORTAINER_API_KEY PORTAINER_STACK_ID PORTAINER_ENDPOINT_ID; do
if [ -z "$(eval "printf '%s' \"\${$required:-}\"")" ]; then
echo "Missing required secret: $required" >&2
exit 1
fi
done
CURL_ARGS=(--fail --show-error --silent --retry 3 --retry-all-errors)
if [ "${PORTAINER_INSECURE_TLS:-false}" = "true" ]; then
CURL_ARGS+=(--insecure)
fi
STACK_BASE="${PORTAINER_URL%/}/api/stacks/${PORTAINER_STACK_ID}"
QUERY="endpointId=${PORTAINER_ENDPOINT_ID}"
# Git-based stacks can be redeployed directly.
if curl "${CURL_ARGS[@]}" \
--header "X-API-Key: ${PORTAINER_API_KEY}" \
--request POST \
"${STACK_BASE}/git/redeploy?${QUERY}" >/dev/null; then
echo "Portainer deploy: git stack redeploy triggered."
exit 0
fi
# Non-git stacks: fetch current stack file and redeploy with pullImage=true.
STACK_FILE_CONTENT="$(
curl "${CURL_ARGS[@]}" \
--header "X-API-Key: ${PORTAINER_API_KEY}" \
"${STACK_BASE}/file" \
| jq -r '.StackFileContent'
)"
PAYLOAD_LOWER="$(jq -cn --arg stackFileContent "${STACK_FILE_CONTENT}" \
'{stackFileContent: $stackFileContent, prune: false, pullImage: true}')"
if curl "${CURL_ARGS[@]}" \
--header "X-API-Key: ${PORTAINER_API_KEY}" \
--header "Content-Type: application/json" \
--request PUT \
--data "${PAYLOAD_LOWER}" \
"${STACK_BASE}?${QUERY}" >/dev/null; then
echo "Portainer deploy: stack updated with lower-camel payload."
exit 0
fi
PAYLOAD_UPPER="$(jq -cn --arg StackFileContent "${STACK_FILE_CONTENT}" \
'{StackFileContent: $StackFileContent, Prune: false, PullImage: true}')"
curl "${CURL_ARGS[@]}" \
--header "X-API-Key: ${PORTAINER_API_KEY}" \
--header "Content-Type: application/json" \
--request PUT \
--data "${PAYLOAD_UPPER}" \
"${STACK_BASE}?${QUERY}" >/dev/null
echo "Portainer deploy: stack updated with upper-camel payload."