# softprops/action-gh-release: tags, asset globs that match nothing, and reruns

> softprops/action-gh-release publishes even when a glob matches nothing. Get tag triggers, file globs, drafts, release notes and safe reruns.

Source: https://latchkey.dev/learn/actions/softprops-action-gh-release  
Updated: 2026-11-12

softprops/action-gh-release creates a GitHub release for a tag, or updates the one that already exists, and uploads every file your `files` globs match; it needs `permissions: contents: write` and a tag, either from a tag push or from the `tag_name` input. v3 moved it to the Node 24 runtime with no input changes, and v2.6.2, the last v2 release, is no longer maintained.

You push `v1.4.0`, a workflow builds the binaries, and you want a release page with notes and the archives attached, without clicking through the GitHub UI. softprops/action-gh-release is the action most workflows use for that last step, and most of what goes wrong with it comes from defaults: a glob that matched nothing and did not fail, a release body that was overwritten on rerun, a release created by `GITHUB_TOKEN` that never triggered the workflow listening for it.

A rerun is the case to design for. When the release for a tag already exists, the action updates it instead of creating a second one, which makes a retry safe, but a rerun with `body` set rewrites notes someone edited by hand. The one error with its own page is the matrix race, [softprops/action-gh-release already_exists](/learn/github-actions/softprops-action-gh-release-validation-failed-already-exists).

## A workflow that runs on version tags

The usual shape is a workflow that only runs on version tags, builds, and hands the output to the action. With no `tag_name` input the action uses `github.ref_name`, so the tag filter on the trigger is what supplies the tag. If the run is not on a tag and you set neither `tag_name` nor `draft`, the action stops with `⚠️ GitHub Releases requires a tag`, the string thrown in `src/run.ts`.

Set `fail_on_unmatched_files: true` on any workflow that ships artifacts. Without it a pattern that matches nothing only prints a warning, and the release publishes without the files your users came for.

```.github/workflows/release.yml
name: release
on:
  push:
    tags: ['v*.*.*']
permissions:
  contents: write
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - run: ./scripts/build-release.sh   # writes dist/*.tar.gz and dist/SHA256SUMS
      - uses: softprops/action-gh-release@8a0cd651e5b2365bd640854a3fa5dfc70e4875f3 # v3.0.4
        with:
          files: |
            dist/*.tar.gz
            dist/SHA256SUMS
          fail_on_unmatched_files: true
          generate_release_notes: true
          prerelease: ${{ contains(github.ref_name, '-') }}
```

> SHA for v3.0.4 from `git ls-remote https://github.com/softprops/action-gh-release refs/tags/v3.0.4^{}`, read 2026-11-12.

## Nineteen inputs, all optional

All inputs are optional. Booleans arrive as strings, so `true` and `false` are the only values worth writing.

| Input | Default | What it does |
| --- | --- | --- |
| `tag_name` | `github.ref_name` | The tag to release; `refs/tags/<name>` is normalized to `<name>` |
| `name` | the tag name | Release title |
| `body` | none | Release notes text |
| `body_path` | none | File to read the notes from; tried first, falls back to `body` |
| `append_body` | `false` | Append to an existing release body instead of replacing it |
| `generate_release_notes` | `false` | Ask GitHub to generate the notes; `body` is prepended to them |
| `previous_tag` | none | Comparison base for generated notes |
| `draft` | `false` | Keep the release a draft |
| `prerelease` | `false` | Mark the release as a prerelease |
| `make_latest` | GitHub API default | `true`, `false` or `legacy`; drafts and prereleases cannot be latest |
| `files` | none | Newline-delimited globs of assets to upload |
| `working_directory` | workspace root | Base directory the `files` globs resolve against |
| `fail_on_unmatched_files` | `false` | Fail if any glob matches nothing |
| `overwrite_files` | `true` | Replace an existing asset with the same name |
| `preserve_order` | `false` | Upload sequentially in the order given |
| `target_commitish` | default branch | Branch or SHA to create a new tag from |
| `repository` | current repository | Release in another `<owner>/<repo>` |
| `token` | `github.token` | Token for the API calls; a non-empty value overrides `GITHUB_TOKEN` |
| `discussion_category_name` | none | Create a linked discussion in this existing category |

> Outputs are `url`, `id`, `upload_url` and `assets`, a JSON array of the assets uploaded or overwritten in this run. Inputs and defaults from action.yml and README.md at tag v3.0.4, read 2026-11-12.

## files: globs that match nothing do not fail by default

`files` is one string with a pattern per line, which is why it is almost always written as a YAML block with `|`. Patterns resolve from the workspace root unless you set `working_directory`, `~/` expands to the runner home directory, and literal `[` or `]` in a filename must be escaped because the input is a glob.

A pattern that matches nothing logs `🤔 Pattern 'dist/*.zip' does not match any files.` and the run continues. With `fail_on_unmatched_files: true` the same condition throws instead. When an asset with the same name is already on the release, the default `overwrite_files: true` deletes it and uploads the new file, logging `♻️ Deleting previously uploaded asset`; with `false` the existing file is kept and the new one skipped.

```.github/workflows/release.yml
- uses: softprops/action-gh-release@8a0cd651e5b2365bd640854a3fa5dfc70e4875f3 # v3.0.4
  with:
    working_directory: dist
    files: |
      *.tar.gz
      checksums/*.txt
    fail_on_unmatched_files: true
    overwrite_files: false
```

## Drafts, prereleases and make_latest

Since v2.5.0 a new full release is created as a draft, the assets are uploaded, and only then is the draft published. A reader never sees a published release with half its files, and on a repository with immutable releases the order matters, because GitHub locks assets once a release is published.

Prereleases are the exception. They are created already published, so workflows listening for `release: types: [prereleased]` still fire. On an immutable-release repository that means uploads to a prerelease fail, and the action's own error tells you the fix: set `draft: true`, publish the draft later, and have downstream workflows listen for `release.published`. Setting `draft: true` on any release keeps it a draft; leaving it unset on an existing draft publishes that draft after the upload.

`make_latest` passes straight to the GitHub API, which defaults it to `true` for a newly published release. Set it to `false` when you publish a patch for an older line, such as `v1.9.3` after `v2.0.0`, so the repository's Latest badge stays on the newest major.

## Release notes: generated, from a file, or both

`generate_release_notes: true` passes the REST API flag of the same name, so GitHub writes the release name and body from the changes since the previous release. If you also set `body`, it is prepended. When GitHub picks the wrong comparison base, for example because you tag staging and production releases in one repository, `previous_tag` pins it.

`body_path` reads the notes from a file your workflow wrote, which is how you plug in any changelog generator. If the file cannot be read the action logs a warning and falls back to `body` rather than failing.

```.github/workflows/deploy.yml
- uses: softprops/action-gh-release@8a0cd651e5b2365bd640854a3fa5dfc70e4875f3 # v3.0.4
  with:
    tag_name: prod-2026-11-12
    target_commitish: ${{ github.sha }}
    previous_tag: prod-2026-10-29
    generate_release_notes: true
    body: 'Deployed to production from ${{ github.sha }}.'
```

## Updating a release that already exists

The action looks the release up by tag first. If it finds one, it updates it instead of creating a second: the name stays unless you set `name`, the body is replaced by whatever your workflow supplies unless `append_body: true`, `prerelease` keeps its old value unless you set it, and the draft state is kept. Assets are then uploaded with the overwrite rule above. That makes a rerun of a release job safe, with one trap: a rerun with `body` set rewrites notes someone edited by hand on the release page.

Two jobs that publish the same tag at the same moment are a different story. Both find no release, both try to create one, and one loses with a 422. That race, and the fix of creating the release once and letting a matrix attach to it, is on the [already_exists page](/learn/github-actions/softprops-action-gh-release-validation-failed-already-exists).

## Permissions and the token

The action needs `contents: write`, plus `discussions: write` if you set `discussion_category_name`. A `403 Resource not accessible by integration` usually means the job's token is read-only. The README also names one case where `contents: write` is not enough: creating a new tag on an older commit through `target_commitish` can be refused for `github.token`, and a PAT or another token with enough contents permission is the fix.

The other token question is downstream. A release created with `GITHUB_TOKEN` does not start workflows that listen for `release` events, because GitHub does not create new workflow runs from events that token causes, except `workflow_dispatch` and `repository_dispatch`. If a separate workflow publishes on `release: types: [published]`, create the release with a GitHub App installation token or a PAT, or do the publishing in the same workflow.

```.github/workflows/release.yml
permissions:
  contents: write
  discussions: write   # only with discussion_category_name
steps:
  - uses: softprops/action-gh-release@8a0cd651e5b2365bd640854a3fa5dfc70e4875f3 # v3.0.4
    with:
      token: ${{ secrets.RELEASE_TOKEN }}   # so release: published workflows fire
      discussion_category_name: Announcements
```

## v2.5 drafts until uploaded, v3 moves to Node 24

Pin to a full SHA and let Dependabot move it; the floating `v3` tag moves with every patch.

| Release | Change |
| --- | --- |
| v2.0.0 | Runtime moved to Node 20 |
| v2.5.0 | New releases stay a draft until every asset is uploaded |
| v2.6.0 | `previous_tag` for generated notes |
| v2.6.2 | Final v2 release, no longer maintained |
| v3.0.0 | Runtime moved to Node 24; no input changes |
| v3.0.2 | An existing draft is reused when publishing it as a prerelease |

> From CHANGELOG.md at tag v3.0.4, read 2026-11-12.

## FAQ

### Why does the action say GitHub Releases requires a tag?

Because the run was not triggered by a tag and no `tag_name` was given, so there is nothing to attach a release to. Add a tag filter to the `push` trigger, gate the step with `if: github.ref_type == 'tag'`, or pass `tag_name` explicitly. Setting `draft: true` also lets the action proceed without one.

### Why does it say Pattern does not match any files?

The glob resolved to nothing from the directory the action used, which is the workspace root unless `working_directory` is set. Check the path the build actually wrote, remember that `files` needs one pattern per line in a `|` block, and turn on `fail_on_unmatched_files` so the next miss fails the job instead of publishing an empty release.

### Why is the release asset upload failing with Resource not accessible by integration?

The token the step used cannot write to the repository. Add `permissions: contents: write` to the job or workflow, and check that an organization or repository setting has not made the default token read-only. Releasing into another repository always needs a PAT or App token, because `GITHUB_TOKEN` is scoped to the repository the workflow runs in.

### Does softprops/action-gh-release run on Node 24?

v3 does: v3.0.0 moved the runtime from Node 20 to Node 24 with no input changes. v2.6.2 was the last Node 20 release and is no longer maintained, so moving a pinned v2 SHA to v3.0.4 is a drop-in change.

## References

- [softprops/action-gh-release action.yml at v3.0.4 (read 2026-11-12)](https://github.com/softprops/action-gh-release/blob/v3.0.4/action.yml)
- [softprops/action-gh-release README at v3.0.4](https://github.com/softprops/action-gh-release/blob/v3.0.4/README.md)
- [softprops/action-gh-release CHANGELOG at v3.0.4](https://github.com/softprops/action-gh-release/blob/v3.0.4/CHANGELOG.md)
- [GitHub REST API: create a release (make_latest, generate_release_notes)](https://docs.github.com/en/rest/releases/releases#create-a-release)
- [GitHub Docs: triggering a workflow from a workflow](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow)

---

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
