Skip to content
SRE & DevOpsDeep Dive Published Updated 9 min readViews unavailable

GitHub Actions Artifacts: Reliable Handoffs, Matrix Jobs, and Retention

Design GitHub Actions artifact handoffs with immutable names, matrix-safe downloads, retention controls, cache boundaries, and reliable missing-file failures.

GitHub Actions artifacts provide a run-scoped handoff for files a job creates: compiled packages, test reports, screenshots, coverage output, logs, and other evidence that another job or a person needs later. A reliable workflow treats the artifact as an explicit pipeline interface. The producer names and publishes a defined set of files; consumers wait for the right producer, download the expected artifact, and fail clearly if the expected content is missing.

Artifacts are not the same as dependency caches. A cache is an optimization for data that can be downloaded or rebuilt when a cache entry is absent. An artifact is a result of a workflow run that should be inspected, passed to a dependent job, or retained after the run. Use an artifact for a build output that a later test job must consume; use a cache for package-manager downloads that make a future install faster. GitHub’s workflow artifact documentation explicitly treats these as different mechanisms.

Make the artifact boundary intentional

An artifact should represent a useful boundary in the pipeline, not an accidental archive of the runner’s workspace. Decide which job produces it, which downstream jobs need it, which files belong in it, and how long those files must be available. Good boundaries include one platform’s build output, a named test suite’s report bundle, or a release package produced after validation.

Name artifacts by their purpose and, when a matrix produces several variants, by the dimensions that distinguish them. A name such as test-results-linux-x64 tells the consumer more than a generic output. Avoid putting every job’s files into a shared name and hoping concurrent uploads merge together. With the current artifact actions, each uploaded artifact is immutable after creation. A second job uploading to the same name in the same run will fail rather than append files to the first artifact.

For example, assume a Node.js repository has a committed .nvmrc, a package-lock.json, and a build script that writes its package into dist/. A build matrix should create distinct artifacts and a downstream verification job should wait for the complete matrix:

name: Build and verify

on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    name: Build ${{ matrix.os }} / ${{ matrix.arch }}
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            arch: linux-x64
          - os: windows-latest
            arch: windows-x64
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v7
        with:
          node-version-file: .nvmrc
      - name: Install locked dependencies
        run: npm ci
      - name: Build package
        run: npm run build
      - name: Upload package
        uses: actions/upload-artifact@v7
        with:
          name: package-${{ matrix.arch }}
          path: dist/**
          if-no-files-found: error
          retention-days: 14

  verify:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download platform packages
        uses: actions/download-artifact@v5
        with:
          pattern: package-*
          path: incoming
      - name: Inspect packages
        run: find incoming -maxdepth 3 -type f -print

The example uses the currently documented GitHub-hosted action majors (upload-artifact v7 and download-artifact v5). Check the action’s maintained documentation and your GitHub product’s support policy before adopting a major version; in particular, the upload action documents that v4 and later are not supported on GitHub Enterprise Server. Do not copy a GitHub.com version into GHES without checking the server-compatible release.

needs: build is important here. A downstream job should not race the producer matrix or download an incomplete set of expected outputs. By default, a job with a failed dependency is skipped; decide explicitly whether the consumer should run after partial failure to gather diagnostics, and use if: always() only when the step can safely handle missing artifacts. A report collector can often run after test failures, while a release packager should normally require all build and test dependencies to succeed.

Account for immutability and parallel producers

The current artifact model makes one upload a stable object for the rest of its lifetime. That is helpful for repeatability, but it changes how parallel jobs must be designed. If a matrix has Linux, macOS, and Windows workers, each worker needs its own artifact name, for example package-linux, package-macos, and package-windows. The consumer can download all names or select them with a pattern. It should not assume that several matrix jobs can append their outputs to a shared archive.

When a later job needs one combined directory, download the distinct artifacts and then assemble or merge them deliberately. With merge-multiple: true, files are extracted into one destination; if separate artifacts contain the same relative filename, the last writer wins. Preserve unique filenames or separate directories when same-named files carry different platform content. For release bundles, an explicit packaging step that checks the expected matrix members is safer than letting a filename collision silently choose a winner.

The overwrite input is not an append operation. It deletes an existing artifact with the same name and creates a new artifact, which receives a different ID. Use it only when the workflow intentionally replaces a previous upload. It is not a good way for independent concurrent jobs to coordinate: two runs or jobs can race, and the later upload can erase a result another consumer expected. Prefer a distinct name based on the producer and matrix dimensions, and keep artifacts from separate workflow runs separate.

Define paths and missing-file behavior

The path input accepts files, directories, wildcard patterns, and exclusions. A wildcard can change the archive’s directory layout: the portion before the first wildcard is removed from the stored hierarchy. When several search paths are supplied, the least common ancestor of the included paths becomes the archive root. Those rules matter when a later job expects a particular file path after download.

If downstream automation expects a build package or report, set if-no-files-found: error. The default warning can let a pipeline look successful even though the producer generated no matching files. Use warn when an output is genuinely optional and ignore only when silence is intentional. Test the upload and download pair using the actual build directory so glob expansion and extracted paths are verified, rather than inferred from the YAML.

Hidden files are excluded by default in current upload-artifact behavior. If a legitimate build output requires a hidden file, enable include-hidden-files narrowly and inspect the exact path set before upload. Do not enable it on a broad workspace glob without reviewing what the job may place in hidden directories. For most build and test outputs, an explicit dist/, reports/, or test-results/ path is easier to audit and less likely to collect unrelated runner files.

Retention is part of the output contract

Artifacts expire according to the retention policy that applies to the repository, organization, and enterprise. A workflow may request retention-days, but the value cannot exceed the governing limit. The current upload action documents a minimum of one day and a default based on repository settings; effective limits depend on GitHub product and repository policy. Set short retention for bulky intermediate output and choose a longer period for release evidence only when policy allows it. If a compliance or customer process needs a durable archive, export the required output to an approved long-term system rather than treating workflow retention as permanent storage.

Remember that the artifact link is tied to the artifact, run, and repository remaining available. A retention change applies to newly created objects and does not extend existing artifacts retroactively. A job that downloads from another workflow run or repository needs an explicit run identifier and appropriate token; the simple same-run handoff shown above avoids that cross-run lookup entirely.

The upload action exposes the artifact identifier, URL, and SHA-256 digest. The download action validates a downloaded artifact’s digest against the value associated with the upload and reports a warning if they differ. Preserve the run URL and digest in release or test records when they are useful for tracing which exact output was checked. This integrity check verifies the artifact transfer; it does not replace application-level validation of the package contents or prove that the build itself was correct.

Test the producer-consumer contract

Treat the artifact name and extracted directory structure as an interface between jobs. A change from dist/app.tar to out/release/app.tar, or a change from a single artifact to a matrix of named artifacts, can break every consumer even when the build itself passes. Include a smoke test that downloads the artifact and verifies expected files, sizes, and metadata before promotion. For a multi-platform build, assert that every required platform artifact exists before packaging.

The workflow should also cover realistic failure cases:

  • Run a build that intentionally produces no output and confirm that if-no-files-found: error fails the producer.
  • Verify that a test consumer waits for every required matrix job and that a failed build cannot be packaged as a release.
  • Exercise the download glob with the actual artifact names and confirm that platform files do not overwrite one another.
  • Verify that a diagnostic job can gather whatever reports are available after test failures without masking the failed test result.
  • Check the repository’s effective retention policy and confirm that the chosen duration is accepted.
  • Confirm that the downloaded archive contains the intended directory hierarchy and no unrelated workspace files.

Avoid transporting large, regenerable dependency trees as artifacts unless there is a specific reason. They consume artifact storage and produce a handoff that is harder to reproduce than resolving the dependencies from a lock file and cache. Conversely, do not rely on a cache as the only copy of a release package or diagnostic report: a cache is allowed to be evicted and is designed to be reconstructible.

Operational checklist

  • Give each artifact a stable purpose-based name; include matrix dimensions for parallel producers.
  • Use needs to express producer-consumer order and define behavior for failed producers.
  • Use exact paths and if-no-files-found: error for required deliverables.
  • Keep retention within the effective repository or organization limit and match it to the output’s purpose.
  • Download and validate the artifact in a consumer job before treating the handoff as complete.
  • Use a deliberate merge or packaging job when multiple artifacts must become one output; guard against path collisions.
  • Use caches for reconstructible performance inputs and artifacts for workflow outputs that must be handed off or retained.
  • Verify the action version supports the GitHub product in use, especially for GitHub Enterprise Server.

Workflow artifacts make multi-job builds observable and composable when their names, paths, retention, and consumers are designed as part of the pipeline contract. The reliable pattern is simple: publish one deliberate output per producer, make dependencies explicit, download it into a predictable location, and verify it before the next stage relies on it.

Related:

Sources:

Comments