The artifacts: directive uploads files from the build VM to sourcehut after the build completes. They appear as download links on the job page.
image: alpine/edge
packages:
- go
sources:
- https://git.sr.ht/~user/mytool
tasks:
- build: |
cd mytool
go build -o /home/build/mytool ./cmd/mytool
artifacts:
- mytool
After this build succeeds, the job page at https://builds.sr.ht/~user/job/N shows a download link for mytool.
Paths are interpreted literally. No shell expansion, no globs, no ~, no environment variables.
mytool — relative to the build user's home directory (/home/build on Linux; /root on most BSD images — set by the image's Homedir).myproject/dist/release.tar.gz — nested relative path./home/build/output/binary — absolute path.*.tar.gz — glob, doesn't match anything literally.~/dist/binary — ~ isn't expanded (the worker explicitly checks for ~ and prints "You probably need to remove ~/ from the artifact path." when an artifact fails to read).$HOME/output — no env expansion.dist/ — directories aren't supported, only single files.bin/foo and lib/foo — the manifest parser rejects this with "Expected artifacts to be a list of unique file paths" because the artifact's display name is the basename and would collide.If your build produces files with unpredictable names (versioned filenames, hash-suffixed output), rename them in a task to a stable name first, or tar them all together.
worker/tasks.go:UploadArtifacts) logs "no more than 8 artifacts per build are accepted" and silently drops the rest — the build doesn't fail, you just get fewer downloads than expected.builds.sr.ht::worker/s3-bucket config). On a self-hosted instance without S3 configured, artifacts: will fail with "Build artifacts were requested, but S3 is not configured for this build runner." — check the instance config before relying on it.If any task fails, no artifacts are uploaded. The VM persists for SSH debugging, so you can pull files manually with scp if you really need them from a failed build — but the standard artifacts: mechanism is success-only.
Artifacts on free sourcehut accounts are deleted 90 days after the build. Don't use this as long-term storage. For releases, the right pattern is:
git tag -a vX.Y.Z.uploadArtifact mutation), which attaches the file to the tag's "refs" page where it lives indefinitely.The git.sr.ht annotated-tag attachment is the durable equivalent of GitHub Releases.
Build produces myapp-v1.2.3-linux-amd64.tar.gz but you want the artifact named consistently:
tasks:
- build: |
cd myapp
make release VERSION=v1.2.3
# ends up at myapp/dist/myapp-v1.2.3-linux-amd64.tar.gz
- rename: |
cp myapp/dist/myapp-*-linux-amd64.tar.gz /home/build/myapp-linux-amd64.tar.gz
artifacts:
- myapp-linux-amd64.tar.gz
Two-task split because shell expansion only works inside tasks (which are scripts), not inside artifacts: (which is just YAML strings).
Building several binaries and want one downloadable bundle:
tasks:
- build: |
cd mytool
go build -o /home/build/dist/mytool ./cmd/mytool
go build -o /home/build/dist/mytool-cli ./cmd/cli
go build -o /home/build/dist/mytoold ./cmd/daemon
- package: |
cd /home/build/dist
tar -czf /home/build/mytool-bundle.tar.gz .
artifacts:
- mytool-bundle.tar.gz
Use separate manifests in .builds/ so each architecture is a separate job with its own artifact set:
# .builds/amd64.yml
image: alpine/edge
arch: x86_64
packages: [go]
sources: [https://git.sr.ht/~user/mytool]
tasks:
- build: |
cd mytool
go build -o /home/build/mytool-amd64 ./cmd/mytool
artifacts:
- mytool-amd64
# .builds/aarch64.yml
image: alpine/edge
arch: aarch64
packages: [go]
sources: [https://git.sr.ht/~user/mytool]
tasks:
- build: |
cd mytool
go build -o /home/build/mytool-aarch64 ./cmd/mytool
artifacts:
- mytool-aarch64
Each job is independent and produces its own downloads. There's no built-in "matrix output" concept — each job's artifacts page is its own; if you want everything in one place, use a trigger to push them to a release tag, S3, or your own server.
image: alpine/edge
packages: [make, gcc]
sources: [https://git.sr.ht/~user/myproject]
tasks:
- build: |
cd myproject
make release
cp dist/myproject-*.tar.gz /home/build/release.tar.gz
artifacts:
- release.tar.gz
triggers:
- action: email
condition: success
to: "Me <me@example.com>"
The email contains a link to the job page, where the artifact is downloadable. Triggers don't see artifact URLs directly, so a webhook trigger isn't more useful here unless it pulls the job details from the GraphQL API.
There is no user-callable mutation to upload arbitrary files to a job. The schema does have createArtifact(jobId, path, contents) but it's decorated @worker and rejects normal user tokens — only the runner that owns the job can call it. So you cannot:
If you need any of those, write to your own storage from inside the build (scp/rsync/S3 PUT) and treat the link as the artifact instead.
For release artifacts that should live indefinitely, the right primitive is git.sr.ht's uploadArtifact(repoId: Int!, revspec: String!, file: Upload!) (scope OBJECTS:RW), which attaches files to a git ref (typically an annotated tag) — these show up on the tag's page and aren't pruned. hut git artifact upload … is a wrapper.
"No artifacts shown on job page"
artifacts: [mytool] expects exactly /home/build/mytool. If your build wrote to /home/build/myrepo/mytool, the path should be myrepo/mytool.*.tar.gz is searched for and not found."Artifact name is weird / has the wrong directory prefix"
The artifact filename in the UI is derived from the basename of the path. artifacts: [dist/myapp] shows up as myapp, not dist/myapp. If you want a different display name, rename the file before listing it as an artifact.
"Artifact is empty"
The most common cause: building into a temporary directory that got cleaned up, then listing a path that doesn't exist. Add a ls -la /home/build/ task before the build to verify file locations during debugging.
"Artifact upload timed out"
Very large artifacts (multi-GB) may time out on upload. Split into smaller files, or upload to your own storage from inside the build and skip artifacts:.
artifacts: is notactions/upload-artifact + actions/download-artifact pairing. To pass data between jobs, use a trigger of type job and include data inline, or upload to external storage.secrets-and-oauth.md).