Getting a build artifact out of CI and into a person's hands takes three things: a tightly scoped machine credential, an upload that survives interruption, and a link handed back at the end. The two things that actually go wrong are the runner being reclaimed mid-upload and a credential that can do far more than the pipeline needs. Both are avoidable if you decide them before the first run.
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 output must 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 temporary. It may be reclaimed while your upload is in flight. If the upload cannot resume, a long one fails repeatedly and never completes.
- The credential is unattended. Nobody is there to approve anything, so the key must be scoped so that leaking it is survivable.
- The output is often big. Builds, 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 send dist/release.zip \
--to client@example.com \
--message "Build ${GITHUB_REF_NAME}" \
--jsonyungle send runs the whole create–upload–finalise sequence over a resumable protocol, which is the part that matters on a runner. With --json it prints a single JSON object — the transfer and its link — on stdout, while progress goes to stderr, so a later step can parse the result without the progress meter getting in the way. The reference is at docs.yungle.co if you would rather call the API directly, and the API guide walks through the requests.
Scope the credential properly
An API key in CI is a machine credential stored where many people can trigger jobs. Three rules:
Give it the narrowest scope that works. A key that only sends transfers needs transfers:write and nothing else — no collections, no contacts.
One key per pipeline. Revoking one then breaks nothing else, and the usage tells you which pipeline did what.
Make rotation routine. If rotating a key feels scary, you will not do it, and it will be years old on the day it leaks.
One property worth knowing about the model you are buying into: a Yungle key belongs to a workspace, not to the person who created it, and it cannot switch to another workspace later. If a key's reach can be widened by ambient state, you find that out during an incident rather than during evaluation.
Handle failures like a pipeline, not like a person
Retry per request, not per job. Re-running the job re-runs the build. When the upload tool retries internally, a network blip costs seconds, not a rebuild.
Size the timeout to the file. A six-hour default hides a stuck upload; a ten-minute one kills a legitimate 40 GB transfer. Work it out from the artifact size and your runner's upload speed.
Fail loudly. An upload step that swallows errors produces a green build and no delivery — the same failure as stopping between upload and finalise, where the transfer exists, looks sent, and tells nobody.
Do not deliver from a matrix. Five parallel runners each sending the same artifact is a mistake people discover on their bill. Build in the matrix; deliver once, in a single dependent job.
Do not email the artifact
The tempting shortcut is to have CI email the build as an attachment. It fails for the same reasons it fails for people — attachment limits sit around 25 MB, and the recipient's server has its own — plus one you might not expect: an automated system mailing attachments to outside addresses is a good way to earn 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, because a pipeline runs on schedule and never gets bored.
Give each transfer a lifetime instead of accumulating one per build forever. Nightly builds are the classic case: by the end of the year, 364 of them are of no interest to anyone.
Decide what a release deserves compared with a nightly. They are different categories; giving them the same lifetime is how storage grows without anyone deciding anything. Retention as a decision, and why it is also the biggest environmental lever you control.
Checklist
- Does the upload resume? Test it with a deliberate disconnect.
- Is the key scoped to exactly what this pipeline does?
- Is it separate from other pipelines' keys?
- Does the step fail the build when the upload fails?
- Is the timeout derived from the artifact size?
- Does delivery happen once, not once per matrix leg?
- Do artifacts expire, or accumulate forever?
One thing to know about building on Yungle, stated here rather than discovered later: API uploads are metered: 10 GB a month included on every plan, free included, then charged from credit you top up — so a nightly pipeline shipping 5 GB will pass the included amount in its first week. Automation & CI shows how teams usually set this up.