~bigbes/sourcehut-root

sourcehut-root/skills/sourcehut-ci/references/artifacts.md -rw-r--r-- 8.2 KiB
72581ff9 — Eugene Blikh docs(sourcehut-ci): fix factual errors found in full audit 2 days ago

#Build Artifacts

The artifacts: directive uploads files from the build VM to sourcehut after the build completes. They appear as download links on the job page.

#Basic usage

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.

#Path rules — these will trip you up

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.
  • ❌ Two entries with the same basename, e.g. 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.

#Hard limits

  • 8 artifacts max per build. The worker (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.
  • 1 GiB per file. Larger files cause the upload step to fail the entire build with "Artifact exceeds maximum file size". Split into chunks if you must ship more.
  • No upload on failure. Artifact extraction runs only after every task returns success.
  • Storage is S3-backed on the instance (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.

#Only uploaded on success

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.

#Pruned after 90 days

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:

  1. Tag the release with git tag -a vX.Y.Z.
  2. Run a CI build that produces the release artifact.
  3. Upload it to the git tag via the git.sr.ht UI or the GraphQL API (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.

#Pattern: variable-name file → stable artifact

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).

#Pattern: multiple binaries → single archive

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

#Pattern: per-arch fan-out with artifacts

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.

#Pattern: artifact + email notification

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.

#What about uploading artifacts via the 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:

  • attach artifacts to a failed build,
  • attach artifacts to a build after it ended,
  • attach a file named differently from a path that existed in the VM.

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.

#Common failure modes

"No artifacts shown on job page"

  • The build failed (check the log). Artifacts are only uploaded on success.
  • The path doesn't match. artifacts: [mytool] expects exactly /home/build/mytool. If your build wrote to /home/build/myrepo/mytool, the path should be myrepo/mytool.
  • A glob pattern was used. Globs don't expand; the literal string *.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:.

#What artifacts: is not

  • Not for passing files between jobs. Jobs are independent VMs; there's no actions/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.
  • Not for releases. Use annotated git tags with attached files for that.
  • Not for build caching. No download-from-previous-build feature. Implement caching by pushing to a cache repo with a deploy key (see secrets-and-oauth.md).