Open-source project
svenstaro/upload-release-action avatar
svenstaro/upload-release-action

upload-release-action: attaching binaries to a GitHub release

Upload files to a GitHub release

715 stars120 forksTypeScriptMIT

At a glance

What is it?
upload-release-action is an MIT licensed GitHub Action that uploads files to the release for a tag you have just pushed, with glob support, draft releases, prerelease promotion, cross-repository uploads and a duplicate check that can be turned off to save API calls. The judgement is that it is a small, well specified action whose input table is the documentation, and that the two arguments people usually miss are check_duplicates and release_id.
Who is it for?
upload-release-action fits a project that builds a binary per platform and publishes it from a tag workflow, because the action takes a path, names the asset and attaches it to the right release without any scripting in the workflow.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 159 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The action for a tag you have just pushed

upload-release-action is a GitHub Action for uploading files to a GitHub release, and the readme describes the job in one sentence, allowing you to select which files to upload to the just-tagged release. It runs on all the operating system types GitHub offers, which matters because a release job that only runs on Linux cannot attach a Windows binary.

The use case it serves is the one every compiled project hits. A workflow triggers on a tag, builds an artefact, and needs that artefact attached to the release that the tag created. Doing that with the gh command line means scripting the release lookup, the asset upload and the error handling in every repository that needs it, and this action is that script, packaged.

The project is MIT licensed, its primary language is TypeScript, and the author is Sven-Hendrik Haase. The release history shows steady maintenance, version 2.11.3 on 2025-11-20, 2.11.4 on 2026-02-25 and 2.11.5 on 2026-03-16, with the last push to the repository on 2026-05-18. The badge at the top points at an end-to-end test workflow, which is the right thing to see on an action that talks to the GitHub API.

The version to use in a workflow is the major tag, and every example in the readme uses svenstaro/upload-release-action@v2, so the action is versioned in the way actions are expected to be.

One required input, and the optional ones that bite

There is exactly one required input, file, the local file to be uploaded as the asset. Everything else has a default, and the defaults are chosen so that the common case needs one line of configuration.

The defaults that matter are the ones that change behaviour silently. The tag defaults to github.ref, and the refs/tags/ and refs/heads/ prefixes are stripped automatically, so the same value works for a tag and for a branch. The asset name defaults to the filename, and the $tag placeholder can be used to include the tag name in the asset, which is what you want when the same file name is uploaded for several releases.

Then there are the two that people get wrong. check_duplicates defaults to true and checks for existing assets with the same name, and the readme explains that disabling it removes that validity check and allows reduced GitHub API usage when there are a large number of files. So it is a trade between safety and API budget, and the action is explicit that the check costs requests.

And release_id is the one for uploading to a release that already exists. It is used for searching for the existing release instead of the tag, and the readme says it must be used if you are uploading files to an existing draft release. A draft has no tag, so the tag lookup cannot find it, which is the whole reason the input exists.

The rest are the release attributes: overwrite, promote for turning a prerelease into a release, draft to create the release without publishing it, prerelease, make_latest, release_name, target_commit for the commit the tag is based on, body for the release text, and repo_name for a release in a different repository.

Glob support replaces the per-file configuration

The file_glob option changes the shape of the release job, and the readme is explicit about the consequence.

If file_glob is set to true, the file argument can be a glob pattern, and the asset_name argument is ignored in that case. That is a bigger change than it sounds, because without globs you need one upload step per artefact, and each step needs its own asset name. With globs, the action finds everything that matches and names the assets itself.

The readme shows the three ways the action is used. The simple case, a single artefact with an explicit name:

yaml
    - name: Upload binaries to release
      uses: svenstaro/upload-release-action@v2
      with:
        repo_token: ${{ secrets.GITHUB_TOKEN }}
        file: target/release/mything
        asset_name: mything
        tag: ${{ github.ref }}
        overwrite: true
        body: "This is my release text"

The multi-platform case, which uses a build matrix over Ubuntu, Windows and macOS with a different artefact name and a different asset name per entry, and the glob case, where the file argument is a pattern such as the release directory with a wildcard and file_glob is set to true.

The body option in the simple example is worth noticing too. The release text can be set by the action rather than by a separate release-creation step, which is what makes the whole job a single upload step after the build.

Uploading into another repository, and getting the URL back

Two of the inputs are about a release that is not the one the workflow pushed, and they work together.

repo_name specifies the name of the GitHub repository in which the release will be created, edited and deleted. The readme gives a separate example for creating a release in a foreign repository with it. Combined with release_id, that covers the case where the release you want to attach to already exists, possibly as a draft, possibly in another repository, which is the case that the gh command line handles worst.

The action also returns two outputs, and the first is the one that closes the loop in a workflow. browser_download_url is the publicly available URL of the asset, and release_id is the numerical ID of the release that was created or updated. The second one matters because it is what you need for a later step, for instance to promote a draft or to attach another file to the same release without looking it up again.

That combination is what makes the action usable as a library inside a workflow rather than only as a terminal step. A job can upload one asset, take the release id from the output, and use it in a subsequent step, and a job that creates a draft in another repository can come back later, still in the same workflow, and attach the rest of the files to it.

TypeScript, ncc and a Jest run under a module flag

The build configuration tells you how the action is put together, and it is the standard modern action setup.

The package is written in TypeScript, the build script runs tsc, and the pack script runs ncc build, which is the bundler that produces the single JavaScript file a JavaScript action has to ship. The dist directory in the repository is that output, and it is committed, because GitHub runs the action from the repository rather than building it.

The test script is the interesting line, because it explains itself:

code
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js"

The package is an ES module, the type field is set to module, and Jest needs the experimental module flag to run ES modules under Node, so the flag is on the command line rather than in a configuration file.

The dependency list is short and says what the action does. The GitHub toolkit packages provide the core functions and the GitHub client, the Octokit core is the API client underneath, glob is the matcher for the glob mode, and @lifeomic/attempt is the retry helper, which is what you want when a release job uploads a hundred assets and one request fails.

The development tooling is careful. The eslint configuration is a flat config file, and the plugin list includes eslint-plugin-github, which applies the rules for GitHub Actions code specifically, so the action is linted against the conventions of the platform it runs on. There is a separate prettier configuration, and the all script runs the build, the formatter, the lint, the pack and the tests in that order, which is exactly the sequence a release of the action needs.

The parts of the input table that change the shape of a release

It is worth pulling the input table apart into the categories it actually contains, because they are not all the same kind of setting.

There is one input about what to upload, file, and one about how to name it, asset_name. Then there are the inputs about where to put it, tag, release_id, repo_name and target_commit, which is the commit hash or branch the tag is based on and defaults to the default branch, usually main.

Then there are the inputs about the state of the release itself. draft sets the release as a draft instead of publishing it, which allows edits before it goes out. prerelease marks it as a pre-release. promote does the opposite, taking a prerelease that already exists and promoting it to a release. make_latest, defaulting to true, marks the release as the latest one for the repository, which is what determines what a user sees when they open the releases tab.

And two behavioural switches that are easy to miss because they are not about the release at all. overwrite decides whether an asset with the same name is replaced, and check_duplicates decides whether the action looks for one first.

For a team that publishes continuously, promote and draft are the pair that matter, because they let a release be assembled in pieces and published once at the end, and release_id is what makes the second piece possible. The default of make_latest being true is also worth knowing, because in a repository with several release tracks, the newest upload can quietly become the one users are offered.

Editorial conclusion

upload-release-action fits a project that builds a binary per platform and publishes it from a tag workflow, because the action takes a path, names the asset and attaches it to the right release without any scripting in the workflow. It is the wrong choice if you need to upload to a release that already exists as a draft, because then you have to know the numeric release id and pass it explicitly, and it is the wrong choice for a job with a few hundred assets on a repository with tight API limits, because the duplicate check costs a request per file unless you turn it off and accept that a name clash is no longer caught. Before you adopt it, decide whether you want the glob form, since the glob and asset name options exclude each other, and read the release id behaviour, because the difference between uploading to a tag and uploading to a draft is the one that catches people out.

Frequently asked questions

What does the upload-release-action GitHub Action do?

It uploads files to the release for a tag, so a workflow that builds an artefact can attach it to the release the tag created. It runs on all the operating systems GitHub offers, so one workflow can attach a Linux, a Windows and a macOS build to the same release.

Which inputs are required?

Only file, the local file to be uploaded as the asset. The tag defaults to github.ref with the refs/tags/ and refs/heads/ prefixes stripped, the asset name defaults to the filename, and the token defaults to github.token.

How do I upload several files at once?

Set file_glob to true and make the file argument a glob pattern. The asset name argument is ignored in that mode, so the action names the assets itself rather than taking one name per file.

How do I attach files to a release that is already a draft?

Use the release_id input, which is used for searching for the existing release instead of the tag and must be used when uploading to an existing draft, because a draft has no tag for the lookup to find.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. svenstaro/upload-release-action on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/svenstaro-upload-release-action.svg)](https://hysenlabs.com/projects/svenstaro-upload-release-action)

Was this useful?