GitLab CI Artifacts
Learn GitLab CI artifacts, how to define, use, and download build artifacts, pass files between jobs, set expiration times, and use artifacts for 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?