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>
158 lines
6.0 KiB
YAML
158 lines
6.0 KiB
YAML
name: Build and Push Image
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
workflow_dispatch:
|
|
|
|
env:
|
|
REGISTRY_HOST: registry.campbellwireless.net
|
|
IMAGE_NAME: ${{ github.repository }}
|
|
|
|
jobs:
|
|
docker-build-and-push:
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Checkout
|
|
env:
|
|
REPO_URL: https://cloud.campbellwireless.net/git/${{ github.repository }}.git
|
|
run: |
|
|
set -eux
|
|
git init .
|
|
git remote add origin "$REPO_URL"
|
|
auth="$(printf '%s' '${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}' | base64 | tr -d '\n')"
|
|
git config --local http.https://cloud.campbellwireless.net/.extraheader "AUTHORIZATION: basic $auth"
|
|
git fetch --prune --no-recurse-submodules origin +refs/heads/*:refs/remotes/origin/* +refs/tags/*:refs/tags/*
|
|
git checkout --detach "${{ github.sha }}"
|
|
|
|
- name: Setup Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Log in to Gitea registry
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ env.REGISTRY_HOST }}
|
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
|
|
|
- name: Compute image tags
|
|
id: tags
|
|
run: |
|
|
echo "sha_short=$(echo '${{ github.sha }}' | cut -c1-12)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Build and push image
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: .
|
|
file: ./Dockerfile
|
|
push: true
|
|
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."
|