From 3dc6409628e37df3b600df5b900e3f791d53ea0c Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Wed, 9 Sep 2026 21:17:21 +1000 Subject: [PATCH] Assert the release zip's shape before publishing Uploads are immutable, so a malformed archive cannot be replaced at its version path. verify-package checks the zip has one top-level directory, carries lua/, plugin/, doc/ and README.md, and leaks no dev or CI files. --- .woodpecker/release.yaml | 3 ++- Makefile | 31 ++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/.woodpecker/release.yaml b/.woodpecker/release.yaml index e1c989c..c48c893 100644 --- a/.woodpecker/release.yaml +++ b/.woodpecker/release.yaml @@ -23,8 +23,9 @@ steps: - name: package image: alpine:3.22 commands: - - apk add --no-cache git make + - apk add --no-cache git make unzip - make package VERSION=${CI_COMMIT_TAG} + - make verify-package VERSION=${CI_COMMIT_TAG} depends_on: [test] backend_options: kubernetes: diff --git a/Makefile b/Makefile index 30a9dfd..fafd95e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: test lint fmt pre-commit package patch minor major _tag +.PHONY: test lint fmt pre-commit package verify-package patch minor major _tag VERSION ?= $(shell git describe --tags --always) # Archive names drop the leading "v", the way tag source archives do. @@ -24,6 +24,35 @@ package: git archive --format=zip --prefix=arti-pack-$(BARE_VERSION)/ -o $(ARCHIVE) HEAD @echo $(ARCHIVE) +# Published uploads are immutable, so a malformed archive cannot be replaced at +# its version path. Assert the shape before anything ships. +RUNTIME_PATHS = lua/ plugin/ doc/ README.md +EXCLUDED_PATHS = tests/ .woodpecker/ Makefile .editorconfig .gitattributes \ + .gitignore .luacheckrc .pre-commit-config.yaml .stylua.toml + +verify-package: + @test -f $(ARCHIVE) || { echo "verify-package: $(ARCHIVE) not built"; exit 1; } + @names=$$(unzip -Z1 $(ARCHIVE)); \ + roots=$$(printf '%s\n' "$$names" | cut -d/ -f1 | sort -u); \ + if [ "$$(printf '%s\n' "$$roots" | wc -l)" -ne 1 ]; then \ + echo "verify-package: expected one top-level directory, found:"; \ + printf ' %s\n' $$roots; exit 1; \ + fi; \ + fail=0; \ + printf '%s\n' "$$names" | grep -q "^$$roots/." || \ + { echo "verify-package: top-level $$roots is not a directory"; fail=1; }; \ + for p in $(RUNTIME_PATHS); do \ + printf '%s\n' "$$names" | grep -q "^$$roots/$$p" || \ + { echo "verify-package: missing runtime path $$p"; fail=1; }; \ + done; \ + for p in $(EXCLUDED_PATHS); do \ + if printf '%s\n' "$$names" | grep -q "^$$roots/$$p"; then \ + echo "verify-package: excluded path $$p leaked into the archive"; fail=1; \ + fi; \ + done; \ + [ $$fail -eq 0 ] || exit 1; \ + echo "verify-package: $(ARCHIVE) ok, root $$roots" + # Bump helpers — read the latest semver tag and create the next one. # If no tag exists yet, start from v0.0.0. _LATEST = $(shell git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$$' | head -1)