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

> actions/upload-artifact made artifacts immutable in v4. Get upload v7 and download v8 inputs, merge-multiple, retention and cross-run downloads.

Source: https://latchkey.dev/learn/actions/upload-artifact  
Updated: 2026-11-12

`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.

## 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
```

| Input | Default | What it does |
| --- | --- | --- |
| `name` | `artifact` | Artifact name; ignored when `archive` is `false` |
| `path` | Required | File, directory or glob; several lines, and `!` lines to exclude |
| `if-no-files-found` | `warn` | `warn`, `error` or `ignore` |
| `retention-days` | `0`, the repository default | 1 to 90, unless the repository setting allows more |
| `compression-level` | `6` | zlib level 0 to 9; 0 for data that does not compress |
| `overwrite` | `false` | Delete an artifact of the same name first, instead of failing |
| `include-hidden-files` | `false` | Include files and folders whose names start with `.` |
| `archive` | `true` | New in v7: `false` uploads one file as-is, named after the file |

> From actions/upload-artifact action.yml at tag v7.0.2 (commit cf430e030ddbb5b0abf93d22962f4752f3646cd9), read 2026-11-12.

## 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.

| Input | Default | What it does |
| --- | --- | --- |
| `name` | None: all artifacts | One artifact by name |
| `artifact-ids` | None | Comma-separated IDs; use instead of `name`, not with it |
| `path` | `$GITHUB_WORKSPACE` | Destination; supports `~` |
| `pattern` | None | Glob over artifact names; ignored when `name` is set |
| `merge-multiple` | `false` | `true` extracts every match into the same directory |
| `github-token` | None | Required 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-decompress` | `false` | New in v8: keep a zipped artifact zipped |
| `digest-mismatch` | `error` | New in v8: `ignore`, `info`, `warn` or `error` |

> From actions/download-artifact action.yml at tag v8.0.2 (commit 9000827ccba6bdab643e8b6fd33ac0654aef8333) and the v5.0.0 and v8.0.0 release notes, read 2026-11-12.

## 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-*
```

> The 409 text is quoted from actions/upload-artifact issue 478. The v4 changes are from the GitHub changelog of 2023-12-14; the merge sub-action inputs from merge/action.yml at v7.0.2. Read 2026-11-12.

## 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.

| Release | Date | What changed |
| --- | --- | --- |
| download v4.1.0 | 2023-12-18 | `pattern` and `merge-multiple` inputs |
| upload v4.3.0 | 2024-01-23 | `actions/upload-artifact/merge` sub-action |
| upload v4.4.0 | 2024-08-30 | Hidden files excluded by default; `include-hidden-files` added |
| download v5.0.0 | 2025-08-05 | A single artifact downloaded by ID extracts into `path`, as by name |
| upload v5 and download v6 | 2025-10-24 | Node 24 support, still running on node20 by default |
| upload v6 and download v7 | 2025-12-12 | Runs on node24; needs runner 2.327.1 or later |
| upload v7.0.0 and download v8.0.0 | 2026-02-26 | `archive: 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.2 | 2026-10-07 | Retries on HTTP 429, honoring `Retry-After` |

> From the actions/upload-artifact and actions/download-artifact release notes, read 2026-11-12.

## 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](/learn/github-actions/gha-artifact-retention-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](/learn/github-actions/gha-artifact-download-from-other-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 see | Where to go |
| --- | --- |
| `No files were found with the provided path: <path>. No artifacts will be uploaded.`, as a warning or an error | [No files were found with the provided path](/learn/github-actions/upload-artifact-no-files-were-found-v4) |
| `The least common ancestor is <dir>. This will be the root directory of the artifact`, then files at the wrong depth | [The least common ancestor decides your paths](/learn/github-actions/gha-artifact-path-glob-empty) |
| `(409) Conflict: an artifact with this name already exists on the workflow run` | [An artifact with this name already exists](/learn/github-actions/gha-artifact-name-already-exists) |
| A file count, then `Attempt 1 of 5 failed with error`, or a failure with no retries | [Artifact upload failed after the files were found](/learn/github-actions/gha-artifact-upload-failed-v4) |
| `Unable to download artifact(s): Artifact not found for name: <name>` | [Artifact not found for name](/learn/github-actions/github-actions-failed-to-download-artifact-not-found) |
| `Unable to find any artifacts for the associated workflow`, from download v3 or older | [Unable to find any artifacts](/learn/github-actions/github-actions-download-artifact-unable-to-find) |
| `Unable to download artifact(s): Resource not accessible by integration` | [Download from another workflow run](/learn/github-actions/gha-artifact-download-from-other-run) |
| `Unable to download artifact(s): Artifact has expired` | [Artifact has expired](/learn/github-actions/gha-artifact-retention-expired) |

## FAQ

### 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

- [actions/upload-artifact action.yml at v7.0.2 (read 2026-11-12)](https://github.com/actions/upload-artifact/blob/v7.0.2/action.yml)
- [actions/download-artifact action.yml at v8.0.2 (read 2026-11-12)](https://github.com/actions/download-artifact/blob/v8.0.2/action.yml)
- [GitHub changelog: Artifacts v4 is now generally available](https://github.blog/changelog/2023-12-14-github-actions-artifacts-v4-is-now-generally-available/)
- [actions/upload-artifact migration guide](https://github.com/actions/upload-artifact/blob/v7.0.2/docs/MIGRATION.md)
- [GitHub Docs: configuring artifact and log retention](https://docs.github.com/en/organizations/managing-organization-settings/configuring-the-retention-period-for-github-actions-artifacts-and-logs-in-your-organization)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
