Sending build artifacts from CI

By Hein de Wilde··4 min read

Getting a build artifact out of CI and to a person is three steps — authenticate with a machine credential, upload with something resumable, and hand back a link — and the two things that go wrong are the runner being reclaimed mid-upload and the credential being scoped too widely. Both are avoidable if you decide them up front.

Why CI artifacts are their own problem

Most CI systems have built-in artifact storage, and for anything that stays inside the pipeline you should use it. This is about the case where the artifact has to reach a human outside the build system: a client build, a nightly render, a signed installer for someone to test.

Three properties make that different from an ordinary upload:

The runner is ephemeral. It may be reclaimed while your upload is in flight. If the upload is not resumable, a long one will fail repeatedly and never complete.

The credential is unattended. Nobody is there to approve anything, so the token has to be scoped tightly enough that its leaking is survivable.

The output is often large. Build outputs, container images, game builds and rendered media routinely run to tens of gigabytes.

The shape

# .github/workflows/deliver.yml
name: Deliver build
on:
  workflow_dispatch:
  push:
    tags: ['v*']

jobs:
  deliver:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build
        run: make release        # produces dist/release.zip

      - name: Send it
        env:
          YUNGLE_API_KEY: ${{ secrets.YUNGLE_API_KEY }}
        run: |
          npx -y yungle-cli upload dist/release.zip \
            --to client@example.com \
            --message "Build ${GITHUB_REF_NAME}"

The CLI handles the create-upload-finalise sequence and uses a resumable protocol underneath, which is the part that matters on a runner. The API reference is at /developers if you would rather call it directly, and the API guide covers the request sequence.

Scope the credential properly

An API key in CI is a machine credential that lives in a place many people can trigger. Three rules:

Give it the narrowest scope that works. If it only ever creates transfers, it does not need permission to read collections or manage contacts.

Do not reuse it across pipelines. One key per pipeline means revoking one does not break the others, and you can tell from usage which pipeline is doing what.

Treat rotation as routine, not as an incident. If rotating a key is a scary operation, you will not do it, and then it will be years old when it leaks.

Worth knowing about the model you are buying into: a well-designed API key belongs to a workspace, not to whoever created it, and cannot switch to another workspace later. If a key's reach can be widened by ambient state, that is a property you find out about during an incident rather than during evaluation.

Handling failures like a pipeline, not like a person

Retry at the chunk level, not the job level. Re-running the whole job re-runs the build. If your upload tool retries chunks internally, a network blip costs seconds instead of a rebuild.

Set a timeout that reflects the file size. A default six-hour job timeout hides a stuck upload; a ten-minute one kills a legitimate 40 GB transfer. Compute it from the artifact size and your runner's egress rate.

Fail loudly. An upload step that swallows errors produces a green build and no delivery — the same failure mode as stopping halfway through the create-upload-finalise sequence, where the transfer exists, looks sent, and notifies nobody.

Do not upload from a matrix job. Five parallel runners each sending the same artifact is a mistake people find out about via their bill. Build in the matrix, deliver in a single dependent job.

Do not email the artifact

A tempting shortcut is to have CI email the build as an attachment. It fails for the same reasons it fails for humans: attachment limits are about 25 MB, and the recipient's server has its own. It also adds one you would not expect — an automated system emailing attachments to external addresses is a good way to acquire a poor sending reputation.

Send a link. The email stays small, it does not bounce, and the link can expire.

Retention and cleanup

Artifacts pile up faster than anything else, because a pipeline runs on a schedule and never gets bored.

Set an expiry on the transfer rather than accumulating one per build forever. Nightly builds are the classic case: 364 of them are of no interest by the time the year ends.

Decide what a release build deserves versus a nightly. They are different categories and giving them the same lifetime is how storage bills grow without anyone deciding anything. Retention as a decision, and why it is also the largest environmental lever you control.

Checklist

  1. Is the upload resumable? Test it with a deliberate disconnect.
  2. Is the key scoped to just what this pipeline does?
  3. Is it a separate key from other pipelines?
  4. Does the step fail the build when the upload fails?
  5. Is the timeout derived from the artifact size?
  6. Does delivery happen once, not once per matrix leg?
  7. Do artifacts expire, or accumulate forever?

Two caveats about building on Yungle specifically, stated here rather than discovered later: it runs on a single server with no failover and no service level agreement, which is documented at /developers. And API uploads are metered — 10 GB a month included on every tier including free, then charged from prepaid credit. A nightly pipeline shipping a 5 GB artifact will pass that in the first week, so work out the number before you wire it up.

Hein de Wilde

I build and run Yungle, and I write everything here. Comparisons name competitors and credit them, every claim about another company comes from that company’s own documentation, and where we fall short it says so. More about who is behind this.

Read next