Stage 5 · Platform
CI/CD Foundations
Artifacts and Workspaces
Passing build outputs, test reports, coverage files, and Docker image metadata between jobs.
Why Artifacts Matter
In a pipeline, each job runs in an isolated environment. The build job produces compiled code, the test job needs that code to run tests, and the deploy job needs the final artifact to push to production. Artifacts are how you pass data between these isolated jobs.
Without artifacts, you would rebuild the code in every job — wasting time and introducing inconsistency. The artifact built in the build job should be the exact same artifact deployed to production. This is the principle of immutable artifacts.
Build once, use everywhere. The artifact created in the build stage should be the same binary, container image, or bundle that gets tested and deployed. Never rebuild for different environments — configure instead.
GitHub Actions Artifacts
GitHub Actions provides upload-artifact and download-artifact actions to pass files between jobs. Artifacts are stored on GitHub for a configurable retention period and can be downloaded from the workflow UI.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- name: Upload build output
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: test-results/
test:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download build output
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Run tests against built artifact
run: npm test -- --output coverage/
- name: Upload coverage
uses: actions/upload-artifact@v4
if: always()
with:
name: coverage
path: coverage/The build job uploads the dist/ directory as an artifact. The test job downloads it and runs tests against the pre-built output. Using if: always() ensures test results are uploaded even when tests fail.
GitLab CI Artifacts
GitLab CI uses the artifacts keyword to define which files a job produces. Downstream jobs can access these artifacts through the needs keyword. Artifacts are stored on the GitLab instance and can be downloaded from the pipeline page.
build:
stage: build
script:
- npm ci && npm run build
artifacts:
paths:
- dist/
- package.json
expire_in: 1 week
test:unit:
stage: test
needs: [build]
script:
- npm test -- --unit --coverage
artifacts:
when: always
reports:
junit: test-results/junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura.xml
deploy:
stage: deploy
needs: [build]
script:
- ls dist/
- ./deploy.shGitLab CI artifacts can include test reports in specific formats (JUnit, Cobertura) that GitLab renders in the merge request UI. The reports keyword makes these reports visible without additional tooling.
Workspaces in GitHub Actions
Workspaces provide a shared filesystem for all jobs in a workflow. Unlike artifacts, workspaces are ephemeral and exist only during the workflow run. They are faster than artifacts for passing data between jobs on the same runner, but they do not persist after the run completes.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: dist
- run: npm test
report:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: dist
- run: ./generate-report.sh dist/For simple cases, upload-artifact and download-artifact work well. For performance-critical pipelines, consider using actions/cache to store build outputs in the GitHub Actions cache instead.
Passing Test Reports
Test reports and coverage data are critical artifacts. They inform merge request decisions and track quality over time. Both GitHub and GitLab have built-in support for test report formats like JUnit XML and Cobertura.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test -- --reporter=junit --output=test-results/
- name: Publish test results
uses: dorny/test-reporter@v1
if: always()
with:
name: Jest Results
path: test-results/*.xml
reporter: java-junit
- name: Publish coverage
uses: actions/github-script@v7
if: always()
with:
script: |
const coverage = require('./coverage/coverage-summary.json');
const pct = coverage.total.statements.pct;
core.summary.addHeading('Coverage Report');
core.summary.addTable([
[{data: 'Metric', header: true}, {data: 'Coverage', header: true}],
['Statements', pct + '%'],
]);
await core.summary.write();The test-reporter action parses JUnit XML and displays results in the GitHub Actions UI. Coverage data can be posted as a job summary or as a check annotation on the pull request.
Best Practices
- Upload artifacts with if: always() so they are available even when a job fails — this is essential for debugging failed builds.
- Set retention periods appropriately — keep release artifacts longer than intermediate build outputs.
- Use artifacts/needs instead of caching for passing data between jobs — caches are for dependencies, artifacts are for build outputs.
- Compress large artifacts to reduce upload/download time and storage costs.
- Include metadata (git sha, version, build number) alongside artifacts for traceability.
GitHub supports artifact attestations that cryptographically link an artifact to its source commit and build process. This prevents tampering and provides verifiable provenance for your release artifacts.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.