A simple artifact storage to pipelines.
  • Go 94.2%
  • Dockerfile 5.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
renovate-bot e15662f0b7
All checks were successful
ci/woodpecker/push/change_log Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/cron/vulnerability Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
ci/woodpecker/cron/auto_merge Pipeline was successful
ci/woodpecker/cron/release Pipeline was successful
maint: Update gcr.io/distroless/static-debian13:nonroot Docker digest to 2293b36 #21
Merged automatically
2026-09-14 03:11:29 +00:00
internal/api feat: embed openapi.yaml directly and drop oapi-codegen's spec blob 2026-09-11 19:54:57 +02:00
.gitignore ci: Add ci pipeline files to gitignore (#7) 2026-09-11 19:54:57 +00:00
.goreleaser.yaml feat: CI/CD temporary artifact store 2026-09-11 19:37:01 +02:00
.markdownlint.json docs: add README, markdown lint/format config, and update CLAUDE.md (#4) 2026-09-11 19:16:32 +00:00
.prettierrc.json docs: add README, markdown lint/format config, and update CLAUDE.md (#4) 2026-09-11 19:16:32 +00:00
.trivyignore ci: suppress GHSA-r277-6w6q-xmqw via .trivyignore (#2) 2026-09-11 18:51:02 +00:00
CLAUDE.md fix: Widen artifact id pattern to accept any valid filename character (#19) 2026-09-13 14:21:49 +00:00
Containerfile maint: Update gcr.io/distroless/static-debian13:nonroot Docker digest to 2293b36 #21 2026-09-14 03:11:29 +00:00
go.mod fix: Typo in the project name (#16) 2026-09-13 13:55:45 +00:00
go.sum feat: add OpenAPI 3.1 spec, oapi-codegen server, and Swagger UI 2026-09-11 19:49:58 +02:00
main.go fix: Typo in the project name (#16) 2026-09-13 13:55:45 +00:00
openapi.go feat: embed openapi.yaml directly and drop oapi-codegen's spec blob 2026-09-11 19:54:57 +02:00
openapi.yaml fix: Widen artifact id pattern to accept any valid filename character (#19) 2026-09-13 14:21:49 +00:00
README.md fix: Widen artifact id pattern to accept any valid filename character (#19) 2026-09-13 14:21:49 +00:00
renovate.json ci: Add renovate.json (#13) 2026-09-11 20:31:07 +00:00
server.go fix: Widen artifact id pattern to accept any valid filename character (#19) 2026-09-13 14:21:49 +00:00
store.go feat: structured JSON logging via log/slog, with upload/download events 2026-09-11 20:20:55 +02:00
store_test.go feat: structured JSON logging via log/slog, with upload/download events 2026-09-11 20:20:55 +02:00
swaggerui.go feat: embed openapi.yaml directly and drop oapi-codegen's spec blob 2026-09-11 19:54:57 +02:00

CI/CD Temporary Artifact Store

A small REST service for passing build artifacts between stages or jobs in a CI/CD environment. It stores uploaded artifacts on local disk and automatically deletes them 15 minutes after upload. It is meant to run only on trusted, local infrastructure: there is no TLS and no authentication.

This project was built with AI assistance (Claude Code). Review the code before relying on it in your own environment.

Features

  • Simple HTTP API: upload with POST, download with GET.
  • Artifacts expire automatically 15 minutes after their last upload.
  • Local disk storage only, no external database or object store.
  • Single static binary, no CGO, no runtime dependencies.
  • Structured JSON logging.
  • OpenAPI 3.1 spec and an interactive Swagger UI served by the binary itself.

API

  • POST /artifact?id=<id>: uploads the request body under the given id. Uploading to an id that already exists overwrites it and resets its 15 minute expiry.
    • 201 Created on first upload.
    • 200 OK when an existing id was overwritten.
    • 400 Bad Request if id is missing or invalid.
  • GET /artifact?id=<id>: downloads the stored bytes with the original Content-Type.
    • 404 Not Found if the id was never uploaded or has expired.
    • 400 Bad Request if id is missing or invalid.
  • id must be 1-128 characters, with no / or control characters, and not . or .. (it's used directly as the on-disk filename, so this prevents path traversal while otherwise allowing anything a Linux filename can hold).

Example:

curl -X POST --data-binary @report.txt 'http://localhost:8080/artifact?id=report'
curl 'http://localhost:8080/artifact?id=report' -o report.txt

The full spec is served at /openapi.yaml, and an interactive Swagger UI is available at /swagger/.

Configuration

The server is configured with environment variables. Both are required, there are no defaults.

Variable Description
ARTIFACT_STORE_ADDR Listen address, for example :8080
ARTIFACT_STORE_DIR Directory to store artifacts in

ARTIFACT_STORE_DIR can be a mounted volume. The application only clears a content subdirectory under it on startup, never the directory itself.

Building

Build a single static binary:

CGO_ENABLED=0 go build .

Or with goreleaser:

goreleaser build --snapshot --clean --single-target

The binary has two subcommands:

artifact-store serve     # start the HTTP server
artifact-store version   # print the build version

Container image

A Containerfile is provided. It does not compile the binary, it only copies the binary already built by goreleaser, from ./dist/artifact-store_linux_amd64_v1/. Build the binary first, then build the image from the repository root:

goreleaser build --snapshot --clean --single-target
podman build -f Containerfile -t artifact-store:latest .

The image runs as a non-root user and has no default command, so serve must be passed explicitly:

podman run --rm -p 8080:8080 \
  -e ARTIFACT_STORE_ADDR=:8080 \
  -e ARTIFACT_STORE_DIR=/data \
  -v artifact-data:/data \
  artifact-store:latest serve

Running as a service with Podman Quadlet

Create /etc/containers/systemd/artifact-store.container:

[Unit]
Description=CI/CD artifact store
StartLimitBurst=5
StartLimitIntervalSec=90

[Container]
Exec=serve
Image=code.thinkaboutit.tech/pandora/artifact-store.goapp:latest
Volume=artifact-data:/data
PublishPort=8080:8080

Environment="ARTIFACT_STORE_ADDR=:8080"
Environment="ARTIFACT_STORE_DIR=/data"

[Service]
Restart=on-failure
RestartSec=2

[Install]
# If you want auto-start after reboot
WantedBy=default.target

Then reload systemd and start it:

systemctl daemon-reload
systemctl start artifact-store.service

Running as a systemd service on NixOS

See Pandora flakse.

Development

go build ./...
go vet ./...
go test ./...
golangci-lint run ./...

After changing openapi.yaml, regenerate the server code with:

go generate ./...

internal/api/api.gen.go is generated and must not be edited by hand.

License

GPL-3.0.