├── .github
├── FUNDING.yml
├── dependabot.yml
├── release.yml
└── workflows
│ └── main.yml
├── .gitignore
├── .tool-versions
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── __tests__
├── github.test.ts
├── release.txt
└── util.test.ts
├── action.yml
├── demo.png
├── dist
└── index.js
├── jest.config.ts
├── package-lock.json
├── package.json
├── src
├── github.ts
├── main.ts
└── util.ts
├── tests
└── data
│ └── foo
│ └── bar.txt
└── tsconfig.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: softprops
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: weekly
7 | groups:
8 | npm:
9 | patterns:
10 | - "*"
11 | ignore:
12 | - dependency-name: node-fetch
13 | versions:
14 | - ">=3.0.0"
15 | # ignore mime and @types/mime per https://github.com/softprops/action-gh-release/pull/475
16 | - dependency-name: mime
17 | versions:
18 | - ">=4.0.0"
19 | - dependency-name: "@types/mime"
20 | versions:
21 | - ">=4.0.0"
22 | commit-message:
23 | prefix: "chore(deps)"
24 | - package-ecosystem: github-actions
25 | directory: "/"
26 | schedule:
27 | interval: weekly
28 | groups:
29 | github-actions:
30 | patterns:
31 | - "*"
32 | commit-message:
33 | prefix: "chore(deps)"
34 |
--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
1 | changelog:
2 | exclude:
3 | labels:
4 | - ignore-for-release
5 | - github-actions
6 | authors:
7 | - octocat
8 | - renovate[bot]
9 | categories:
10 | - title: Breaking Changes 🛠
11 | labels:
12 | - breaking-change
13 | - title: Exciting New Features 🎉
14 | labels:
15 | - enhancement
16 | - feature
17 | - title: Bug fixes 🐛
18 | labels:
19 | - bug
20 | - title: Other Changes 🔄
21 | labels:
22 | - "*"
23 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: main
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-24.04
10 | steps:
11 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
12 |
13 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
14 | with:
15 | node-version-file: ".tool-versions"
16 | cache: "npm"
17 |
18 | - name: Install
19 | run: npm ci
20 | - name: Build
21 | run: npm run build
22 | - name: Test
23 | run: npm run test
24 | - name: Format
25 | run: npm run fmtcheck
26 | # - name: "check for uncommitted changes"
27 | # # Ensure no changes, but ignore node_modules dir since dev/fresh ci deps installed.
28 | # run: |
29 | # git diff --exit-code --stat -- . ':!node_modules' \
30 | # || (echo "##[error] found changed files after build. please 'npm run build && npm run fmt'" \
31 | # "and check in all changes" \
32 | # && exit 1)
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __tests__/runner/*
2 | # actions requires a node_modules dir https://github.com/actions/toolkit/blob/master/docs/javascript-action.md#publish-a-releasesv1-action
3 | # but its recommended not to check these in https://github.com/actions/toolkit/blob/master/docs/action-versioning.md#recommendations
4 | node_modules
5 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 20.19.0
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 2.2.2
2 |
3 | ## What's Changed
4 |
5 | ### Bug fixes 🐛
6 |
7 | * fix: updating release draft status from true to false by @galargh in https://github.com/softprops/action-gh-release/pull/316
8 |
9 | ### Other Changes 🔄
10 |
11 | * chore: simplify ref_type test by @steinybot in https://github.com/softprops/action-gh-release/pull/598
12 | * fix(docs): clarify the default for tag_name by @muzimuzhi in https://github.com/softprops/action-gh-release/pull/599
13 | * test(release): add unit tests when searching for a release by @rwaskiewicz in https://github.com/softprops/action-gh-release/pull/603
14 | * dependency updates
15 |
16 | ## 2.2.1
17 |
18 | ## What's Changed
19 |
20 | ### Bug fixes 🐛
21 |
22 | * fix: big file uploads by @xen0n in https://github.com/softprops/action-gh-release/pull/562
23 |
24 | ### Other Changes 🔄
25 | * chore(deps): bump @types/node from 22.10.1 to 22.10.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/559
26 | * chore(deps): bump @types/node from 22.10.2 to 22.10.5 by @dependabot in https://github.com/softprops/action-gh-release/pull/569
27 | * chore: update error and warning messages for not matching files in files field by @ytimocin in https://github.com/softprops/action-gh-release/pull/568
28 |
29 | ## 2.2.0
30 |
31 | ## What's Changed
32 |
33 | ### Exciting New Features 🎉
34 |
35 | * feat: read the release assets asynchronously by @xen0n in https://github.com/softprops/action-gh-release/pull/552
36 |
37 | ### Bug fixes 🐛
38 |
39 | * fix(docs): clarify the default for tag_name by @alexeagle in https://github.com/softprops/action-gh-release/pull/544
40 |
41 | ### Other Changes 🔄
42 |
43 | * chore(deps): bump typescript from 5.6.3 to 5.7.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/548
44 | * chore(deps): bump @types/node from 22.9.0 to 22.9.4 by @dependabot in https://github.com/softprops/action-gh-release/pull/547
45 | * chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 by @dependabot in https://github.com/softprops/action-gh-release/pull/545
46 | * chore(deps): bump @vercel/ncc from 0.38.2 to 0.38.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/543
47 | * chore(deps): bump prettier from 3.3.3 to 3.4.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/550
48 | * chore(deps): bump @types/node from 22.9.4 to 22.10.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/551
49 | * chore(deps): bump prettier from 3.4.1 to 3.4.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/554
50 |
51 | ## 2.1.0
52 |
53 | ## What's Changed
54 |
55 | ### Exciting New Features 🎉
56 | * feat: add support for release assets with multiple spaces within the name by @dukhine in https://github.com/softprops/action-gh-release/pull/518
57 | * feat: preserve upload order by @richarddd in https://github.com/softprops/action-gh-release/pull/500
58 |
59 | ### Other Changes 🔄
60 | * chore(deps): bump @types/node from 22.8.2 to 22.8.7 by @dependabot in https://github.com/softprops/action-gh-release/pull/539
61 |
62 | ## 2.0.9
63 |
64 | - maintenance release with updated dependencies
65 |
66 | ## 2.0.8
67 |
68 | ### Other Changes 🔄
69 | * chore(deps): bump prettier from 2.8.0 to 3.3.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/480
70 | * chore(deps): bump @types/node from 20.14.9 to 20.14.11 by @dependabot in https://github.com/softprops/action-gh-release/pull/483
71 | * chore(deps): bump @octokit/plugin-throttling from 9.3.0 to 9.3.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/484
72 | * chore(deps): bump glob from 10.4.2 to 11.0.0 by @dependabot in https://github.com/softprops/action-gh-release/pull/477
73 | * refactor: write jest config in ts by @chenrui333 in https://github.com/softprops/action-gh-release/pull/485
74 | * chore(deps): bump @actions/github from 5.1.1 to 6.0.0 by @dependabot in https://github.com/softprops/action-gh-release/pull/470
75 |
76 | ## 2.0.7
77 |
78 | ### Bug fixes 🐛
79 |
80 | * Fix missing update release body by @FirelightFlagboy in https://github.com/softprops/action-gh-release/pull/365
81 |
82 | ### Other Changes 🔄
83 |
84 | * Bump @octokit/plugin-retry from 4.0.3 to 7.1.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/443
85 | * Bump typescript from 4.9.5 to 5.5.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/467
86 | * Bump @types/node from 20.14.6 to 20.14.8 by @dependabot in https://github.com/softprops/action-gh-release/pull/469
87 | * Bump @types/node from 20.14.8 to 20.14.9 by @dependabot in https://github.com/softprops/action-gh-release/pull/473
88 | * Bump typescript from 5.5.2 to 5.5.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/472
89 | * Bump ts-jest from 29.1.5 to 29.2.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/479
90 | * docs: document that existing releases are updated by @jvanbruegge in https://github.com/softprops/action-gh-release/pull/474
91 |
92 | ## 2.0.6
93 |
94 | - maintenance release with updated dependencies
95 |
96 | ## 2.0.5
97 |
98 | - Factor in file names with spaces when upserting files [#446](https://github.com/softprops/action-gh-release/pull/446) via [@MystiPanda](https://github.com/MystiPanda)
99 | - Improvements to error handling [#449](https://github.com/softprops/action-gh-release/pull/449) via [@till](https://github.com/till)
100 |
101 | ## 2.0.4
102 |
103 | - Minor follow up to [#417](https://github.com/softprops/action-gh-release/pull/417). [#425](https://github.com/softprops/action-gh-release/pull/425)
104 |
105 | ## 2.0.3
106 |
107 | - Declare `make_latest` as an input field in `action.yml` [#419](https://github.com/softprops/action-gh-release/pull/419)
108 |
109 | ## 2.0.2
110 |
111 | - Revisit approach to [#384](https://github.com/softprops/action-gh-release/pull/384) making unresolved pattern failures opt-in [#417](https://github.com/softprops/action-gh-release/pull/417)
112 |
113 | ## 2.0.1
114 |
115 | - Add support for make_latest property [#304](https://github.com/softprops/action-gh-release/pull/304) via [@samueljseay](https://github.com/samueljseay)
116 | - Fail run if files setting contains invalid patterns [#384](https://github.com/softprops/action-gh-release/pull/384) via [@rpdelaney](https://github.com/rpdelaney)
117 | - Add support for proxy env variables (don't use node-fetch) [#386](https://github.com/softprops/action-gh-release/pull/386/) via [@timor-raiman](https://github.com/timor-raiman)
118 | - Suppress confusing warning when input_files is empty [#389](https://github.com/softprops/action-gh-release/pull/389) via [@Drowze](https://github.com/Drowze)
119 |
120 | ## 2.0.0
121 |
122 | - `2.0.0`!? this release corrects a disjunction between git tag versions used in the marketplace and versions list this file. Previous versions should have really been 1.\*. Going forward this should be better aligned.
123 | - Upgrade action.yml declaration to node20 to address deprecations
124 |
125 | ## 0.1.15
126 |
127 | - Upgrade to action.yml declaration to node16 to address deprecations
128 | - Upgrade dependencies
129 | - Add `asset` output as a JSON array containing information about the uploaded assets
130 |
131 | ## 0.1.14
132 |
133 | - provides an new workflow input option `generate_release_notes` which when set to true will automatically generate release notes for you based on GitHub activity [#179](https://github.com/softprops/action-gh-release/pull/179). Please see the [GitHub docs for this feature](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) for more information
134 |
135 | ## 0.1.13
136 |
137 | - fix issue with multiple runs concatenating release bodies [#145](https://github.com/softprops/action-gh-release/pull/145)
138 |
139 | ## 0.1.12
140 |
141 | - fix bug leading to empty strings subsituted for inputs users don't provide breaking api calls [#144](https://github.com/softprops/action-gh-release/pull/144)
142 |
143 | ## 0.1.11
144 |
145 | - better error message on release create failed [#143](https://github.com/softprops/action-gh-release/pull/143)
146 |
147 | ## 0.1.10
148 |
149 | - fixed error message formatting for file uploads
150 |
151 | ## 0.1.9
152 |
153 | - add support for linking release to GitHub discussion [#136](https://github.com/softprops/action-gh-release/pull/136)
154 |
155 | ## 0.1.8
156 |
157 | - address recent warnings in assert upload api as well as introduce asset upload overrides, allowing for multiple runs for the same release with the same named asserts [#134](https://github.com/softprops/action-gh-release/pull/134)
158 | - fix backwards compatibility with `GITHUB_TOKEN` resolution. `GITHUB_TOKEN` is no resolved first from an env varibale and then from and input [#133](https://github.com/softprops/action-gh-release/pull/133)
159 | - trim white space in provided `tag_name` [#130](https://github.com/softprops/action-gh-release/pull/130)
160 |
161 | ## 0.1.7
162 |
163 | - allow creating draft releases without a tag [#95](https://github.com/softprops/action-gh-release/pull/95)
164 | - Set default token for simpler setup [#83](https://github.com/softprops/action-gh-release/pull/83)
165 | - fix regression with action yml [#126](https://github.com/softprops/action-gh-release/pull/126)
166 |
167 | ## 0.1.6
168 |
169 | This is a release catch up have a hiatus. Future releases will happen more frequently
170 |
171 | - Add 'fail_on_unmatched_files' input, useful for catching cases were your `files` input does not actually match what you expect [#55](https://github.com/softprops/action-gh-release/pull/55)
172 | - Add `repository` input, useful for creating a release in an external repository [#61](https://github.com/softprops/action-gh-release/pull/61)
173 | - Add release `id` to outputs, useful for refering to release in workflow steps following the step that uses this action [#60](https://github.com/softprops/action-gh-release/pull/60)
174 | - Add `upload_url` as action output, useful for managing uploads separately [#75](https://github.com/softprops/action-gh-release/pull/75)
175 | - Support custom `target_commitish` value, useful to customize the default [#76](https://github.com/softprops/action-gh-release/pull/76)
176 | - fix `body_path` input first then fall back on `body` input. this was the originally documented precedence but was implemened the the opposite order! [#85](https://github.com/softprops/action-gh-release/pull/85)
177 | - Retain original release info if the keys are not set, useful for filling in blanks for a release you've already started separately [#109](https://github.com/softprops/action-gh-release/pull/109)
178 | - Limit number of times github api request to create a release is retried, useful for avoiding eating up your rate limit and action minutes do to either an invalid token or other circumstance causing the api call to fail [#111](https://github.com/softprops/action-gh-release/pull/111)
179 |
180 | ## 0.1.5
181 |
182 | - Added support for specifying tag name [#39](https://github.com/softprops/action-gh-release/pull/39)
183 |
184 | ## 0.1.4
185 |
186 | - Added support for updating releases body [#36](https://github.com/softprops/action-gh-release/pull/36)
187 | - Steps can now access the url of releases with the `url` output of this Action [#28](https://github.com/softprops/action-gh-release/pull/28)
188 | - Added basic GitHub API retry support to manage API turbulance [#26](https://github.com/softprops/action-gh-release/pull/26)
189 |
190 | ## 0.1.3
191 |
192 | - Fixed where `with: body_path` was not being used in generated GitHub releases
193 |
194 | ## 0.1.2
195 |
196 | - Add support for merging draft releases [#16](https://github.com/softprops/action-gh-release/pull/16)
197 |
198 | GitHub's api doesn't explicitly have a way of fetching a draft release by tag name which caused draft releases to appear as separate releases when used in a build matrix.
199 | This is now fixed.
200 |
201 | - Add support for newline-delimited asset list [#18](https://github.com/softprops/action-gh-release/pull/18)
202 |
203 | GitHub actions inputs don't inherently support lists of things and one might like to append a list of files to include in a release. Previously this was possible using a comma-delimited list of asset path patterns to upload. You can now provide these as a newline delimieted list for better readability
204 |
205 | ```yaml
206 | - name: Release
207 | uses: softprops/action-gh-release@v1
208 | if: startsWith(github.ref, 'refs/tags/')
209 | with:
210 | files: |
211 | filea.txt
212 | fileb.txt
213 | filec.txt
214 | env:
215 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
216 | ```
217 |
218 | - Add support for prerelease annotated GitHub releases with the new input field `with.prerelease: true` [#19](https://github.com/softprops/action-gh-release/pull/19)
219 |
220 | ---
221 |
222 | ## 0.1.1
223 |
224 | - Add support for publishing releases on all supported virtual hosts
225 |
226 | You'll need to remove `docker://` prefix and use the `@v1` action tag
227 |
228 | ---
229 |
230 | ## 0.1.0
231 |
232 | - Initial release
233 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## bootstrapping
2 |
3 | This a [JavaScript](https://help.github.com/en/articles/about-actions#types-of-actions) action but uses [TypeScript](https://www.typescriptlang.org/docs/home.html) to generate that JavaScript.
4 |
5 | You can bootstrap your environment with a modern version of npm and by running `npm i` at the root of this repo.
6 |
7 | ## testing
8 |
9 | Tests can be found under under `__tests__` directory and are runnable with the `npm t` command.
10 |
11 | ## source code
12 |
13 | Source code can be found under the `src` directory. Running `npm run build` will generate the JavaScript that will run within GitHub workflows.
14 |
15 | ## formatting
16 |
17 | A minimal attempt at keeping a consistent code style is can be applied by running `npm run fmt`.
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019-current Doug Tangren
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 📦 :octocat:
3 |
4 |
5 | action gh-release
6 |
7 |
8 |
9 | A GitHub Action for creating GitHub Releases on Linux, Windows, and macOS virtual environments
10 |
11 |
12 |
13 |

14 |
15 |
16 |
21 |
22 |
23 |
24 | - [🤸 Usage](#-usage)
25 | - [🚥 Limit releases to pushes to tags](#-limit-releases-to-pushes-to-tags)
26 | - [⬆️ Uploading release assets](#️-uploading-release-assets)
27 | - [📝 External release notes](#-external-release-notes)
28 | - [💅 Customizing](#-customizing)
29 | - [inputs](#inputs)
30 | - [outputs](#outputs)
31 | - [environment variables](#environment-variables)
32 | - [Permissions](#permissions)
33 |
34 | ## 🤸 Usage
35 |
36 | ### 🚥 Limit releases to pushes to tags
37 |
38 | Typically usage of this action involves adding a step to a build that
39 | is gated pushes to git tags. You may find `step.if` field helpful in accomplishing this
40 | as it maximizes the reuse value of your workflow for non-tag pushes.
41 |
42 | Below is a simple example of `step.if` tag gating
43 |
44 | ```yaml
45 | name: Main
46 |
47 | on: push
48 |
49 | jobs:
50 | build:
51 | runs-on: ubuntu-latest
52 | steps:
53 | - name: Checkout
54 | uses: actions/checkout@v4
55 | - name: Release
56 | uses: softprops/action-gh-release@v2
57 | if: github.ref_type == 'tag'
58 | ```
59 |
60 | You can also use push config tag filter
61 |
62 | ```yaml
63 | name: Main
64 |
65 | on:
66 | push:
67 | tags:
68 | - "v*.*.*"
69 |
70 | jobs:
71 | build:
72 | runs-on: ubuntu-latest
73 | steps:
74 | - name: Checkout
75 | uses: actions/checkout@v4
76 | - name: Release
77 | uses: softprops/action-gh-release@v2
78 | ```
79 |
80 | ### ⬆️ Uploading release assets
81 |
82 | You can configure a number of options for your
83 | GitHub release and all are optional.
84 |
85 | A common case for GitHub releases is to upload your binary after its been validated and packaged.
86 | Use the `with.files` input to declare a newline-delimited list of glob expressions matching the files
87 | you wish to upload to GitHub releases. If you'd like you can just list the files by name directly.
88 | If a tag already has a GitHub release, the existing release will be updated with the release assets.
89 |
90 | Below is an example of uploading a single asset named `Release.txt`
91 |
92 | ```yaml
93 | name: Main
94 |
95 | on: push
96 |
97 | jobs:
98 | build:
99 | runs-on: ubuntu-latest
100 | steps:
101 | - name: Checkout
102 | uses: actions/checkout@v4
103 | - name: Build
104 | run: echo ${{ github.sha }} > Release.txt
105 | - name: Test
106 | run: cat Release.txt
107 | - name: Release
108 | uses: softprops/action-gh-release@v2
109 | if: github.ref_type == 'tag'
110 | with:
111 | files: Release.txt
112 | ```
113 |
114 | Below is an example of uploading more than one asset with a GitHub release
115 |
116 | ```yaml
117 | name: Main
118 |
119 | on: push
120 |
121 | jobs:
122 | build:
123 | runs-on: ubuntu-latest
124 | steps:
125 | - name: Checkout
126 | uses: actions/checkout@v4
127 | - name: Build
128 | run: echo ${{ github.sha }} > Release.txt
129 | - name: Test
130 | run: cat Release.txt
131 | - name: Release
132 | uses: softprops/action-gh-release@v2
133 | if: github.ref_type == 'tag'
134 | with:
135 | files: |
136 | Release.txt
137 | LICENSE
138 | ```
139 |
140 | > **⚠️ Note:** Notice the `|` in the yaml syntax above ☝️. That lets you effectively declare a multi-line yaml string. You can learn more about multi-line yaml syntax [here](https://yaml-multiline.info)
141 |
142 | > **⚠️ Note for Windows:** Paths must use `/` as a separator, not `\`, as `\` is used to escape characters with special meaning in the pattern; for example, instead of specifying `D:\Foo.txt`, you must specify `D:/Foo.txt`. If you're using PowerShell, you can do this with `$Path = $Path -replace '\\','/'`
143 |
144 | ### 📝 External release notes
145 |
146 | Many systems exist that can help generate release notes for you. This action supports
147 | loading release notes from a path in your repository's build to allow for the flexibility
148 | of using any changelog generator for your releases, including a human 👩💻
149 |
150 | ```yaml
151 | name: Main
152 |
153 | on: push
154 |
155 | jobs:
156 | build:
157 | runs-on: ubuntu-latest
158 | steps:
159 | - name: Checkout
160 | uses: actions/checkout@v4
161 | - name: Generate Changelog
162 | run: echo "# Good things have arrived" > ${{ github.workspace }}-CHANGELOG.txt
163 | - name: Release
164 | uses: softprops/action-gh-release@v2
165 | if: github.ref_type == 'tag'
166 | with:
167 | body_path: ${{ github.workspace }}-CHANGELOG.txt
168 | repository: my_gh_org/my_gh_repo
169 | # note you'll typically need to create a personal access token
170 | # with permissions to create releases in the other repo
171 | token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
172 | ```
173 |
174 | ### 💅 Customizing
175 |
176 | #### inputs
177 |
178 | The following are optional as `step.with` keys
179 |
180 | | Name | Type | Description |
181 | | -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
182 | | `body` | String | Text communicating notable changes in this release |
183 | | `body_path` | String | Path to load text communicating notable changes in this release |
184 | | `draft` | Boolean | Indicator of whether or not this release is a draft |
185 | | `prerelease` | Boolean | Indicator of whether or not is a prerelease |
186 | | `preserve_order` | Boolean | Indicator of whether order of files should be preserved when uploading assets |
187 | | `files` | String | Newline-delimited globs of paths to assets to upload for release |
188 | | `name` | String | Name of the release. defaults to tag name |
189 | | `tag_name` | String | Name of a tag. defaults to `github.ref_name` |
190 | | `fail_on_unmatched_files` | Boolean | Indicator of whether to fail if any of the `files` globs match nothing |
191 | | `repository` | String | Name of a target repository in `/` format. Defaults to GITHUB_REPOSITORY env variable |
192 | | `target_commitish` | String | Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Defaults to repository default branch. |
193 | | `token` | String | Secret GitHub Personal Access Token. Defaults to `${{ github.token }}` |
194 | | `discussion_category_name` | String | If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. For more information, see ["Managing categories for discussions in your repository."](https://docs.github.com/en/discussions/managing-discussions-for-your-community/managing-categories-for-discussions-in-your-repository) |
195 | | `generate_release_notes` | Boolean | Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes. See the [GitHub docs for this feature](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) for more information |
196 | | `append_body` | Boolean | Append to existing body instead of overwriting it |
197 | | `make_latest` | String | Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. Can be `true`, `false`, or `legacy`. Uses GitHub api defaults if not provided |
198 |
199 | 💡 When providing a `body` and `body_path` at the same time, `body_path` will be
200 | attempted first, then falling back on `body` if the path can not be read from.
201 |
202 | 💡 When the release info keys (such as `name`, `body`, `draft`, `prerelease`, etc.)
203 | are not explicitly set and there is already an existing release for the tag, the
204 | release will retain its original info.
205 |
206 | #### outputs
207 |
208 | The following outputs can be accessed via `${{ steps..outputs }}` from this action
209 |
210 | | Name | Type | Description |
211 | | ------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
212 | | `url` | String | Github.com URL for the release |
213 | | `id` | String | Release ID |
214 | | `upload_url` | String | URL for uploading assets to the release |
215 | | `assets` | String | JSON array containing information about each uploaded asset, in the format given [here](https://docs.github.com/en/rest/releases/assets#get-a-release-asset) (minus the `uploader` field) |
216 |
217 | As an example, you can use `${{ fromJSON(steps..outputs.assets)[0].browser_download_url }}` to get the download URL of the first asset.
218 |
219 | #### environment variables
220 |
221 | The following `step.env` keys are allowed as a fallback but deprecated in favor of using inputs.
222 |
223 | | Name | Description |
224 | | ------------------- | ------------------------------------------------------------------------------------------ |
225 | | `GITHUB_TOKEN` | GITHUB_TOKEN as provided by `secrets` |
226 | | `GITHUB_REPOSITORY` | Name of a target repository in `/` format. defaults to the current repository |
227 |
228 | > **⚠️ Note:** This action was previously implemented as a Docker container, limiting its use to GitHub Actions Linux virtual environments only. With recent releases, we now support cross platform usage. You'll need to remove the `docker://` prefix in these versions
229 |
230 | ### Permissions
231 |
232 | This Action requires the following permissions on the GitHub integration token:
233 |
234 | ```yaml
235 | permissions:
236 | contents: write
237 | ```
238 |
239 | When used with `discussion_category_name`, additional permission is needed:
240 |
241 | ```yaml
242 | permissions:
243 | contents: write
244 | discussions: write
245 | ```
246 |
247 | [GitHub token permissions](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) can be set for an individual job, workflow, or for Actions as a whole.
248 |
249 | Note that if you intend to run workflows on the release event (`on: { release: { types: [published] } }`), you need to use
250 | a personal access token for this action, as the [default `secrets.GITHUB_TOKEN` does not trigger another workflow](https://github.com/actions/create-release/issues/71).
251 |
252 | Doug Tangren (softprops) 2019
253 |
--------------------------------------------------------------------------------
/__tests__/github.test.ts:
--------------------------------------------------------------------------------
1 | import * as assert from "assert";
2 | import {
3 | mimeOrDefault,
4 | asset,
5 | Releaser,
6 | Release,
7 | findTagFromReleases,
8 | } from "../src/github";
9 |
10 | describe("github", () => {
11 | describe("mimeOrDefault", () => {
12 | it("returns a specific mime for common path", async () => {
13 | assert.equal(mimeOrDefault("foo.tar.gz"), "application/gzip");
14 | });
15 | it("returns default mime for uncommon path", async () => {
16 | assert.equal(mimeOrDefault("foo.uncommon"), "application/octet-stream");
17 | });
18 | });
19 |
20 | describe("asset", () => {
21 | it("derives asset info from a path", async () => {
22 | const { name, mime, size } = asset("tests/data/foo/bar.txt");
23 | assert.equal(name, "bar.txt");
24 | assert.equal(mime, "text/plain");
25 | assert.equal(size, 10);
26 | });
27 | });
28 |
29 | describe("findTagFromReleases", () => {
30 | const owner = "owner";
31 | const repo = "repo";
32 |
33 | const mockRelease: Release = {
34 | id: 1,
35 | upload_url: `https://api.github.com/repos/${owner}/${repo}/releases/1/assets`,
36 | html_url: `https://github.com/${owner}/${repo}/releases/tag/v1.0.0`,
37 | tag_name: "v1.0.0",
38 | name: "Test Release",
39 | body: "Test body",
40 | target_commitish: "main",
41 | draft: false,
42 | prerelease: false,
43 | assets: [],
44 | } as const;
45 |
46 | const mockReleaser: Releaser = {
47 | getReleaseByTag: () => Promise.reject("Not implemented"),
48 | createRelease: () => Promise.reject("Not implemented"),
49 | updateRelease: () => Promise.reject("Not implemented"),
50 | allReleases: async function* () {
51 | yield { data: [mockRelease] };
52 | },
53 | } as const;
54 |
55 | describe("when the tag_name is not an empty string", () => {
56 | const targetTag = "v1.0.0";
57 |
58 | it("finds a matching release in first batch of results", async () => {
59 | const targetRelease = {
60 | ...mockRelease,
61 | owner,
62 | repo,
63 | tag_name: targetTag,
64 | };
65 | const otherRelease = {
66 | ...mockRelease,
67 | owner,
68 | repo,
69 | tag_name: "v1.0.1",
70 | };
71 |
72 | const releaser = {
73 | ...mockReleaser,
74 | allReleases: async function* () {
75 | yield { data: [targetRelease] };
76 | yield { data: [otherRelease] };
77 | },
78 | };
79 |
80 | const result = await findTagFromReleases(
81 | releaser,
82 | owner,
83 | repo,
84 | targetTag,
85 | );
86 |
87 | assert.deepStrictEqual(result, targetRelease);
88 | });
89 |
90 | it("finds a matching release in second batch of results", async () => {
91 | const targetRelease = {
92 | ...mockRelease,
93 | owner,
94 | repo,
95 | tag_name: targetTag,
96 | };
97 | const otherRelease = {
98 | ...mockRelease,
99 | owner,
100 | repo,
101 | tag_name: "v1.0.1",
102 | };
103 |
104 | const releaser = {
105 | ...mockReleaser,
106 | allReleases: async function* () {
107 | yield { data: [otherRelease] };
108 | yield { data: [targetRelease] };
109 | },
110 | };
111 |
112 | const result = await findTagFromReleases(
113 | releaser,
114 | owner,
115 | repo,
116 | targetTag,
117 | );
118 | assert.deepStrictEqual(result, targetRelease);
119 | });
120 |
121 | it("returns undefined when a release is not found in any batch", async () => {
122 | const otherRelease = {
123 | ...mockRelease,
124 | owner,
125 | repo,
126 | tag_name: "v1.0.1",
127 | };
128 | const releaser = {
129 | ...mockReleaser,
130 | allReleases: async function* () {
131 | yield { data: [otherRelease] };
132 | yield { data: [otherRelease] };
133 | },
134 | };
135 |
136 | const result = await findTagFromReleases(
137 | releaser,
138 | owner,
139 | repo,
140 | targetTag,
141 | );
142 |
143 | assert.strictEqual(result, undefined);
144 | });
145 |
146 | it("returns undefined when no releases are returned", async () => {
147 | const releaser = {
148 | ...mockReleaser,
149 | allReleases: async function* () {
150 | yield { data: [] };
151 | },
152 | };
153 |
154 | const result = await findTagFromReleases(
155 | releaser,
156 | owner,
157 | repo,
158 | targetTag,
159 | );
160 |
161 | assert.strictEqual(result, undefined);
162 | });
163 | });
164 |
165 | describe("when the tag_name is an empty string", () => {
166 | const emptyTag = "";
167 |
168 | it("finds a matching release in first batch of results", async () => {
169 | const targetRelease = {
170 | ...mockRelease,
171 | owner,
172 | repo,
173 | tag_name: emptyTag,
174 | };
175 | const otherRelease = {
176 | ...mockRelease,
177 | owner,
178 | repo,
179 | tag_name: "v1.0.1",
180 | };
181 |
182 | const releaser = {
183 | ...mockReleaser,
184 | allReleases: async function* () {
185 | yield { data: [targetRelease] };
186 | yield { data: [otherRelease] };
187 | },
188 | };
189 |
190 | const result = await findTagFromReleases(
191 | releaser,
192 | owner,
193 | repo,
194 | emptyTag,
195 | );
196 |
197 | assert.deepStrictEqual(result, targetRelease);
198 | });
199 |
200 | it("finds a matching release in second batch of results", async () => {
201 | const targetRelease = {
202 | ...mockRelease,
203 | owner,
204 | repo,
205 | tag_name: emptyTag,
206 | };
207 | const otherRelease = {
208 | ...mockRelease,
209 | owner,
210 | repo,
211 | tag_name: "v1.0.1",
212 | };
213 |
214 | const releaser = {
215 | ...mockReleaser,
216 | allReleases: async function* () {
217 | yield { data: [otherRelease] };
218 | yield { data: [targetRelease] };
219 | },
220 | };
221 |
222 | const result = await findTagFromReleases(
223 | releaser,
224 | owner,
225 | repo,
226 | emptyTag,
227 | );
228 | assert.deepStrictEqual(result, targetRelease);
229 | });
230 |
231 | it("returns undefined when a release is not found in any batch", async () => {
232 | const otherRelease = {
233 | ...mockRelease,
234 | owner,
235 | repo,
236 | tag_name: "v1.0.1",
237 | };
238 | const releaser = {
239 | ...mockReleaser,
240 | allReleases: async function* () {
241 | yield { data: [otherRelease] };
242 | yield { data: [otherRelease] };
243 | },
244 | };
245 |
246 | const result = await findTagFromReleases(
247 | releaser,
248 | owner,
249 | repo,
250 | emptyTag,
251 | );
252 |
253 | assert.strictEqual(result, undefined);
254 | });
255 |
256 | it("returns undefined when no releases are returned", async () => {
257 | const releaser = {
258 | ...mockReleaser,
259 | allReleases: async function* () {
260 | yield { data: [] };
261 | },
262 | };
263 |
264 | const result = await findTagFromReleases(
265 | releaser,
266 | owner,
267 | repo,
268 | emptyTag,
269 | );
270 |
271 | assert.strictEqual(result, undefined);
272 | });
273 | });
274 | });
275 | });
276 |
--------------------------------------------------------------------------------
/__tests__/release.txt:
--------------------------------------------------------------------------------
1 | bar
--------------------------------------------------------------------------------
/__tests__/util.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | releaseBody,
3 | isTag,
4 | paths,
5 | parseConfig,
6 | parseInputFiles,
7 | unmatchedPatterns,
8 | uploadUrl,
9 | alignAssetName,
10 | } from "../src/util";
11 | import * as assert from "assert";
12 |
13 | describe("util", () => {
14 | describe("uploadUrl", () => {
15 | it("strips template", () => {
16 | assert.equal(
17 | uploadUrl(
18 | "https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}",
19 | ),
20 | "https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets",
21 | );
22 | });
23 | });
24 | describe("parseInputFiles", () => {
25 | it("parses empty strings", () => {
26 | assert.deepStrictEqual(parseInputFiles(""), []);
27 | });
28 | it("parses comma-delimited strings", () => {
29 | assert.deepStrictEqual(parseInputFiles("foo,bar"), ["foo", "bar"]);
30 | });
31 | it("parses newline and comma-delimited (and then some)", () => {
32 | assert.deepStrictEqual(
33 | parseInputFiles("foo,bar\nbaz,boom,\n\ndoom,loom "),
34 | ["foo", "bar", "baz", "boom", "doom", "loom"],
35 | );
36 | });
37 | });
38 | describe("releaseBody", () => {
39 | it("uses input body", () => {
40 | assert.equal(
41 | "foo",
42 | releaseBody({
43 | github_ref: "",
44 | github_repository: "",
45 | github_token: "",
46 | input_body: "foo",
47 | input_body_path: undefined,
48 | input_draft: false,
49 | input_prerelease: false,
50 | input_preserve_order: undefined,
51 | input_files: [],
52 | input_name: undefined,
53 | input_tag_name: undefined,
54 | input_target_commitish: undefined,
55 | input_discussion_category_name: undefined,
56 | input_generate_release_notes: false,
57 | input_make_latest: undefined,
58 | }),
59 | );
60 | });
61 | it("uses input body path", () => {
62 | assert.equal(
63 | "bar",
64 | releaseBody({
65 | github_ref: "",
66 | github_repository: "",
67 | github_token: "",
68 | input_body: undefined,
69 | input_body_path: "__tests__/release.txt",
70 | input_draft: false,
71 | input_prerelease: false,
72 | input_preserve_order: undefined,
73 | input_files: [],
74 | input_name: undefined,
75 | input_tag_name: undefined,
76 | input_target_commitish: undefined,
77 | input_discussion_category_name: undefined,
78 | input_generate_release_notes: false,
79 | input_make_latest: undefined,
80 | }),
81 | );
82 | });
83 | it("defaults to body path when both body and body path are provided", () => {
84 | assert.equal(
85 | "bar",
86 | releaseBody({
87 | github_ref: "",
88 | github_repository: "",
89 | github_token: "",
90 | input_body: "foo",
91 | input_body_path: "__tests__/release.txt",
92 | input_draft: false,
93 | input_prerelease: false,
94 | input_preserve_order: undefined,
95 | input_files: [],
96 | input_name: undefined,
97 | input_tag_name: undefined,
98 | input_target_commitish: undefined,
99 | input_discussion_category_name: undefined,
100 | input_generate_release_notes: false,
101 | input_make_latest: undefined,
102 | }),
103 | );
104 | });
105 | });
106 | describe("parseConfig", () => {
107 | it("parses basic config", () => {
108 | assert.deepStrictEqual(
109 | parseConfig({
110 | // note: inputs declared in actions.yml, even when declared not required,
111 | // are still provided by the actions runtime env as empty strings instead of
112 | // the normal absent env value one would expect. this breaks things
113 | // as an empty string !== undefined in terms of what we pass to the api
114 | // so we cover that in a test case here to ensure undefined values are actually
115 | // resolved as undefined and not empty strings
116 | INPUT_TARGET_COMMITISH: "",
117 | INPUT_DISCUSSION_CATEGORY_NAME: "",
118 | }),
119 | {
120 | github_ref: "",
121 | github_repository: "",
122 | github_token: "",
123 | input_append_body: false,
124 | input_body: undefined,
125 | input_body_path: undefined,
126 | input_draft: undefined,
127 | input_prerelease: undefined,
128 | input_preserve_order: undefined,
129 | input_files: [],
130 | input_name: undefined,
131 | input_tag_name: undefined,
132 | input_fail_on_unmatched_files: false,
133 | input_target_commitish: undefined,
134 | input_discussion_category_name: undefined,
135 | input_generate_release_notes: false,
136 | input_make_latest: undefined,
137 | },
138 | );
139 | });
140 |
141 | it("parses basic config with commitish", () => {
142 | assert.deepStrictEqual(
143 | parseConfig({
144 | INPUT_TARGET_COMMITISH: "affa18ef97bc9db20076945705aba8c516139abd",
145 | }),
146 | {
147 | github_ref: "",
148 | github_repository: "",
149 | github_token: "",
150 | input_append_body: false,
151 | input_body: undefined,
152 | input_body_path: undefined,
153 | input_draft: undefined,
154 | input_prerelease: undefined,
155 | input_files: [],
156 | input_preserve_order: undefined,
157 | input_name: undefined,
158 | input_tag_name: undefined,
159 | input_fail_on_unmatched_files: false,
160 | input_target_commitish: "affa18ef97bc9db20076945705aba8c516139abd",
161 | input_discussion_category_name: undefined,
162 | input_generate_release_notes: false,
163 | input_make_latest: undefined,
164 | },
165 | );
166 | });
167 | it("supports discussion category names", () => {
168 | assert.deepStrictEqual(
169 | parseConfig({
170 | INPUT_DISCUSSION_CATEGORY_NAME: "releases",
171 | }),
172 | {
173 | github_ref: "",
174 | github_repository: "",
175 | github_token: "",
176 | input_append_body: false,
177 | input_body: undefined,
178 | input_body_path: undefined,
179 | input_draft: undefined,
180 | input_prerelease: undefined,
181 | input_files: [],
182 | input_preserve_order: undefined,
183 | input_name: undefined,
184 | input_tag_name: undefined,
185 | input_fail_on_unmatched_files: false,
186 | input_target_commitish: undefined,
187 | input_discussion_category_name: "releases",
188 | input_generate_release_notes: false,
189 | input_make_latest: undefined,
190 | },
191 | );
192 | });
193 |
194 | it("supports generating release notes", () => {
195 | assert.deepStrictEqual(
196 | parseConfig({
197 | INPUT_GENERATE_RELEASE_NOTES: "true",
198 | }),
199 | {
200 | github_ref: "",
201 | github_repository: "",
202 | github_token: "",
203 | input_append_body: false,
204 | input_body: undefined,
205 | input_body_path: undefined,
206 | input_draft: undefined,
207 | input_prerelease: undefined,
208 | input_preserve_order: undefined,
209 | input_files: [],
210 | input_name: undefined,
211 | input_tag_name: undefined,
212 | input_fail_on_unmatched_files: false,
213 | input_target_commitish: undefined,
214 | input_discussion_category_name: undefined,
215 | input_generate_release_notes: true,
216 | input_make_latest: undefined,
217 | },
218 | );
219 | });
220 |
221 | it("prefers GITHUB_TOKEN over token input for backwards compatibility", () => {
222 | assert.deepStrictEqual(
223 | parseConfig({
224 | INPUT_DRAFT: "false",
225 | INPUT_PRERELEASE: "true",
226 | INPUT_PRESERVE_ORDER: "true",
227 | GITHUB_TOKEN: "env-token",
228 | INPUT_TOKEN: "input-token",
229 | }),
230 | {
231 | github_ref: "",
232 | github_repository: "",
233 | github_token: "env-token",
234 | input_append_body: false,
235 | input_body: undefined,
236 | input_body_path: undefined,
237 | input_draft: false,
238 | input_prerelease: true,
239 | input_preserve_order: true,
240 | input_files: [],
241 | input_name: undefined,
242 | input_tag_name: undefined,
243 | input_fail_on_unmatched_files: false,
244 | input_target_commitish: undefined,
245 | input_discussion_category_name: undefined,
246 | input_generate_release_notes: false,
247 | input_make_latest: undefined,
248 | },
249 | );
250 | });
251 | it("uses input token as the source of GITHUB_TOKEN by default", () => {
252 | assert.deepStrictEqual(
253 | parseConfig({
254 | INPUT_DRAFT: "false",
255 | INPUT_PRERELEASE: "true",
256 | INPUT_TOKEN: "input-token",
257 | }),
258 | {
259 | github_ref: "",
260 | github_repository: "",
261 | github_token: "input-token",
262 | input_append_body: false,
263 | input_body: undefined,
264 | input_body_path: undefined,
265 | input_draft: false,
266 | input_prerelease: true,
267 | input_preserve_order: undefined,
268 | input_files: [],
269 | input_name: undefined,
270 | input_tag_name: undefined,
271 | input_fail_on_unmatched_files: false,
272 | input_target_commitish: undefined,
273 | input_discussion_category_name: undefined,
274 | input_generate_release_notes: false,
275 | input_make_latest: undefined,
276 | },
277 | );
278 | });
279 | it("parses basic config with draft and prerelease", () => {
280 | assert.deepStrictEqual(
281 | parseConfig({
282 | INPUT_DRAFT: "false",
283 | INPUT_PRERELEASE: "true",
284 | }),
285 | {
286 | github_ref: "",
287 | github_repository: "",
288 | github_token: "",
289 | input_append_body: false,
290 | input_body: undefined,
291 | input_body_path: undefined,
292 | input_draft: false,
293 | input_prerelease: true,
294 | input_preserve_order: undefined,
295 | input_files: [],
296 | input_name: undefined,
297 | input_tag_name: undefined,
298 | input_fail_on_unmatched_files: false,
299 | input_target_commitish: undefined,
300 | input_discussion_category_name: undefined,
301 | input_generate_release_notes: false,
302 | input_make_latest: undefined,
303 | },
304 | );
305 | });
306 | it("parses basic config where make_latest is passed", () => {
307 | assert.deepStrictEqual(
308 | parseConfig({
309 | INPUT_MAKE_LATEST: "false",
310 | }),
311 | {
312 | github_ref: "",
313 | github_repository: "",
314 | github_token: "",
315 | input_append_body: false,
316 | input_body: undefined,
317 | input_body_path: undefined,
318 | input_draft: undefined,
319 | input_prerelease: undefined,
320 | input_preserve_order: undefined,
321 | input_files: [],
322 | input_name: undefined,
323 | input_tag_name: undefined,
324 | input_fail_on_unmatched_files: false,
325 | input_target_commitish: undefined,
326 | input_discussion_category_name: undefined,
327 | input_generate_release_notes: false,
328 | input_make_latest: "false",
329 | },
330 | );
331 | });
332 | it("parses basic config with append_body", () => {
333 | assert.deepStrictEqual(
334 | parseConfig({
335 | INPUT_APPEND_BODY: "true",
336 | }),
337 | {
338 | github_ref: "",
339 | github_repository: "",
340 | github_token: "",
341 | input_append_body: true,
342 | input_body: undefined,
343 | input_body_path: undefined,
344 | input_draft: undefined,
345 | input_prerelease: undefined,
346 | input_preserve_order: undefined,
347 | input_files: [],
348 | input_name: undefined,
349 | input_tag_name: undefined,
350 | input_fail_on_unmatched_files: false,
351 | input_target_commitish: undefined,
352 | input_discussion_category_name: undefined,
353 | input_generate_release_notes: false,
354 | input_make_latest: undefined,
355 | },
356 | );
357 | });
358 | });
359 | describe("isTag", () => {
360 | it("returns true for tags", async () => {
361 | assert.equal(isTag("refs/tags/foo"), true);
362 | });
363 | it("returns false for other kinds of refs", async () => {
364 | assert.equal(isTag("refs/heads/master"), false);
365 | });
366 | });
367 |
368 | describe("paths", () => {
369 | it("resolves files given a set of paths", async () => {
370 | assert.deepStrictEqual(
371 | paths(["tests/data/**/*", "tests/data/does/not/exist/*"]),
372 | ["tests/data/foo/bar.txt"],
373 | );
374 | });
375 | });
376 |
377 | describe("unmatchedPatterns", () => {
378 | it("returns the patterns that don't match any files", async () => {
379 | assert.deepStrictEqual(
380 | unmatchedPatterns(["tests/data/**/*", "tests/data/does/not/exist/*"]),
381 | ["tests/data/does/not/exist/*"],
382 | );
383 | });
384 | });
385 |
386 | describe("replaceSpacesWithDots", () => {
387 | it("replaces all spaces with dots", () => {
388 | expect(alignAssetName("John Doe.bla")).toBe("John.Doe.bla");
389 | });
390 |
391 | it("handles names with multiple spaces", () => {
392 | expect(alignAssetName("John William Doe.bla")).toBe(
393 | "John.William.Doe.bla",
394 | );
395 | });
396 |
397 | it("returns the same string if there are no spaces", () => {
398 | expect(alignAssetName("JohnDoe")).toBe("JohnDoe");
399 | });
400 | });
401 | });
402 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | # https://help.github.com/en/articles/metadata-syntax-for-github-actions
2 | name: "GH Release"
3 | description: "Github Action for creating Github Releases"
4 | author: "softprops"
5 | inputs:
6 | body:
7 | description: "Note-worthy description of changes in release"
8 | required: false
9 | body_path:
10 | description: "Path to load note-worthy description of changes in release from"
11 | required: false
12 | name:
13 | description: "Gives the release a custom name. Defaults to tag name"
14 | required: false
15 | tag_name:
16 | description: "Gives a tag name. Defaults to github.ref_name"
17 | required: false
18 | draft:
19 | description: "Creates a draft release. Defaults to false"
20 | required: false
21 | prerelease:
22 | description: "Identify the release as a prerelease. Defaults to false"
23 | required: false
24 | preserve_order:
25 | description: "Preserver the order of the artifacts when uploading"
26 | required: false
27 | files:
28 | description: "Newline-delimited list of path globs for asset files to upload"
29 | required: false
30 | fail_on_unmatched_files:
31 | description: "Fails if any of the `files` globs match nothing. Defaults to false"
32 | required: false
33 | repository:
34 | description: "Repository to make releases against, in / format"
35 | required: false
36 | token:
37 | description: "Authorized secret GitHub Personal Access Token. Defaults to github.token"
38 | required: false
39 | default: ${{ github.token }}
40 | target_commitish:
41 | description: "Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA."
42 | required: false
43 | discussion_category_name:
44 | description: "If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. If there is already a discussion linked to the release, this parameter is ignored."
45 | required: false
46 | generate_release_notes:
47 | description: "Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes."
48 | required: false
49 | append_body:
50 | description: "Append to existing body instead of overwriting it. Default is false."
51 | required: false
52 | make_latest:
53 | description: "Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. Can be `true`, `false`, or `legacy`. Uses GitHub api default if not provided"
54 | required: false
55 | env:
56 | GITHUB_TOKEN: "As provided by Github Actions"
57 | outputs:
58 | url:
59 | description: "URL to the Release HTML Page"
60 | id:
61 | description: "Release ID"
62 | upload_url:
63 | description: "URL for uploading assets to the release"
64 | assets:
65 | description: "JSON array containing information about each uploaded asset, in the format given [here](https://docs.github.com/en/rest/reference/repos#upload-a-release-asset--code-samples) (minus the `uploader` field)"
66 | runs:
67 | using: "node20"
68 | main: "dist/index.js"
69 | branding:
70 | color: "green"
71 | icon: "package"
72 |
--------------------------------------------------------------------------------
/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/softprops/action-gh-release/37fd9d0351a2df198244c8ef9f56d02d1f921e20/demo.png
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { JestConfigWithTsJest } from 'ts-jest';
2 |
3 | const config: JestConfigWithTsJest = {
4 | preset: 'ts-jest/presets/default-esm',
5 | clearMocks: true,
6 | moduleFileExtensions: ['js', 'ts'],
7 | testEnvironment: 'node',
8 | testMatch: ['**/*.test.ts'],
9 | testRunner: 'jest-circus/runner',
10 | transform: {
11 | '^.+\\.ts$': 'ts-jest',
12 | },
13 | verbose: true,
14 | };
15 |
16 | export default config;
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "action-gh-release",
3 | "version": "2.2.2",
4 | "private": true,
5 | "description": "GitHub Action for creating GitHub Releases",
6 | "main": "lib/main.js",
7 | "scripts": {
8 | "build": "ncc build src/main.ts --minify",
9 | "build-debug": "ncc build src/main.ts --v8-cache --source-map",
10 | "test": "jest",
11 | "fmt": "prettier --write \"src/**/*.ts\" \"__tests__/**/*.ts\"",
12 | "fmtcheck": "prettier --check \"src/**/*.ts\" \"__tests__/**/*.ts\"",
13 | "updatetag": "git tag -d v2 && git push origin :v2 && git tag -a v2 -m '' && git push origin v2"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/softprops/action-gh-release.git"
18 | },
19 | "keywords": [
20 | "actions"
21 | ],
22 | "author": "softprops",
23 | "dependencies": {
24 | "@actions/core": "^1.11.1",
25 | "@actions/github": "^6.0.0",
26 | "@octokit/plugin-retry": "^7.2.1",
27 | "@octokit/plugin-throttling": "^10.0.0",
28 | "glob": "^11.0.2",
29 | "mime": "^3.0.0"
30 | },
31 | "devDependencies": {
32 | "@types/glob": "^8.1.0",
33 | "@types/jest": "^29.5.14",
34 | "@types/mime": "^3.0.1",
35 | "@types/node": "^20.17.32",
36 | "@vercel/ncc": "^0.38.3",
37 | "jest": "^29.3.1",
38 | "jest-circus": "^29.3.1",
39 | "prettier": "3.5.3",
40 | "ts-jest": "^29.3.2",
41 | "ts-node": "^10.9.2",
42 | "typescript": "^5.8.3",
43 | "typescript-formatter": "^7.2.2"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/github.ts:
--------------------------------------------------------------------------------
1 | import { GitHub } from "@actions/github/lib/utils";
2 | import { Config, isTag, releaseBody, alignAssetName } from "./util";
3 | import { statSync } from "fs";
4 | import { open } from "fs/promises";
5 | import { getType } from "mime";
6 | import { basename } from "path";
7 |
8 | type GitHub = InstanceType;
9 |
10 | export interface ReleaseAsset {
11 | name: string;
12 | mime: string;
13 | size: number;
14 | }
15 |
16 | export interface Release {
17 | id: number;
18 | upload_url: string;
19 | html_url: string;
20 | tag_name: string;
21 | name: string | null;
22 | body?: string | null | undefined;
23 | target_commitish: string;
24 | draft: boolean;
25 | prerelease: boolean;
26 | assets: Array<{ id: number; name: string }>;
27 | }
28 |
29 | export interface Releaser {
30 | getReleaseByTag(params: {
31 | owner: string;
32 | repo: string;
33 | tag: string;
34 | }): Promise<{ data: Release }>;
35 |
36 | createRelease(params: {
37 | owner: string;
38 | repo: string;
39 | tag_name: string;
40 | name: string;
41 | body: string | undefined;
42 | draft: boolean | undefined;
43 | prerelease: boolean | undefined;
44 | target_commitish: string | undefined;
45 | discussion_category_name: string | undefined;
46 | generate_release_notes: boolean | undefined;
47 | make_latest: "true" | "false" | "legacy" | undefined;
48 | }): Promise<{ data: Release }>;
49 |
50 | updateRelease(params: {
51 | owner: string;
52 | repo: string;
53 | release_id: number;
54 | tag_name: string;
55 | target_commitish: string;
56 | name: string;
57 | body: string | undefined;
58 | draft: boolean | undefined;
59 | prerelease: boolean | undefined;
60 | discussion_category_name: string | undefined;
61 | generate_release_notes: boolean | undefined;
62 | make_latest: "true" | "false" | "legacy" | undefined;
63 | }): Promise<{ data: Release }>;
64 |
65 | allReleases(params: {
66 | owner: string;
67 | repo: string;
68 | }): AsyncIterableIterator<{ data: Release[] }>;
69 | }
70 |
71 | export class GitHubReleaser implements Releaser {
72 | github: GitHub;
73 | constructor(github: GitHub) {
74 | this.github = github;
75 | }
76 |
77 | getReleaseByTag(params: {
78 | owner: string;
79 | repo: string;
80 | tag: string;
81 | }): Promise<{ data: Release }> {
82 | return this.github.rest.repos.getReleaseByTag(params);
83 | }
84 |
85 | createRelease(params: {
86 | owner: string;
87 | repo: string;
88 | tag_name: string;
89 | name: string;
90 | body: string | undefined;
91 | draft: boolean | undefined;
92 | prerelease: boolean | undefined;
93 | target_commitish: string | undefined;
94 | discussion_category_name: string | undefined;
95 | generate_release_notes: boolean | undefined;
96 | make_latest: "true" | "false" | "legacy" | undefined;
97 | }): Promise<{ data: Release }> {
98 | if (
99 | typeof params.make_latest === "string" &&
100 | !["true", "false", "legacy"].includes(params.make_latest)
101 | ) {
102 | params.make_latest = undefined;
103 | }
104 |
105 | return this.github.rest.repos.createRelease(params);
106 | }
107 |
108 | updateRelease(params: {
109 | owner: string;
110 | repo: string;
111 | release_id: number;
112 | tag_name: string;
113 | target_commitish: string;
114 | name: string;
115 | body: string | undefined;
116 | draft: boolean | undefined;
117 | prerelease: boolean | undefined;
118 | discussion_category_name: string | undefined;
119 | generate_release_notes: boolean | undefined;
120 | make_latest: "true" | "false" | "legacy" | undefined;
121 | }): Promise<{ data: Release }> {
122 | if (
123 | typeof params.make_latest === "string" &&
124 | !["true", "false", "legacy"].includes(params.make_latest)
125 | ) {
126 | params.make_latest = undefined;
127 | }
128 |
129 | return this.github.rest.repos.updateRelease(params);
130 | }
131 |
132 | allReleases(params: {
133 | owner: string;
134 | repo: string;
135 | }): AsyncIterableIterator<{ data: Release[] }> {
136 | const updatedParams = { per_page: 100, ...params };
137 | return this.github.paginate.iterator(
138 | this.github.rest.repos.listReleases.endpoint.merge(updatedParams),
139 | );
140 | }
141 | }
142 |
143 | export const asset = (path: string): ReleaseAsset => {
144 | return {
145 | name: basename(path),
146 | mime: mimeOrDefault(path),
147 | size: statSync(path).size,
148 | };
149 | };
150 |
151 | export const mimeOrDefault = (path: string): string => {
152 | return getType(path) || "application/octet-stream";
153 | };
154 |
155 | export const upload = async (
156 | config: Config,
157 | github: GitHub,
158 | url: string,
159 | path: string,
160 | currentAssets: Array<{ id: number; name: string }>,
161 | ): Promise => {
162 | const [owner, repo] = config.github_repository.split("/");
163 | const { name, mime, size } = asset(path);
164 | const currentAsset = currentAssets.find(
165 | // note: GitHub renames asset filenames that have special characters, non-alphanumeric characters, and leading or trailing periods. The "List release assets" endpoint lists the renamed filenames.
166 | // due to this renaming we need to be mindful when we compare the file name we're uploading with a name github may already have rewritten for logical comparison
167 | // see https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset
168 | ({ name: currentName }) => currentName == alignAssetName(name),
169 | );
170 | if (currentAsset) {
171 | console.log(`♻️ Deleting previously uploaded asset ${name}...`);
172 | await github.rest.repos.deleteReleaseAsset({
173 | asset_id: currentAsset.id || 1,
174 | owner,
175 | repo,
176 | });
177 | }
178 | console.log(`⬆️ Uploading ${name}...`);
179 | const endpoint = new URL(url);
180 | endpoint.searchParams.append("name", name);
181 | const fh = await open(path);
182 | try {
183 | const resp = await github.request({
184 | method: "POST",
185 | url: endpoint.toString(),
186 | headers: {
187 | "content-length": `${size}`,
188 | "content-type": mime,
189 | authorization: `token ${config.github_token}`,
190 | },
191 | data: fh.readableWebStream({ type: "bytes" }),
192 | });
193 | const json = resp.data;
194 | if (resp.status !== 201) {
195 | throw new Error(
196 | `Failed to upload release asset ${name}. received status code ${
197 | resp.status
198 | }\n${json.message}\n${JSON.stringify(json.errors)}`,
199 | );
200 | }
201 | console.log(`✅ Uploaded ${name}`);
202 | return json;
203 | } finally {
204 | await fh.close();
205 | }
206 | };
207 |
208 | export const release = async (
209 | config: Config,
210 | releaser: Releaser,
211 | maxRetries: number = 3,
212 | ): Promise => {
213 | if (maxRetries <= 0) {
214 | console.log(`❌ Too many retries. Aborting...`);
215 | throw new Error("Too many retries.");
216 | }
217 |
218 | const [owner, repo] = config.github_repository.split("/");
219 | const tag =
220 | config.input_tag_name ||
221 | (isTag(config.github_ref)
222 | ? config.github_ref.replace("refs/tags/", "")
223 | : "");
224 |
225 | const discussion_category_name = config.input_discussion_category_name;
226 | const generate_release_notes = config.input_generate_release_notes;
227 | try {
228 | const _release: Release | undefined = await findTagFromReleases(
229 | releaser,
230 | owner,
231 | repo,
232 | tag,
233 | );
234 |
235 | if (_release === undefined) {
236 | return await createRelease(
237 | tag,
238 | config,
239 | releaser,
240 | owner,
241 | repo,
242 | discussion_category_name,
243 | generate_release_notes,
244 | maxRetries,
245 | );
246 | }
247 |
248 | let existingRelease: Release = _release!;
249 | console.log(
250 | `Found release ${existingRelease.name} (with id=${existingRelease.id})`,
251 | );
252 |
253 | const release_id = existingRelease.id;
254 | let target_commitish: string;
255 | if (
256 | config.input_target_commitish &&
257 | config.input_target_commitish !== existingRelease.target_commitish
258 | ) {
259 | console.log(
260 | `Updating commit from "${existingRelease.target_commitish}" to "${config.input_target_commitish}"`,
261 | );
262 | target_commitish = config.input_target_commitish;
263 | } else {
264 | target_commitish = existingRelease.target_commitish;
265 | }
266 |
267 | const tag_name = tag;
268 | const name = config.input_name || existingRelease.name || tag;
269 | // revisit: support a new body-concat-strategy input for accumulating
270 | // body parts as a release gets updated. some users will likely want this while
271 | // others won't previously this was duplicating content for most which
272 | // no one wants
273 | const workflowBody = releaseBody(config) || "";
274 | const existingReleaseBody = existingRelease.body || "";
275 | let body: string;
276 | if (config.input_append_body && workflowBody && existingReleaseBody) {
277 | body = existingReleaseBody + "\n" + workflowBody;
278 | } else {
279 | body = workflowBody || existingReleaseBody;
280 | }
281 |
282 | const draft =
283 | config.input_draft !== undefined
284 | ? config.input_draft
285 | : existingRelease.draft;
286 | const prerelease =
287 | config.input_prerelease !== undefined
288 | ? config.input_prerelease
289 | : existingRelease.prerelease;
290 |
291 | const make_latest = config.input_make_latest;
292 |
293 | const release = await releaser.updateRelease({
294 | owner,
295 | repo,
296 | release_id,
297 | tag_name,
298 | target_commitish,
299 | name,
300 | body,
301 | draft,
302 | prerelease,
303 | discussion_category_name,
304 | generate_release_notes,
305 | make_latest,
306 | });
307 | return release.data;
308 | } catch (error) {
309 | if (error.status !== 404) {
310 | console.log(
311 | `⚠️ Unexpected error fetching GitHub release for tag ${config.github_ref}: ${error}`,
312 | );
313 | throw error;
314 | }
315 |
316 | return await createRelease(
317 | tag,
318 | config,
319 | releaser,
320 | owner,
321 | repo,
322 | discussion_category_name,
323 | generate_release_notes,
324 | maxRetries,
325 | );
326 | }
327 | };
328 |
329 | /**
330 | * Finds a release by tag name from all a repository's releases.
331 | *
332 | * @param releaser - The GitHub API wrapper for release operations
333 | * @param owner - The owner of the repository
334 | * @param repo - The name of the repository
335 | * @param tag - The tag name to search for
336 | * @returns The release with the given tag name, or undefined if no release with that tag name is found
337 | */
338 | export async function findTagFromReleases(
339 | releaser: Releaser,
340 | owner: string,
341 | repo: string,
342 | tag: string,
343 | ): Promise {
344 | for await (const { data: releases } of releaser.allReleases({
345 | owner,
346 | repo,
347 | })) {
348 | const release = releases.find((release) => release.tag_name === tag);
349 | if (release) {
350 | return release;
351 | }
352 | }
353 | return undefined;
354 | }
355 |
356 | async function createRelease(
357 | tag: string,
358 | config: Config,
359 | releaser: Releaser,
360 | owner: string,
361 | repo: string,
362 | discussion_category_name: string | undefined,
363 | generate_release_notes: boolean | undefined,
364 | maxRetries: number,
365 | ) {
366 | const tag_name = tag;
367 | const name = config.input_name || tag;
368 | const body = releaseBody(config);
369 | const draft = config.input_draft;
370 | const prerelease = config.input_prerelease;
371 | const target_commitish = config.input_target_commitish;
372 | const make_latest = config.input_make_latest;
373 | let commitMessage: string = "";
374 | if (target_commitish) {
375 | commitMessage = ` using commit "${target_commitish}"`;
376 | }
377 | console.log(
378 | `👩🏭 Creating new GitHub release for tag ${tag_name}${commitMessage}...`,
379 | );
380 | try {
381 | let release = await releaser.createRelease({
382 | owner,
383 | repo,
384 | tag_name,
385 | name,
386 | body,
387 | draft,
388 | prerelease,
389 | target_commitish,
390 | discussion_category_name,
391 | generate_release_notes,
392 | make_latest,
393 | });
394 | return release.data;
395 | } catch (error) {
396 | // presume a race with competing matrix runs
397 | console.log(`⚠️ GitHub release failed with status: ${error.status}`);
398 | console.log(`${JSON.stringify(error.response.data)}`);
399 |
400 | switch (error.status) {
401 | case 403:
402 | console.log(
403 | "Skip retry — your GitHub token/PAT does not have the required permission to create a release",
404 | );
405 | throw error;
406 |
407 | case 404:
408 | console.log("Skip retry - discussion category mismatch");
409 | throw error;
410 |
411 | case 422:
412 | console.log("Skip retry - validation failed");
413 | throw error;
414 | }
415 |
416 | console.log(`retrying... (${maxRetries - 1} retries remaining)`);
417 | return release(config, releaser, maxRetries - 1);
418 | }
419 | }
420 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import {
2 | paths,
3 | parseConfig,
4 | isTag,
5 | unmatchedPatterns,
6 | uploadUrl,
7 | } from "./util";
8 | import { release, upload, GitHubReleaser } from "./github";
9 | import { getOctokit } from "@actions/github";
10 | import { setFailed, setOutput } from "@actions/core";
11 |
12 | import { env } from "process";
13 |
14 | async function run() {
15 | try {
16 | const config = parseConfig(env);
17 | if (
18 | !config.input_tag_name &&
19 | !isTag(config.github_ref) &&
20 | !config.input_draft
21 | ) {
22 | throw new Error(`⚠️ GitHub Releases requires a tag`);
23 | }
24 | if (config.input_files) {
25 | const patterns = unmatchedPatterns(config.input_files);
26 | patterns.forEach((pattern) => {
27 | if (config.input_fail_on_unmatched_files) {
28 | throw new Error(`⚠️ Pattern '${pattern}' does not match any files.`);
29 | } else {
30 | console.warn(`🤔 Pattern '${pattern}' does not match any files.`);
31 | }
32 | });
33 | if (patterns.length > 0 && config.input_fail_on_unmatched_files) {
34 | throw new Error(`⚠️ There were unmatched files`);
35 | }
36 | }
37 |
38 | // const oktokit = GitHub.plugin(
39 | // require("@octokit/plugin-throttling"),
40 | // require("@octokit/plugin-retry")
41 | // );
42 |
43 | const gh = getOctokit(config.github_token, {
44 | //new oktokit(
45 | throttle: {
46 | onRateLimit: (retryAfter, options) => {
47 | console.warn(
48 | `Request quota exhausted for request ${options.method} ${options.url}`,
49 | );
50 | if (options.request.retryCount === 0) {
51 | // only retries once
52 | console.log(`Retrying after ${retryAfter} seconds!`);
53 | return true;
54 | }
55 | },
56 | onAbuseLimit: (retryAfter, options) => {
57 | // does not retry, only logs a warning
58 | console.warn(
59 | `Abuse detected for request ${options.method} ${options.url}`,
60 | );
61 | },
62 | },
63 | });
64 | //);
65 | const rel = await release(config, new GitHubReleaser(gh));
66 | if (config.input_files && config.input_files.length > 0) {
67 | const files = paths(config.input_files);
68 | if (files.length == 0) {
69 | if (config.input_fail_on_unmatched_files) {
70 | throw new Error(
71 | `⚠️ ${config.input_files} does not include a valid file.`,
72 | );
73 | } else {
74 | console.warn(
75 | `🤔 ${config.input_files} does not include a valid file.`,
76 | );
77 | }
78 | }
79 | const currentAssets = rel.assets;
80 |
81 | const uploadFile = async (path) => {
82 | const json = await upload(
83 | config,
84 | gh,
85 | uploadUrl(rel.upload_url),
86 | path,
87 | currentAssets,
88 | );
89 | delete json.uploader;
90 | return json;
91 | };
92 |
93 | let assets;
94 | if (!config.input_preserve_order) {
95 | assets = await Promise.all(files.map(uploadFile));
96 | } else {
97 | assets = [];
98 | for (const path of files) {
99 | assets.push(await uploadFile(path));
100 | }
101 | }
102 | setOutput("assets", assets);
103 | }
104 | console.log(`🎉 Release ready at ${rel.html_url}`);
105 | setOutput("url", rel.html_url);
106 | setOutput("id", rel.id.toString());
107 | setOutput("upload_url", rel.upload_url);
108 | } catch (error) {
109 | setFailed(error.message);
110 | }
111 | }
112 |
113 | run();
114 |
--------------------------------------------------------------------------------
/src/util.ts:
--------------------------------------------------------------------------------
1 | import * as glob from "glob";
2 | import { statSync, readFileSync } from "fs";
3 |
4 | export interface Config {
5 | github_token: string;
6 | github_ref: string;
7 | github_repository: string;
8 | // user provided
9 | input_name?: string;
10 | input_tag_name?: string;
11 | input_repository?: string;
12 | input_body?: string;
13 | input_body_path?: string;
14 | input_files?: string[];
15 | input_draft?: boolean;
16 | input_preserve_order?: boolean;
17 | input_prerelease?: boolean;
18 | input_fail_on_unmatched_files?: boolean;
19 | input_target_commitish?: string;
20 | input_discussion_category_name?: string;
21 | input_generate_release_notes?: boolean;
22 | input_append_body?: boolean;
23 | input_make_latest: "true" | "false" | "legacy" | undefined;
24 | }
25 |
26 | export const uploadUrl = (url: string): string => {
27 | const templateMarkerPos = url.indexOf("{");
28 | if (templateMarkerPos > -1) {
29 | return url.substring(0, templateMarkerPos);
30 | }
31 | return url;
32 | };
33 |
34 | export const releaseBody = (config: Config): string | undefined => {
35 | return (
36 | (config.input_body_path &&
37 | readFileSync(config.input_body_path).toString("utf8")) ||
38 | config.input_body
39 | );
40 | };
41 |
42 | type Env = { [key: string]: string | undefined };
43 |
44 | export const parseInputFiles = (files: string): string[] => {
45 | return files.split(/\r?\n/).reduce(
46 | (acc, line) =>
47 | acc
48 | .concat(line.split(","))
49 | .filter((pat) => pat)
50 | .map((pat) => pat.trim()),
51 | [],
52 | );
53 | };
54 |
55 | export const parseConfig = (env: Env): Config => {
56 | return {
57 | github_token: env.GITHUB_TOKEN || env.INPUT_TOKEN || "",
58 | github_ref: env.GITHUB_REF || "",
59 | github_repository: env.INPUT_REPOSITORY || env.GITHUB_REPOSITORY || "",
60 | input_name: env.INPUT_NAME,
61 | input_tag_name: env.INPUT_TAG_NAME?.trim(),
62 | input_body: env.INPUT_BODY,
63 | input_body_path: env.INPUT_BODY_PATH,
64 | input_files: parseInputFiles(env.INPUT_FILES || ""),
65 | input_draft: env.INPUT_DRAFT ? env.INPUT_DRAFT === "true" : undefined,
66 | input_preserve_order: env.INPUT_PRESERVE_ORDER
67 | ? env.INPUT_PRESERVE_ORDER == "true"
68 | : undefined,
69 | input_prerelease: env.INPUT_PRERELEASE
70 | ? env.INPUT_PRERELEASE == "true"
71 | : undefined,
72 | input_fail_on_unmatched_files: env.INPUT_FAIL_ON_UNMATCHED_FILES == "true",
73 | input_target_commitish: env.INPUT_TARGET_COMMITISH || undefined,
74 | input_discussion_category_name:
75 | env.INPUT_DISCUSSION_CATEGORY_NAME || undefined,
76 | input_generate_release_notes: env.INPUT_GENERATE_RELEASE_NOTES == "true",
77 | input_append_body: env.INPUT_APPEND_BODY == "true",
78 | input_make_latest: parseMakeLatest(env.INPUT_MAKE_LATEST),
79 | };
80 | };
81 |
82 | const parseMakeLatest = (
83 | value: string | undefined,
84 | ): "true" | "false" | "legacy" | undefined => {
85 | if (value === "true" || value === "false" || value === "legacy") {
86 | return value;
87 | }
88 | return undefined;
89 | };
90 |
91 | export const paths = (patterns: string[]): string[] => {
92 | return patterns.reduce((acc: string[], pattern: string): string[] => {
93 | return acc.concat(
94 | glob.sync(pattern).filter((path) => statSync(path).isFile()),
95 | );
96 | }, []);
97 | };
98 |
99 | export const unmatchedPatterns = (patterns: string[]): string[] => {
100 | return patterns.reduce((acc: string[], pattern: string): string[] => {
101 | return acc.concat(
102 | glob.sync(pattern).filter((path) => statSync(path).isFile()).length == 0
103 | ? [pattern]
104 | : [],
105 | );
106 | }, []);
107 | };
108 |
109 | export const isTag = (ref: string): boolean => {
110 | return ref.startsWith("refs/tags/");
111 | };
112 |
113 | export const alignAssetName = (assetName: string): string => {
114 | return assetName.replace(/ /g, ".");
115 | };
116 |
--------------------------------------------------------------------------------
/tests/data/foo/bar.txt:
--------------------------------------------------------------------------------
1 | release me
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "useUnknownInCatchVariables": false,
4 | /* Basic Options */
5 | // "incremental": true, /* Enable incremental compilation */
6 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
7 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
8 | // "allowJs": true, /* Allow javascript files to be compiled. */
9 | // "checkJs": true, /* Report errors in .js files. */
10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
13 | // "sourceMap": true, /* Generates corresponding '.map' file. */
14 | // "outFile": "./", /* Concatenate and emit output to single file. */
15 | "outDir": "./lib", /* Redirect output structure to the directory. */
16 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
17 | // "composite": true, /* Enable project compilation */
18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
19 | // "removeComments": true, /* Do not emit comments to output. */
20 | // "noEmit": true, /* Do not emit outputs. */
21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
24 |
25 | /* Strict Type-Checking Options */
26 | "strict": true, /* Enable all strict type-checking options. */
27 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
28 | // "strictNullChecks": true, /* Enable strict null checks. */
29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
34 |
35 | /* Additional Checks */
36 | // "noUnusedLocals": true, /* Report errors on unused locals. */
37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
40 |
41 | /* Module Resolution Options */
42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
46 | // "typeRoots": [], /* List of folders to include type definitions from. */
47 | // "types": [], /* Type declaration files to be included in compilation. */
48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
52 |
53 | /* Source Map Options */
54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
58 |
59 | /* Experimental Options */
60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
62 | },
63 | "exclude": ["node_modules", "**/*.test.ts", "jest.config.ts"]
64 | }
65 |
--------------------------------------------------------------------------------