fix(action): build payload with jq and document that swarm mode deploys the tag

Interpolating inputs straight into the JSON string let a quote in image or
tag break the payload. Also replaces GNU-only head -n -1 so the action runs
on BSD/macOS runners.
This commit is contained in:
2026-10-12 08:44:27 +02:00
parent 9ea082de82
commit ee38e36bb7
3 changed files with 20 additions and 7 deletions
+4
View File
@@ -73,6 +73,10 @@ curl -sH "Authorization: Bearer $UPDATER_API_KEY" \
with a permission error against a worker-only node. This is an operator/deployment
concern (point `DOCKER_HOST` at a manager, or schedule the updater on a manager),
not something the code can detect or work around.
- **`tag` is cosmetic in compose mode but load-bearing in swarm mode.** `handlers.go` folds it
into the requested image; compose discovery strips it via `NormaliseImage`, but
`SwarmExecutor` assigns it directly to `ContainerSpec.Image`. A request without a tag updates
a Swarm service to `:latest`.
## References
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
|---|---|---|---|
| `endpoint` | yes | — | Full URL to `/update` |
| `image` | yes | — | Image reference without tag |
| `tag` | no | `""` | Tag that was just pushed (logged for audit) |
| `tag` | no | `""` | Tag that was just pushed. Compose mode ignores it (the compose file pins the reference); **swarm mode sets the service image to it**, so omitting it deploys `:latest`. Always pass it. |
| `token` | yes | — | Bearer token configured in package-updater |
## Failure modes
+15 -6
View File
@@ -8,7 +8,7 @@ inputs:
description: "Image reference without tag (e.g. registry.example.com/myapp)"
required: true
tag:
description: "Tag that was just pushed (for logging)"
description: "Tag that was just pushed. Required in practice for swarm instances — it becomes the image the service is set to. Omit it and swarm deploys :latest."
required: false
default: ""
token:
@@ -20,18 +20,27 @@ runs:
- name: Trigger update
shell: bash
env:
ENDPOINT: ${{ inputs.endpoint }}
IMAGE: ${{ inputs.image }}
TAG: ${{ inputs.tag }}
TOKEN: ${{ inputs.token }}
run: |
set -euo pipefail
# Build the payload with jq so quotes or backslashes in the inputs
# cannot break out of the JSON string.
payload=$(jq -nc --arg image "$IMAGE" --arg tag "$TAG" \
'{image: $image, tag: $tag}')
response=$(curl -sS -w "\n%{http_code}" \
-X POST "${{ inputs.endpoint }}" \
-X POST "$ENDPOINT" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"image\":\"${{ inputs.image }}\",\"tag\":\"${{ inputs.tag }}\"}")
body=$(echo "$response" | head -n -1)
code=$(echo "$response" | tail -n 1)
-d "$payload")
# Split on the last newline: curl's -w appended the status code
# there. Pure bash — `head -n -1` is GNU-only and fails on BSD/macOS.
code="${response##*$'\n'}"
body="${response%$'\n'*}"
echo "HTTP $code"
echo "$body" | jq .
printf '%s' "$body" | jq . || printf '%s\n' "$body"
if [ "$code" -ge 400 ]; then
exit 1
fi