GitLab CI Artifacts
Define, use and download GitLab CI artifacts. Passing files between jobs, expiration times, test reports and deployment packages.
Every GitLab job runs in a fresh environment. That clean isolation is great for reliability, but it creates a problem: files produced by one job do not automatically exist in the next job. If your build job creates a compiled app, your test job cannot see it unless you explicitly preserve and pass it forward. That is exactly what artifacts are for.
Artifacts are one of the most important GitLab CI/CD concepts because they let you move meaningful outputs through a pipeline and make them downloadable later from the GitLab UI.
What Are Artifacts?
An artifact is a file or directory saved from a job after the job finishes. GitLab stores those files for a configurable amount of time so they can be:
- downloaded from the job or pipeline page
- consumed by later jobs in the same pipeline
- processed as structured reports such as JUnit test results
Typical artifacts include:
- compiled binaries
dist/folders from frontend builds- zipped deployment packages
- JUnit XML test reports
- coverage reports
- generated documentation
- logs collected for debugging failed jobs
A useful mental model is this:
- Artifacts preserve outputs that matter for correctness
- Cache preserves reusable inputs for speed
If a later job truly needs a file created earlier, artifacts are usually the right answer.
Defining Artifacts with artifacts: paths:
The most common way to create artifacts is with artifacts: paths:.
build-app:
stage: build
image: node:20
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
After the job succeeds, GitLab saves the dist/ directory as a job artifact.
What paths means
Each path is relative to the project directory in the job workspace. You can store files or folders.
artifacts:
paths:
- dist/
- coverage/
- reports/junit.xml
That tells GitLab to archive multiple outputs from the same job.
Why this matters
Suppose your build step compiles a Node.js app into dist/. If you want a later packaging or deployment job to use that compiled output, you should not rebuild it unless you have to. Artifacts allow the downstream job to use the exact build result produced earlier.
artifacts: expire_in: for Automatic Cleanup
Artifacts take storage space, so GitLab lets you define how long they should be kept.
build-app:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
After one week, GitLab automatically removes the stored artifact.
Common expiration choices
1 hourfor temporary build outputs1 dayfor frequent branch pipelines1 weekfor normal CI outputs1 monthor longer for release bundles and reports you want to retain
Choose the shortest retention that still supports your team workflow. A quick feature-branch build probably does not need to live forever. A release package may need a much longer lifetime.
Why expiration is important
Without cleanup, artifact storage can grow quickly. Large projects build many branches, many merge requests, and many reports. expire_in keeps storage predictable and prevents old, unused pipeline files from piling up.
Downloading Artifacts from the GitLab UI
One of the easiest benefits of artifacts is visibility. When a job stores artifacts, GitLab exposes them in the UI.
From the pipeline page or job page, you can typically:
- open the job details
- find the artifact section
- download the artifact archive
- inspect test or report output if the artifact is a recognized report type
This is especially useful when you want to:
- download a compiled application package
- inspect a generated report after a CI run
- retrieve a debug log from a failed pipeline
- share a build output with teammates without rebuilding locally
For many teams, artifacts become the bridge between automation and human review.
Passing Files to Later Jobs
By default, later jobs in later stages can download artifacts from earlier jobs. That makes a simple build-then-test or build-then-deploy pipeline straightforward.
stages:
- build
- deploy
build-app:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
deploy-app:
stage: deploy
script:
- test -d dist
- ./deploy.sh
In this example, deploy-app can use the dist/ directory created by build-app because it is an artifact from an earlier stage.
That behavior is convenient, but in larger pipelines you often want tighter control over exactly which artifacts get downloaded.
Accessing Artifacts from Previous Jobs: dependencies vs needs
There are two related ideas here:
dependenciescontrols which earlier job artifacts a job downloadsneedscreates an execution dependency and can also fetch artifacts sooner in DAG pipelines
Using dependencies
dependencies is useful when you want to limit artifact downloads to specific previous jobs.
package-app:
stage: deploy
script:
- tar -czf release.tar.gz dist
dependencies:
- build-app
This tells GitLab to download artifacts only from build-app, not from every earlier job that might have published artifacts.
Why does that matter? Because downloading unnecessary artifacts wastes time and bandwidth. In a large pipeline, selective downloads can make jobs faster and cleaner.
Using needs
needs is about the job graph. It allows a job to start as soon as its required upstream jobs finish, instead of waiting for the whole previous stage.
package-app:
stage: deploy
needs:
- job: build-app
artifacts: true
script:
- tar -czf release.tar.gz dist
Here, package-app explicitly needs build-app and requests its artifacts. This is powerful because the job can start earlier in a DAG pipeline and still receive the required files.
When to use which
- Use
dependencieswhen you mainly want to control which earlier artifacts are downloaded. - Use
needswhen you want both faster scheduling and explicit dependency relationships.
In newer pipelines, needs is often the more flexible pattern because it improves both clarity and speed.
artifacts: reports: for Structured GitLab Reports
Not all artifacts are just downloadable archives. GitLab understands certain artifact types as reports and uses them to power UI features.
unit-tests:
stage: test
script:
- npm ci
- npm test -- --reporters=jest-junit
artifacts:
reports:
junit: junit.xml
In this case, junit.xml is not only stored as an artifact. GitLab also interprets it as a JUnit test report and can show test results in the pipeline and merge request UI.
Common report artifact types
JUnit test reports
artifacts:
reports:
junit: junit.xml
Use this for unit test or integration test output in JUnit XML format.
Coverage reports
Coverage reporting often uses a specific format produced by your test tool.
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
This helps GitLab visualize code coverage information.
SAST and security reports
Security scanners can produce reports that GitLab understands.
artifacts:
reports:
sast: gl-sast-report.json
The same idea applies to dependency scanning and other security-focused report types in GitLab.
Why reports are special
A normal artifact is something you download. A report artifact is something GitLab can parse and surface directly in the product experience. That makes pipelines much more useful because results are visible where developers already work.
artifacts: when:
By default, artifacts are usually saved on successful jobs. Sometimes you want different behavior.
artifacts:
when: always
paths:
- logs/
Common values are:
on_successon_failurealways
on_success
Save artifacts only if the job succeeds. This is the normal choice for build outputs and deployment bundles.
on_failure
Save artifacts only if the job fails. This is great for debugging data.
integration-tests:
stage: test
script:
- ./run-integration-tests.sh
artifacts:
when: on_failure
paths:
- logs/
- screenshots/
If the test run fails, you can collect screenshots, logs, and traces for investigation.
always
Save artifacts regardless of result. This is useful when diagnostic output is valuable even on passing runs.
artifacts: untracked:
GitLab can also save untracked files automatically.
build-debug:
stage: build
script:
- ./generate-debug-files.sh
artifacts:
untracked: true
This captures files created during the job that are not tracked by Git.
Why use it carefully
untracked: true is convenient, but it can also collect more than you expect. Large temporary files, generated directories, or accidental outputs may get stored. For predictable pipelines, paths: is usually clearer. Use untracked when you genuinely want all generated untracked files and understand the storage impact.
Real Example 1: Build Artifact for a Compiled Application
Imagine a frontend app built with Node.js.
stages:
- build
- deploy
build-web:
stage: build
image: node:20
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
deploy-web:
stage: deploy
image: alpine:3.20
dependencies:
- build-web
script:
- test -d dist
- ./scripts/deploy-dist.sh
Here, the build output in dist/ is preserved and then consumed by deployment. This ensures the deployment job uses the exact files generated during the verified build.
Real Example 2: Test Report Artifact
Now imagine a test job that produces both human-readable output and a machine-readable JUnit report.
unit-tests:
stage: test
image: node:20
script:
- npm ci
- npm test -- --reporters=jest-junit
artifacts:
reports:
junit: junit.xml
paths:
- junit.xml
expire_in: 3 days
The file is stored as a normal artifact and also recognized as a report. That means:
- developers can download it if needed
- GitLab can show test results in the UI
- merge requests get better visibility into failures
Real Example 3: Deployment Package Artifact
Suppose you want to package a Node.js application into a tarball for release.
stages:
- build
- package
- deploy
build-app:
stage: build
image: node:20
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
create-package:
stage: package
image: alpine:3.20
needs:
- job: build-app
artifacts: true
script:
- tar -czf release.tar.gz dist
artifacts:
paths:
- release.tar.gz
expire_in: 1 month
deploy-prod:
stage: deploy
image: alpine:3.20
dependencies:
- create-package
script:
- ls -lh release.tar.gz
- ./deploy-release.sh release.tar.gz
This pattern is common in production pipelines. Build first, package once, then deploy the packaged result.
Good Artifact Hygiene
Artifacts are powerful, but they should be intentional.
Save what matters, not everything
Do not store entire workspaces unless you have a strong reason. Store only the files later jobs or humans actually need.
Set expiration times
A missing expire_in is not always wrong, but clear retention is healthier for storage management.
Use reports where possible
If GitLab can interpret a report, let it. Structured visibility is better than forcing people to download raw files.
Avoid rebuilding if a verified build output already exists
A downstream job should ideally consume the tested build artifact rather than repeating the build. That improves consistency.
Common Mistakes
-
Expecting files to survive automatically between jobs Each job is isolated. Without artifacts, generated files disappear.
-
Using cache instead of artifacts for pipeline outputs Cache is not a correctness mechanism. Artifacts are.
-
Storing too many large files forever This creates unnecessary storage cost.
-
Not narrowing downloads in large pipelines Use
dependenciesorneedsto avoid pulling unrelated artifacts. -
Forgetting report syntax A file under
paths:alone does not automatically become a GitLab test or coverage report.
Final Takeaway
Artifacts make pipelines practical. They preserve outputs, move files between isolated jobs, expose useful downloads in the GitLab UI, and power rich reporting features. Once you start treating artifacts as deliberate pipeline handoffs, your CI/CD design becomes more reliable and much easier to reason about.
Knowledge Check
Question 1: Purpose of Artifacts
What is the primary reason to use artifacts in GitLab CI/CD?
Question 2: Expiration
What does artifacts: expire_in: 1 week do?
Question 3: Reports
Why would you use artifacts: reports: junit: junit.xml in a test job?
Frequently Asked Questions
What is the difference between artifacts and cache in GitLab CI?
Artifacts preserve job output that matters to the result, compiled binaries, test reports, deployment bundles, and are passed to later jobs and downloadable from the UI. Cache stores reusable inputs such as node_modules or a pip download directory to make future runs faster. The distinction that matters is correctness: artifacts are guaranteed to be there, while a cache may be missing, stale or from another branch, so a pipeline must never depend on cache contents to produce a correct result.
How do I pass files from one job to another?
Declare them as artifacts in the producing job with artifacts: paths:, and by default every job in a later stage downloads them automatically. To control that precisely, use needs: to name the specific jobs whose artifacts you want, or dependencies: to restrict it, dependencies: [] downloads nothing at all, which is worth setting on jobs that do not need them since it speeds them up noticeably.
How long are artifacts kept?
Until their expire_in elapses, defaulting to 30 days on GitLab.com unless the instance or project overrides it. Set expire_in explicitly on every artifact: an hour is plenty for files only passed between jobs in the same pipeline, while release builds might justify weeks. Artifacts consume project storage and are a common cause of unexpectedly hitting a storage quota, since a pipeline running dozens of times a day accumulates quickly. The latest artifacts for the most recent successful pipeline on a ref are kept regardless unless you disable that behaviour.
What is artifacts: reports: for?
It tells GitLab that an artifact is a structured report of a known type, JUnit test results, code coverage, SAST findings, a dotenv file, so the platform can parse it and surface the contents in the merge request rather than leaving it as a file to download. A JUnit report turns into a test summary showing exactly which tests newly failed, which is far more useful than reading a log. Report artifacts are subject to their own retention rules and, for some types, size limits.
Why are my artifacts empty or missing?
Usually the paths are wrong relative to the project root, which is where they are evaluated from, not from wherever the script happened to cd. The job log lists which files were collected, so check there first. The other frequent cause is that the job failed before producing them: by default artifacts are only uploaded when a job succeeds, so if you want them from failures too, for example to capture logs or screenshots, set artifacts: when: always.
Can I download artifacts without using the UI?
Yes. The GitLab API exposes them at a per-job endpoint, and there is a stable URL for the latest artifacts from a successful job on a given ref, which is what most external tooling uses. A CI job token authorises access from within another pipeline, which is how multi-project pipelines consume builds from an upstream project without a personal access token.