Skip to content
Latchkey LogoLatchkey home

actions/upload-artifact and download-artifact after the v4 rewrite

actions/upload-artifact zips files from a job and stores them against the workflow run, and actions/download-artifact brings them back in a later job, run or repository. Since v4 an artifact is immutable and its name must be unique in the run; the current majors are upload v7 (v7.0.2), which can upload a single file without zipping it, and download v8 (v8.0.2), which fails on a digest mismatch by default.

Artifacts are how a build job hands its output to a test job, how a matrix hands ten coverage files to one report, and how a release workflow picks up what CI built. Most of the confusion around them dates from one release: v4 replaced the backend, and a pattern that had worked for years, several jobs writing to one artifact name, started failing.

Upload and download are versioned separately now, so their majors no longer line up: upload v7 pairs with download v8. They are covered side by side here, because one job writes an artifact and another reads it, and the name, path and run have to agree between the two.

Matrix legs uploading to one shared name and failing, beside per-leg names merged on download
The v3 pattern of one shared name fails from v4 on. Give each leg its own name and merge them on the way down.

upload-artifact inputs at v7.0.2

Three outputs come back: artifact-id, artifact-digest (a SHA-256 of the upload), and artifact-url, which only works for requests authenticated with GitHub. The default for if-no-files-found is warn, which lets a build with an empty glob stay green, so set it to error on anything a later job depends on.

.github/workflows/build.yml
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
  with:
    name: dist
    path: |
      dist/
      !dist/**/*.map
    if-no-files-found: error
    retention-days: 7
InputDefaultWhat it does
nameartifactArtifact name; ignored when archive is false
pathRequiredFile, directory or glob; several lines, and ! lines to exclude
if-no-files-foundwarnwarn, error or ignore
retention-days0, the repository default1 to 90, unless the repository setting allows more
compression-level6zlib level 0 to 9; 0 for data that does not compress
overwritefalseDelete an artifact of the same name first, instead of failing
include-hidden-filesfalseInclude files and folders whose names start with .
archivetrueNew in v7: false uploads one file as-is, named after the file

download-artifact inputs at v8.0.2

Ask for one artifact by name, several by artifact-ids or pattern, or none of them to get every artifact in the run, each extracted into its own directory under path. The one output is download-path. Two behaviors moved recently: since v5 a single artifact fetched by ID extracts straight into path, the same as one fetched by name, and since v8 a digest mismatch fails the step instead of logging a warning.

InputDefaultWhat it does
nameNone: all artifactsOne artifact by name
artifact-idsNoneComma-separated IDs; use instead of name, not with it
path$GITHUB_WORKSPACEDestination; supports ~
patternNoneGlob over artifact names; ignored when name is set
merge-multiplefalsetrue extracts every match into the same directory
github-tokenNoneRequired for another run or repository
repository${{ github.repository }}Source repository, used with github-token
run-id${{ github.run_id }}Source run, used with github-token
skip-decompressfalseNew in v8: keep a zipped artifact zipped
digest-mismatcherrorNew in v8: ignore, info, warn or error

The v4 rewrite: immutable artifacts and unique names

GitHub's changelog for the v4 release lists the changes that still define the actions: artifacts are scoped to a job and downloadable through the API as soon as the upload finishes, they are immutable, a job can upload at most 500, and v4 cannot read artifacts made by v3 or earlier, so upload and download move together. v4 and later are not supported on GitHub Enterprise Server, which stays on the v3 line.

Immutability is what breaks matrix workflows. A second upload under a name that already exists in the run fails with "Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run", and whichever leg finished first wins, so the failing leg changes from run to run. overwrite: true deletes the old artifact and uploads a new one with a new ID: last writer wins, not a merge.

The fix the changelog gives is to upload under one name per leg and merge on download with pattern and merge-multiple: true. When you need a single artifact a later run or a person can fetch, the actions/upload-artifact/merge sub-action combines matching artifacts into a new one, and can delete the pieces.

.github/workflows/test.yml
jobs:
  test:
    strategy:
      matrix:
        shard: [1, 2, 3]
    runs-on: ubuntu-latest
    steps:
      - run: ./run-tests --shard ${{ matrix.shard }} --coverage-out coverage/
      - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
          name: coverage-${{ matrix.shard }}
          path: coverage/
          if-no-files-found: error
  report:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
        with:
          pattern: coverage-*
          merge-multiple: true
          path: coverage/
      - uses: actions/upload-artifact/merge@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
          name: coverage-all
          pattern: coverage-*

What changed after v4

Most majors since v4 are runtime moves, and the two that change behavior are both about what is inside the zip. Upload and download are versioned separately, so their majors no longer line up: upload v7 pairs with download v8.

ReleaseDateWhat changed
download v4.1.02023-12-18pattern and merge-multiple inputs
upload v4.3.02024-01-23actions/upload-artifact/merge sub-action
upload v4.4.02024-08-30Hidden files excluded by default; include-hidden-files added
download v5.0.02025-08-05A single artifact downloaded by ID extracts into path, as by name
upload v5 and download v62025-10-24Node 24 support, still running on node20 by default
upload v6 and download v72025-12-12Runs on node24; needs runner 2.327.1 or later
upload v7.0.0 and download v8.0.02026-02-26archive: false single-file uploads; downloads skip unzipping non-zip files; skip-decompress; digest-mismatch defaults to error; ESM
upload v7.0.2 and download v8.0.22026-10-07Retries on HTTP 429, honoring Retry-After

Retention and expiry

Artifacts are kept for 90 days by default. retention-days shortens that per upload, between 1 and 90 unless the repository setting allows more. That setting lives at the repository, organization or enterprise level: 1 to 90 days for public repositories and 1 to 400 for private ones, and a change applies only to artifacts created after it.

An expired artifact does not vanish from the listing, which is why the failure is confusing: the lookup succeeds, the download is refused, and the step fails with a short message rather than the usual not-found text. Artifact has expired covers how to tell, and what to move out of artifacts when you need it for longer. Artifacts count toward your storage quota until they expire or are deleted, and the README notes that storage is recalculated every 6 to 12 hours, so deleting does not free quota at once.

Hidden files, permissions and single files

Since v4.4.0 a file or folder whose name starts with . is left out unless you set include-hidden-files: true. The change followed Unit 42's ArtiPACKED research, which found tokens inside public artifacts because workflows uploaded their checkout, .git/config included. If you turn hidden files on, exclude what you do not mean to ship with ! lines.

Zipping drops file modes: every directory comes back 755 and every file 644, so an executable you built is not executable after download. The documented fix is to tar the tree, which keeps modes, and upload the tarball with archive: false, so it is not zipped a second time. With archive: false the path must match exactly one file and the artifact takes the file's name, and download v8 recognizes it as not a zip and saves it as-is.

.github/workflows/build.yml
- run: tar -cf build.tar -C out .
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
  with:
    path: build.tar
    archive: false

# In the consuming job:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
  with:
    name: build.tar
- run: mkdir -p out && tar -xf build.tar -C out

Downloading from another run or repository

Without github-token, download-artifact searches the current run and nothing else; run-id and repository have defaults and are only read when a token is set. Pass a token that has actions: read on the source repository, the run ID, and the repository if it is a different one. Download from another workflow run goes through the permission errors this produces.

.github/workflows/deploy.yml
permissions:
  actions: read
  contents: read
steps:
  - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
    with:
      name: dist
      github-token: ${{ github.token }}
      run-id: ${{ github.event.workflow_run.id }}

Which error page matches your log

Each of these has its own page. Read the step log against the first column before you change anything: several of them look alike from the job summary.

What you seeWhere to go
No files were found with the provided path: <path>. No artifacts will be uploaded., as a warning or an errorNo files were found with the provided path
The least common ancestor is <dir>. This will be the root directory of the artifact, then files at the wrong depthThe least common ancestor decides your paths
(409) Conflict: an artifact with this name already exists on the workflow runAn artifact with this name already exists
A file count, then Attempt 1 of 5 failed with error, or a failure with no retriesArtifact upload failed after the files were found
Unable to download artifact(s): Artifact not found for name: <name>Artifact not found for name
Unable to find any artifacts for the associated workflow, from download v3 or olderUnable to find any artifacts
Unable to download artifact(s): Resource not accessible by integrationDownload from another workflow run
Unable to download artifact(s): Artifact has expiredArtifact has expired

Key takeaways

  • Since v4 an artifact is immutable and its name unique per run: one name per matrix leg, merged on download.
  • Set if-no-files-found: error on anything a later job needs; the default only warns.
  • Hidden files are excluded since v4.4.0, and zipping resets file modes; tar plus archive: false keeps them.
  • Cross-run downloads need github-token with actions: read; run-id alone changes nothing.

Frequently asked questions

Why can I no longer upload to the same artifact name from multiple jobs?
Because v4 made artifacts immutable, and a name can exist once per run. The second upload gets a 409 conflict rather than adding its files, as issue 478 reported four days after v4 shipped. Name each job's artifact differently and use pattern with merge-multiple: true to download them into one directory.
Why does upload-artifact not keep file permissions?
The zip it builds does not store them, so every file comes back 644 and every directory 755, the limitation tracked in issue 38. Archive with tar first and upload the tarball with archive: false on v7; extracting it restores the modes.
Why does upload-artifact double-zip a zip file?
Up to v6 every upload was zipped, so a .zip you built arrived inside another zip (issue 39). v7 added archive: false, which uploads one file exactly as it is, and download v8 saves a non-zip artifact without trying to extract it.
How do I download an artifact from a different workflow?
Give download-artifact a github-token with actions: read on the source repository, plus run-id and, for another repository, repository. Without the token the action only ever looks in the current run, whatever run-id says.

References

upload-artifact and download-artifact, unchanged, on Latchkey runners. Start free → 30-day trial · No credit card