From ee38e36bb778959c555de00191a8003be8eaa7f1 Mon Sep 17 00:00:00 2001 From: Samuel Enocsson Date: Mon, 12 Oct 2026 08:44:27 +0200 Subject: [PATCH] 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. --- CLAUDE.md | 4 ++++ gitea-action/README.md | 2 +- gitea-action/action.yml | 21 +++++++++++++++------ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9b0153b..efaf1fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/gitea-action/README.md b/gitea-action/README.md index bc0816a..b03760b 100644 --- a/gitea-action/README.md +++ b/gitea-action/README.md @@ -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 diff --git a/gitea-action/action.yml b/gitea-action/action.yml index 19148e8..d1e95de 100644 --- a/gitea-action/action.yml +++ b/gitea-action/action.yml @@ -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 -- 2.54.0