From 2960321b0b66a94a31a56d592ee0d114d801d7d2 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Wed, 9 Sep 2026 21:06:09 +1000 Subject: [PATCH 1/4] Publish tagged source zips to artifactapi Add a tag-triggered release pipeline and a package target that builds the archive shape arti-pack's own installer consumes. --- .gitattributes | 10 ++++++ .gitignore | 1 + .woodpecker/release.yaml | 75 ++++++++++++++++++++++++++++++++++++++++ Makefile | 14 +++++++- README.md | 19 ++++++++-- 5 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 .gitattributes create mode 100644 .woodpecker/release.yaml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..db33257 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# Kept out of the release archive: it ships to a package path, not a checkout. +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.luacheckrc export-ignore +.pre-commit-config.yaml export-ignore +.stylua.toml export-ignore +.woodpecker export-ignore +Makefile export-ignore +tests export-ignore diff --git a/.gitignore b/.gitignore index 926ccaa..3fd8493 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ doc/tags +dist/ diff --git a/.woodpecker/release.yaml b/.woodpecker/release.yaml new file mode 100644 index 0000000..195272c --- /dev/null +++ b/.woodpecker/release.yaml @@ -0,0 +1,75 @@ +when: + - event: tag + +steps: + - name: test + image: alpine:3.22 + commands: + - apk add --no-cache neovim curl unzip tar zip make + - make test + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 512Mi + cpu: 1 + limits: + memory: 2Gi + cpu: 2 + + # A source zip wrapping everything in a single "arti-pack-" + # directory, the shape arti-pack's installer expects from a tag archive. + - name: package + image: alpine:3.22 + commands: + - apk add --no-cache git make + - git config --global --add safe.directory ${CI_WORKSPACE} + - make package VERSION=${CI_COMMIT_TAG} + depends_on: [test] + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 128Mi + cpu: 100m + limits: + memory: 512Mi + cpu: 500m + + # Publish under the tag-archive path shape so arti-pack installs itself with + # registry ".../api/v2/remotes/nvim-plugins/files" and { 'unkin/arti-pack' }. + - name: publish + image: alpine:3.22 + commands: + - apk add --no-cache curl + - | + HOST="https://artifactapi.k8s.syd1.au.unkin.net" + REPO="nvim-plugins" + PATH_IN_REPO="unkin/arti-pack/archive/refs/tags/${CI_COMMIT_TAG}.zip" + ZIP=$$(ls dist/arti-pack-*.zip | head -1) + # Uploads are immutable (409 on overwrite); a re-run of an existing tag + # must not fail the pipeline. artifactapi has no HEAD route, so probe + # with GET. + code=$$(curl -s -o /dev/null -w '%{http_code}' "$$HOST/api/v2/remotes/$$REPO/files/$$PATH_IN_REPO" || true) + if [ "$$code" = "200" ]; then + echo "$$PATH_IN_REPO already published (HTTP $$code); skipping upload" + exit 0 + fi + echo "Uploading $$ZIP to $$REPO/$$PATH_IN_REPO (existence probe returned $$code)" + curl -f -X PUT \ + "$$HOST/api/v2/remotes/$$REPO/files/$$PATH_IN_REPO" \ + -H "Content-Type: application/zip" \ + --data-binary @"$$ZIP" + depends_on: [package] + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 128Mi + cpu: 100m + limits: + memory: 512Mi + cpu: 500m diff --git a/Makefile b/Makefile index c3d44e3..30a9dfd 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,10 @@ -.PHONY: test lint fmt pre-commit patch minor major _tag +.PHONY: test lint fmt pre-commit package patch minor major _tag + +VERSION ?= $(shell git describe --tags --always) +# Archive names drop the leading "v", the way tag source archives do. +BARE_VERSION = $(patsubst v%,%,$(VERSION)) +DIST = dist +ARCHIVE = $(DIST)/arti-pack-$(BARE_VERSION).zip test: nvim --headless -l tests/run.lua @@ -12,6 +18,12 @@ fmt: pre-commit: @uvx pre-commit run --all-files +# The prefix directory is what arti-pack's own extractor strips on install. +package: + @mkdir -p $(DIST) + git archive --format=zip --prefix=arti-pack-$(BARE_VERSION)/ -o $(ARCHIVE) HEAD + @echo $(ARCHIVE) + # 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) diff --git a/README.md b/README.md index 2b3e58c..1c5819e 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,28 @@ serves RPMs, containers and Terraform providers. ## Install -arti-pack manages itself least awkwardly as a plain clone into the package path: +Each tag is published as a source zip to the `nvim-plugins` artifactapi repo, +so arti-pack installs the same way as everything it manages: ```sh -git clone https://git.unkin.net/unkin/arti-pack \ +curl -fsSL -o /tmp/arti-pack.zip \ + https://artifactapi.k8s.syd1.au.unkin.net/api/v2/remotes/nvim-plugins/files/unkin/arti-pack/archive/refs/tags/v0.1.0.zip +mkdir -p "${XDG_DATA_HOME:-$HOME/.local/share}/nvim/site/pack/arti-pack/start" +unzip -q /tmp/arti-pack.zip -d /tmp/arti-pack +mv /tmp/arti-pack/arti-pack-0.1.0 \ "${XDG_DATA_HOME:-$HOME/.local/share}/nvim/site/pack/arti-pack/start/arti-pack" ``` +Once installed it can carry itself, since `url` bypasses the registry: + +```lua +{ + 'unkin/arti-pack', + tag = 'v0.1.0', + url = 'https://artifactapi.k8s.syd1.au.unkin.net/api/v2/remotes/nvim-plugins/files/unkin/arti-pack/archive/refs/tags/v0.1.0.zip', +} +``` + ## Usage ```lua -- 2.47.3 From 8f49c50ef3367b3dd721efed54e04473d8cead96 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Wed, 9 Sep 2026 21:08:22 +1000 Subject: [PATCH 2/4] Drop the redundant safe.directory config from the package step --- .woodpecker/release.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.woodpecker/release.yaml b/.woodpecker/release.yaml index 195272c..e1c989c 100644 --- a/.woodpecker/release.yaml +++ b/.woodpecker/release.yaml @@ -24,7 +24,6 @@ steps: image: alpine:3.22 commands: - apk add --no-cache git make - - git config --global --add safe.directory ${CI_WORKSPACE} - make package VERSION=${CI_COMMIT_TAG} depends_on: [test] backend_options: -- 2.47.3 From 3dc6409628e37df3b600df5b900e3f791d53ea0c Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Wed, 9 Sep 2026 21:17:21 +1000 Subject: [PATCH 3/4] 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) -- 2.47.3 From 9b33f27d47849f23e2d3dd279a710feec5a5383b Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Wed, 9 Sep 2026 22:35:54 +1000 Subject: [PATCH 4/4] Use a placeholder host in the install instructions The install snippets named one specific artifact repository by hostname. - fetch the release zip from a placeholder host under the same tag-archive path shape --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cb8e8ff..d45d867 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,13 @@ from. ## Install -Each tag is published as a source zip to the `nvim-plugins` artifactapi repo, -so arti-pack installs the same way as everything it manages: +Each tag is published as a source zip under the same tag-archive path shape +arti-pack expects, so it installs the same way as everything it manages — +mirror it into your own artifact repository and fetch it from there: ```sh curl -fsSL -o /tmp/arti-pack.zip \ - https://artifactapi.k8s.syd1.au.unkin.net/api/v2/remotes/nvim-plugins/files/unkin/arti-pack/archive/refs/tags/v0.1.0.zip + https://artifacts.example.com/artifactory/nvim-plugins/unkin/arti-pack/archive/refs/tags/v0.1.0.zip mkdir -p "${XDG_DATA_HOME:-$HOME/.local/share}/nvim/site/pack/arti-pack/start" unzip -q /tmp/arti-pack.zip -d /tmp/arti-pack mv /tmp/arti-pack/arti-pack-0.1.0 \ @@ -36,7 +37,7 @@ Once installed it can carry itself, since `url` bypasses the registry: { 'unkin/arti-pack', tag = 'v0.1.0', - url = 'https://artifactapi.k8s.syd1.au.unkin.net/api/v2/remotes/nvim-plugins/files/unkin/arti-pack/archive/refs/tags/v0.1.0.zip', + url = 'https://artifacts.example.com/artifactory/nvim-plugins/unkin/arti-pack/archive/refs/tags/v0.1.0.zip', } ``` -- 2.47.3