Compare commits

...

44 Commits

Author SHA1 Message Date
Rui Chen
a06a81a03e release 2.5.0
Some checks failed
main / build (push) Has been cancelled
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-12-01 01:02:34 -05:00
jj
7da8983734 feat: mark release as draft until all artifacts are uploaded (#692)
Previously, the releases were created and then artifacts (if any)
were added to them. This broke when GitHub released "immutable"
releases, which disallow changes after the release is published.

Make it so that releases are always marked as "draft" when being
worked on by the action, and unmarked as draft (if desired) once
the action is completed.

Fixes #653
2025-12-01 00:59:24 -05:00
dependabot[bot]
87973286a4 chore(deps): bump actions/checkout in the github-actions group (#689)
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 5.0.0 to 5.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](08c6903cd8...93cb6efe18)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 00:56:27 -05:00
dependabot[bot]
1bfc62a71b chore(deps): bump the npm group across 1 directory with 5 updates (#697)
Bumps the npm group with 4 updates in the / directory: [glob](https://github.com/isaacs/node-glob), [mime-types](https://github.com/jshttp/mime-types), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8).


Updates `glob` from 11.0.3 to 13.0.0
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v11.0.3...v13.0.0)

Updates `mime-types` from 3.0.1 to 3.0.2
- [Release notes](https://github.com/jshttp/mime-types/releases)
- [Changelog](https://github.com/jshttp/mime-types/blob/master/HISTORY.md)
- [Commits](https://github.com/jshttp/mime-types/compare/v3.0.1...v3.0.2)

Updates `@types/node` from 20.19.24 to 20.19.25
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@vitest/coverage-v8` from 4.0.6 to 4.0.13
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.13/packages/coverage-v8)

Updates `vitest` from 4.0.6 to 4.0.13
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.13/packages/vitest)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 13.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm
- dependency-name: mime-types
  dependency-version: 3.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@types/node"
  dependency-version: 20.19.25
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.0.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: vitest
  dependency-version: 4.0.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 00:56:07 -05:00
Rui Chen
5be0e66d93 release 2.4.2
Some checks failed
main / build (push) Has been cancelled
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-11-08 14:21:57 -05:00
Jens L.
af658b4d5d feat: Ensure generated release notes cannot be over 125000 characters (#684)
* Ensure generated release notes cannot be over 125000 characters

* simpler truncate, and always truncate even without generated
2025-11-08 14:20:00 -05:00
Rui Chen
237aaccf71 chore: bump node to 24.11.0
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-11-08 14:18:20 -05:00
dependabot[bot]
00362bea6f chore(deps): bump the npm group with 5 updates (#687)
Some checks failed
main / build (push) Has been cancelled
Bumps the npm group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@octokit/plugin-retry](https://github.com/octokit/plugin-retry.js) | `8.0.2` | `8.0.3` |
| [@octokit/plugin-throttling](https://github.com/octokit/plugin-throttling.js) | `11.0.2` | `11.0.3` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `20.19.23` | `20.19.24` |
| [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) | `4.0.4` | `4.0.6` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.0.4` | `4.0.6` |


Updates `@octokit/plugin-retry` from 8.0.2 to 8.0.3
- [Release notes](https://github.com/octokit/plugin-retry.js/releases)
- [Commits](https://github.com/octokit/plugin-retry.js/compare/v8.0.2...v8.0.3)

Updates `@octokit/plugin-throttling` from 11.0.2 to 11.0.3
- [Release notes](https://github.com/octokit/plugin-throttling.js/releases)
- [Commits](https://github.com/octokit/plugin-throttling.js/compare/v11.0.2...v11.0.3)

Updates `@types/node` from 20.19.23 to 20.19.24
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@vitest/coverage-v8` from 4.0.4 to 4.0.6
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.6/packages/coverage-v8)

Updates `vitest` from 4.0.4 to 4.0.6
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.6/packages/vitest)

---
updated-dependencies:
- dependency-name: "@octokit/plugin-retry"
  dependency-version: 8.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@octokit/plugin-throttling"
  dependency-version: 11.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@types/node"
  dependency-version: 20.19.24
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.0.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: vitest
  dependency-version: 4.0.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-07 21:47:12 -05:00
dependabot[bot]
0adea5aa98 chore(deps): bump the npm group with 3 updates (#686)
Some checks failed
main / build (push) Has been cancelled
Bumps the npm group with 3 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `@types/node` from 20.19.22 to 20.19.23
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@vitest/coverage-v8` from 3.2.4 to 4.0.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.4/packages/coverage-v8)

Updates `vitest` from 3.2.4 to 4.0.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.4/packages/vitest)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 20.19.23
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.0.4
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm
- dependency-name: vitest
  dependency-version: 4.0.4
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-30 14:36:32 -04:00
dependabot[bot]
aa05f9d779 chore(deps): bump actions/setup-node from 5.0.0 to 6.0.0 in the github-actions group (#683)
Some checks failed
main / build (push) Has been cancelled
* chore(deps): bump actions/setup-node in the github-actions group

Bumps the github-actions group with 1 update: [actions/setup-node](https://github.com/actions/setup-node).


Updates `actions/setup-node` from 5.0.0 to 6.0.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](a0853c2454...2028fbc5c2)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>

* update comment

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rui Chen <rui@chenrui.dev>
2025-10-23 22:23:58 -04:00
dependabot[bot]
bbaccb3a0c chore(deps): bump @types/node from 20.19.21 to 20.19.22 in the npm group (#682)
Bumps the npm group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 20.19.21 to 20.19.22
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 20.19.22
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-23 22:22:08 -04:00
dependabot[bot]
50fda3f773 chore(deps): bump vite from 7.1.5 to 7.1.11 (#681)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.5 to 7.1.11.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.11/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-23 22:21:57 -04:00
dependabot[bot]
5434409c2b chore(deps): bump @types/node from 20.19.19 to 20.19.21 in the npm group (#679)
Some checks failed
main / build (push) Has been cancelled
Bumps the npm group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 20.19.19 to 20.19.21
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 20.19.21
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-16 18:49:13 -04:00
Rui Chen
6da8fa9354 release 2.4.1
Some checks failed
main / build (push) Has been cancelled
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-10-11 11:40:52 -04:00
Copilot
f38efdea4c fix: gracefully fallback to body when body_path cannot be read (#671)
Some checks failed
main / build (push) Has been cancelled
* Initial plan

* fix: gracefully fallback to body when body_path cannot be read

Co-authored-by: chenrui333 <1580956+chenrui333@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: chenrui333 <1580956+chenrui333@users.noreply.github.com>
2025-10-06 23:50:32 -04:00
Copilot
cec1a1113b fix(util): support brace expansion globs containing commas in parseInputFiles (#672)
* Initial plan

* fix(util): support brace expansion globs containing commas in parseInputFiles

Co-authored-by: chenrui333 <1580956+chenrui333@users.noreply.github.com>

* test(util): add comprehensive edge case coverage for brace expansion parsing

Co-authored-by: chenrui333 <1580956+chenrui333@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: chenrui333 <1580956+chenrui333@users.noreply.github.com>
2025-10-06 23:50:00 -04:00
Rui Chen
aec2ec56f9 release 2.4.0
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-10-06 22:53:32 -04:00
Stephen Way
4db716b167 feat: respect working_directory for files globs; add input and tests (#667) 2025-10-06 22:51:45 -04:00
dependabot[bot]
14820f2cee chore(deps): bump the npm group with 2 updates (#668)
Bumps the npm group with 2 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [typescript](https://github.com/microsoft/TypeScript).


Updates `@types/node` from 20.19.18 to 20.19.19
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `typescript` from 5.9.2 to 5.9.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.2...v5.9.3)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 20.19.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript
  dependency-version: 5.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 22:48:36 -04:00
Rui Chen
62c96d0c4e release 2.3.4
Some checks failed
main / build (push) Has been cancelled
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-10-03 14:45:26 -04:00
Stephen Way
7dc9b8ac0f fix(action): handle 422 already_exists race condition (#665)
- Add retry logic for 422 'already_exists' errors in race conditions
- Allow action to find and update existing releases instead of failing
- Add test to verify race condition handling works correctly
- Fixes regression that broke matrix workflows in v2.2.2+

closes #616
2025-10-03 14:34:31 -04:00
dependabot[bot]
0f0e0b98e9 chore(deps): bump the npm group with 3 updates (#666)
Bumps the npm group with 3 updates: [@octokit/plugin-retry](https://github.com/octokit/plugin-retry.js), [@octokit/plugin-throttling](https://github.com/octokit/plugin-throttling.js) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@octokit/plugin-retry` from 8.0.1 to 8.0.2
- [Release notes](https://github.com/octokit/plugin-retry.js/releases)
- [Commits](https://github.com/octokit/plugin-retry.js/compare/v8.0.1...v8.0.2)

Updates `@octokit/plugin-throttling` from 11.0.1 to 11.0.2
- [Release notes](https://github.com/octokit/plugin-throttling.js/releases)
- [Commits](https://github.com/octokit/plugin-throttling.js/compare/v11.0.1...v11.0.2)

Updates `@types/node` from 20.19.17 to 20.19.18
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@octokit/plugin-retry"
  dependency-version: 8.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@octokit/plugin-throttling"
  dependency-version: 11.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@types/node"
  dependency-version: 20.19.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-03 14:31:53 -04:00
dependabot[bot]
97d42c1b50 chore(deps): bump the npm group across 1 directory with 2 updates (#662)
Some checks failed
main / build (push) Has been cancelled
Bumps the npm group with 2 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [@vercel/ncc](https://github.com/vercel/ncc).


Updates `@types/node` from 20.19.13 to 20.19.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@vercel/ncc` from 0.38.3 to 0.38.4
- [Release notes](https://github.com/vercel/ncc/releases)
- [Commits](https://github.com/vercel/ncc/compare/0.38.3...0.38.4)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 20.19.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@vercel/ncc"
  dependency-version: 0.38.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-18 23:26:39 -04:00
dependabot[bot]
19cd0bcd2b chore(deps): bump vite from 7.0.0 to 7.1.5 (#657)
Some checks failed
main / build (push) Has been cancelled
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.0.0 to 7.1.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-10 22:41:43 -04:00
dependabot[bot]
5d1b0b1164 chore(deps): bump @types/node from 20.19.11 to 20.19.13 in the npm group (#655)
Some checks failed
main / build (push) Has been cancelled
Bumps the npm group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 20.19.11 to 20.19.13
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 20.19.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 23:36:06 -04:00
dependabot[bot]
f6021cf9a4 chore(deps): bump actions/setup-node in the github-actions group (#656)
Bumps the github-actions group with 1 update: [actions/setup-node](https://github.com/actions/setup-node).


Updates `actions/setup-node` from 4.4.0 to 5.0.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](49933ea528...a0853c2454)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 23:35:59 -04:00
Rui Chen
6cbd405e2c release 2.3.3
Some checks failed
main / build (push) Has been cancelled
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-09-07 00:36:40 -04:00
Rui Chen
fbadcc90e8 update to use actions/checkout@v5
Some checks failed
main / build (push) Has been cancelled
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-08-23 16:43:38 -04:00
dependabot[bot]
4a840061c4 chore(deps): bump @types/node from 20.19.10 to 20.19.11 in the npm group (#648)
Bumps the npm group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 20.19.10 to 20.19.11
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 20.19.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-23 16:37:50 -04:00
dependabot[bot]
7191749478 chore(deps): bump actions/checkout in the github-actions group (#649)
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 4.2.2 to 5.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](11bd71901b...08c6903cd8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-23 16:37:44 -04:00
dependabot[bot]
126b1e7093 chore(deps): bump @types/node from 20.19.9 to 20.19.10 in the npm group (#647)
Some checks failed
main / build (push) Has been cancelled
Bumps the npm group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 20.19.9 to 20.19.10
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 20.19.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-17 22:22:51 -04:00
dependabot[bot]
f82d31e53e chore(deps): bump the npm group with 3 updates (#643)
Some checks failed
main / build (push) Has been cancelled
Bumps the npm group with 3 updates: [@types/glob](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/glob), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [typescript](https://github.com/microsoft/TypeScript).


Updates `@types/glob` from 8.1.0 to 9.0.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/glob)

Updates `@types/node` from 20.19.7 to 20.19.9
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `typescript` from 5.8.3 to 5.9.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.8.3...v5.9.2)

---
updated-dependencies:
- dependency-name: "@types/glob"
  dependency-version: 9.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm
- dependency-name: "@types/node"
  dependency-version: 20.19.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript
  dependency-version: 5.9.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 13:14:01 -04:00
dependabot[bot]
f2352b97da chore(deps): bump @types/node from 20.19.2 to 20.19.7 in the npm group (#640)
Some checks failed
main / build (push) Has been cancelled
Bumps the npm group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 20.19.2 to 20.19.7
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 20.19.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-19 13:33:52 +08:00
dependabot[bot]
f0b3259de2 chore(deps): bump the npm group across 1 directory with 4 updates (#638)
Some checks failed
main / build (push) Has been cancelled
Bumps the npm group with 3 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) and [prettier](https://github.com/prettier/prettier).


Updates `@types/node` from 20.19.1 to 20.19.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@vitest/coverage-v8` from 3.2.3 to 3.2.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.4/packages/coverage-v8)

Updates `prettier` from 3.5.3 to 3.6.2
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.5.3...3.6.2)

Updates `vitest` from 3.2.3 to 3.2.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.4/packages/vitest)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 20.19.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 3.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: prettier
  dependency-version: 3.6.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: vitest
  dependency-version: 3.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-10 21:36:54 +08:00
dependabot[bot]
f37a2f9143 chore(deps): bump the npm group with 2 updates (#635)
Some checks failed
main / build (push) Has been cancelled
Bumps the npm group with 2 updates: [glob](https://github.com/isaacs/node-glob) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `glob` from 11.0.2 to 11.0.3
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v11.0.2...v11.0.3)

Updates `@types/node` from 20.19.0 to 20.19.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 11.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@types/node"
  dependency-version: 20.19.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 14:36:57 -04:00
dependabot[bot]
db560141c6 chore(deps): bump brace-expansion from 2.0.1 to 2.0.2 (#634)
Some checks failed
main / build (push) Has been cancelled
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 2.0.1 to 2.0.2.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v2.0.1...v2.0.2)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 2.0.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-11 19:04:49 -04:00
Rui Chen
40521a2029 chore: update dist/index.js
Some checks failed
main / build (push) Has been cancelled
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-06-11 11:35:37 -04:00
Rui Chen
c8d8e91662 chore: add prettier config (#633)
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-06-11 11:34:43 -04:00
Adriano dos Santos Fernandes
605f567f95 feat: add input option overwrite_files. (#343)
Some checks failed
main / build (push) Has been cancelled
* Add input option overwrite_files.

* Fix description.

* update test and run fmt/build

Signed-off-by: Rui Chen <rui@chenrui.dev>

---------

Signed-off-by: Rui Chen <rui@chenrui.dev>
Co-authored-by: Rui Chen <rui@chenrui.dev>
2025-06-11 01:54:42 -04:00
Rui Chen
5822334cb4 chore: swap node assert with vitest assert
Some checks failed
main / build (push) Has been cancelled
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-06-10 21:02:08 -04:00
Rui Chen
72f2c25fcb release 2.3.2
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-06-10 18:33:08 -04:00
Rui Chen
552dc5524b fix: revert fs:readableWebStream change (#632)
* Revert "fix: fix file closing issue (#629)"

This reverts commit 07a2257003.

* fix: revert `fh.readableWebStream` change

Signed-off-by: Rui Chen <rui@chenrui.dev>

---------

Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-06-10 18:27:48 -04:00
Rui Chen
f3cad8bcbf release 2.3.1
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-06-10 17:15:05 -04:00
Roland Dominik Szilágyi
07a2257003 fix: fix file closing issue (#629)
* fix: handle file closing gracefully

* fix: failing test on Windows

* format code using npm run fmt
2025-06-10 17:12:05 -04:00
16 changed files with 1354 additions and 1557 deletions

View File

@@ -12,13 +12,9 @@ updates:
- dependency-name: node-fetch
versions:
- ">=3.0.0"
# ignore mime and @types/mime per https://github.com/softprops/action-gh-release/pull/475
- dependency-name: mime
- dependency-name: "@types/node"
versions:
- ">=4.0.0"
- dependency-name: "@types/mime"
versions:
- ">=4.0.0"
- ">=22.0.0"
commit-message:
prefix: "chore(deps)"
- package-ecosystem: github-actions

View File

@@ -8,9 +8,9 @@ jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
node-version-file: ".tool-versions"
cache: "npm"

16
.prettierignore Normal file
View File

@@ -0,0 +1,16 @@
# Build outputs
dist/
lib/
coverage/
# Dependencies
node_modules/
# Misc
.github/
*.log
.DS_Store
__tests__/release.txt
# Package files
package-lock.json

11
.prettierrc.js Normal file
View File

@@ -0,0 +1,11 @@
/**
* @type {import('prettier').Config}
*/
module.exports = {
trailingComma: 'all',
tabWidth: 2,
semi: true,
singleQuote: true,
printWidth: 100,
bracketSpacing: true,
};

View File

@@ -1 +1 @@
nodejs 24.2.0
nodejs 24.11.0

View File

@@ -1,3 +1,78 @@
## 2.5.0
## What's Changed
### Exciting New Features 🎉
* feat: mark release as draft until all artifacts are uploaded by @dumbmoron in https://github.com/softprops/action-gh-release/pull/692
### Other Changes 🔄
* dependency updates
## 2.4.2
## What's Changed
### Exciting New Features 🎉
* feat: Ensure generated release notes cannot be over 125000 characters by @BeryJu in https://github.com/softprops/action-gh-release/pull/684
### Other Changes 🔄
* dependency updates
## 2.4.1
## What's Changed
### Other Changes 🔄
* fix(util): support brace expansion globs containing commas in parseInputFiles by @Copilot in https://github.com/softprops/action-gh-release/pull/672
* fix: gracefully fallback to body when body_path cannot be read by @Copilot in https://github.com/softprops/action-gh-release/pull/671
## 2.4.0
## What's Changed
### Exciting New Features 🎉
* feat(action): respect working_directory for files globs by @stephenway in https://github.com/softprops/action-gh-release/pull/667
## 2.3.4
## What's Changed
### Bug fixes 🐛
* fix(action): handle 422 already_exists race condition by @stephenway in https://github.com/softprops/action-gh-release/pull/665
### Other Changes 🔄
- dependency updates
## 2.3.3
## What's Changed
### Exciting New Features 🎉
* feat: add input option `overwrite_files` by @asfernandes in https://github.com/softprops/action-gh-release/pull/343
### Other Changes 🔄
- dependency updates
## 2.3.2
* fix: revert fs `readableWebStream` change
## 2.3.1
### Bug fixes 🐛
* fix: fix file closing issue by @WailGree in https://github.com/softprops/action-gh-release/pull/629
## 2.3.0
* Migrate from jest to vitest

View File

@@ -51,7 +51,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Release
uses: softprops/action-gh-release@v2
if: github.ref_type == 'tag'
@@ -72,7 +72,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Release
uses: softprops/action-gh-release@v2
```
@@ -99,7 +99,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Build
run: echo ${{ github.sha }} > Release.txt
- name: Test
@@ -123,7 +123,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Build
run: echo ${{ github.sha }} > Release.txt
- name: Test
@@ -157,7 +157,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Generate Changelog
run: echo "# Good things have arrived" > ${{ github.workspace }}-CHANGELOG.txt
- name: Release
@@ -185,6 +185,7 @@ The following are optional as `step.with` keys
| `prerelease` | Boolean | Indicator of whether or not is a prerelease |
| `preserve_order` | Boolean | Indicator of whether order of files should be preserved when uploading assets |
| `files` | String | Newline-delimited globs of paths to assets to upload for release |
| `overwrite_files` | Boolean | Indicator of whether files should be overwritten when they already exist. Defaults to true |
| `name` | String | Name of the release. defaults to tag name |
| `tag_name` | String | Name of a tag. defaults to `github.ref_name` |
| `fail_on_unmatched_files` | Boolean | Indicator of whether to fail if any of the `files` globs match nothing |

View File

@@ -1,63 +1,64 @@
import * as assert from "assert";
import {
mimeOrDefault,
asset,
Releaser,
Release,
findTagFromReleases,
} from "../src/github";
mimeOrDefault,
release,
Release,
Releaser,
} from '../src/github';
import { describe, it } from "vitest";
import { assert, describe, it } from 'vitest';
describe("github", () => {
describe("mimeOrDefault", () => {
it("returns a specific mime for common path", async () => {
assert.equal(mimeOrDefault("foo.tar.gz"), "application/gzip");
describe('github', () => {
describe('mimeOrDefault', () => {
it('returns a specific mime for common path', async () => {
assert.equal(mimeOrDefault('foo.tar.gz'), 'application/gzip');
});
it("returns default mime for uncommon path", async () => {
assert.equal(mimeOrDefault("foo.uncommon"), "application/octet-stream");
it('returns default mime for uncommon path', async () => {
assert.equal(mimeOrDefault('foo.uncommon'), 'application/octet-stream');
});
});
describe("asset", () => {
it("derives asset info from a path", async () => {
const { name, mime, size } = asset("tests/data/foo/bar.txt");
assert.equal(name, "bar.txt");
assert.equal(mime, "text/plain");
describe('asset', () => {
it('derives asset info from a path', async () => {
const { name, mime, size } = asset('tests/data/foo/bar.txt');
assert.equal(name, 'bar.txt');
assert.equal(mime, 'text/plain');
assert.equal(size, 10);
});
});
describe("findTagFromReleases", () => {
const owner = "owner";
const repo = "repo";
describe('findTagFromReleases', () => {
const owner = 'owner';
const repo = 'repo';
const mockRelease: Release = {
id: 1,
upload_url: `https://api.github.com/repos/${owner}/${repo}/releases/1/assets`,
html_url: `https://github.com/${owner}/${repo}/releases/tag/v1.0.0`,
tag_name: "v1.0.0",
name: "Test Release",
body: "Test body",
target_commitish: "main",
tag_name: 'v1.0.0',
name: 'Test Release',
body: 'Test body',
target_commitish: 'main',
draft: false,
prerelease: false,
assets: [],
} as const;
const mockReleaser: Releaser = {
getReleaseByTag: () => Promise.reject("Not implemented"),
createRelease: () => Promise.reject("Not implemented"),
updateRelease: () => Promise.reject("Not implemented"),
getReleaseByTag: () => Promise.reject('Not implemented'),
createRelease: () => Promise.reject('Not implemented'),
updateRelease: () => Promise.reject('Not implemented'),
finalizeRelease: () => Promise.reject('Not implemented'),
allReleases: async function* () {
yield { data: [mockRelease] };
},
} as const;
describe("when the tag_name is not an empty string", () => {
const targetTag = "v1.0.0";
describe('when the tag_name is not an empty string', () => {
const targetTag = 'v1.0.0';
it("finds a matching release in first batch of results", async () => {
it('finds a matching release in first batch of results', async () => {
const targetRelease = {
...mockRelease,
owner,
@@ -68,7 +69,7 @@ describe("github", () => {
...mockRelease,
owner,
repo,
tag_name: "v1.0.1",
tag_name: 'v1.0.1',
};
const releaser = {
@@ -79,17 +80,12 @@ describe("github", () => {
},
};
const result = await findTagFromReleases(
releaser,
owner,
repo,
targetTag,
);
const result = await findTagFromReleases(releaser, owner, repo, targetTag);
assert.deepStrictEqual(result, targetRelease);
});
it("finds a matching release in second batch of results", async () => {
it('finds a matching release in second batch of results', async () => {
const targetRelease = {
...mockRelease,
owner,
@@ -100,7 +96,7 @@ describe("github", () => {
...mockRelease,
owner,
repo,
tag_name: "v1.0.1",
tag_name: 'v1.0.1',
};
const releaser = {
@@ -111,21 +107,16 @@ describe("github", () => {
},
};
const result = await findTagFromReleases(
releaser,
owner,
repo,
targetTag,
);
const result = await findTagFromReleases(releaser, owner, repo, targetTag);
assert.deepStrictEqual(result, targetRelease);
});
it("returns undefined when a release is not found in any batch", async () => {
it('returns undefined when a release is not found in any batch', async () => {
const otherRelease = {
...mockRelease,
owner,
repo,
tag_name: "v1.0.1",
tag_name: 'v1.0.1',
};
const releaser = {
...mockReleaser,
@@ -135,17 +126,12 @@ describe("github", () => {
},
};
const result = await findTagFromReleases(
releaser,
owner,
repo,
targetTag,
);
const result = await findTagFromReleases(releaser, owner, repo, targetTag);
assert.strictEqual(result, undefined);
});
it("returns undefined when no releases are returned", async () => {
it('returns undefined when no releases are returned', async () => {
const releaser = {
...mockReleaser,
allReleases: async function* () {
@@ -153,21 +139,16 @@ describe("github", () => {
},
};
const result = await findTagFromReleases(
releaser,
owner,
repo,
targetTag,
);
const result = await findTagFromReleases(releaser, owner, repo, targetTag);
assert.strictEqual(result, undefined);
});
});
describe("when the tag_name is an empty string", () => {
const emptyTag = "";
describe('when the tag_name is an empty string', () => {
const emptyTag = '';
it("finds a matching release in first batch of results", async () => {
it('finds a matching release in first batch of results', async () => {
const targetRelease = {
...mockRelease,
owner,
@@ -178,7 +159,7 @@ describe("github", () => {
...mockRelease,
owner,
repo,
tag_name: "v1.0.1",
tag_name: 'v1.0.1',
};
const releaser = {
@@ -189,17 +170,12 @@ describe("github", () => {
},
};
const result = await findTagFromReleases(
releaser,
owner,
repo,
emptyTag,
);
const result = await findTagFromReleases(releaser, owner, repo, emptyTag);
assert.deepStrictEqual(result, targetRelease);
});
it("finds a matching release in second batch of results", async () => {
it('finds a matching release in second batch of results', async () => {
const targetRelease = {
...mockRelease,
owner,
@@ -210,7 +186,7 @@ describe("github", () => {
...mockRelease,
owner,
repo,
tag_name: "v1.0.1",
tag_name: 'v1.0.1',
};
const releaser = {
@@ -221,21 +197,16 @@ describe("github", () => {
},
};
const result = await findTagFromReleases(
releaser,
owner,
repo,
emptyTag,
);
const result = await findTagFromReleases(releaser, owner, repo, emptyTag);
assert.deepStrictEqual(result, targetRelease);
});
it("returns undefined when a release is not found in any batch", async () => {
it('returns undefined when a release is not found in any batch', async () => {
const otherRelease = {
...mockRelease,
owner,
repo,
tag_name: "v1.0.1",
tag_name: 'v1.0.1',
};
const releaser = {
...mockReleaser,
@@ -245,17 +216,12 @@ describe("github", () => {
},
};
const result = await findTagFromReleases(
releaser,
owner,
repo,
emptyTag,
);
const result = await findTagFromReleases(releaser, owner, repo, emptyTag);
assert.strictEqual(result, undefined);
});
it("returns undefined when no releases are returned", async () => {
it('returns undefined when no releases are returned', async () => {
const releaser = {
...mockReleaser,
allReleases: async function* () {
@@ -263,15 +229,82 @@ describe("github", () => {
},
};
const result = await findTagFromReleases(
releaser,
owner,
repo,
emptyTag,
);
const result = await findTagFromReleases(releaser, owner, repo, emptyTag);
assert.strictEqual(result, undefined);
});
});
});
describe('error handling', () => {
it('handles 422 already_exists error gracefully', async () => {
const mockReleaser: Releaser = {
getReleaseByTag: () => Promise.reject('Not implemented'),
createRelease: () =>
Promise.reject({
status: 422,
response: { data: { errors: [{ code: 'already_exists' }] } },
}),
updateRelease: () =>
Promise.resolve({
data: {
id: 1,
upload_url: 'test',
html_url: 'test',
tag_name: 'v1.0.0',
name: 'test',
body: 'test',
target_commitish: 'main',
draft: true,
prerelease: false,
assets: [],
},
}),
finalizeRelease: async () => {},
allReleases: async function* () {
yield {
data: [
{
id: 1,
upload_url: 'test',
html_url: 'test',
tag_name: 'v1.0.0',
name: 'test',
body: 'test',
target_commitish: 'main',
draft: false,
prerelease: false,
assets: [],
},
],
};
},
} as const;
const config = {
github_token: 'test-token',
github_ref: 'refs/tags/v1.0.0',
github_repository: 'owner/repo',
input_tag_name: undefined,
input_name: undefined,
input_body: undefined,
input_body_path: undefined,
input_files: [],
input_draft: undefined,
input_prerelease: undefined,
input_preserve_order: undefined,
input_overwrite_files: undefined,
input_fail_on_unmatched_files: false,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_append_body: false,
input_make_latest: undefined,
};
const result = await release(config, mockReleaser, 1);
assert.ok(result);
assert.equal(result.id, 1);
});
});
});

View File

@@ -1,56 +1,72 @@
import {
releaseBody,
alignAssetName,
isTag,
paths,
parseConfig,
parseInputFiles,
paths,
releaseBody,
unmatchedPatterns,
uploadUrl,
alignAssetName,
} from "../src/util";
import * as assert from "assert";
} from '../src/util';
import { describe, it, expect } from "vitest";
import { assert, describe, expect, it } from 'vitest';
describe("util", () => {
describe("uploadUrl", () => {
it("strips template", () => {
describe('util', () => {
describe('uploadUrl', () => {
it('strips template', () => {
assert.equal(
uploadUrl(
"https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}",
'https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}',
),
"https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets",
'https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets',
);
});
});
describe("parseInputFiles", () => {
it("parses empty strings", () => {
assert.deepStrictEqual(parseInputFiles(""), []);
describe('parseInputFiles', () => {
it('parses empty strings', () => {
assert.deepStrictEqual(parseInputFiles(''), []);
});
it("parses comma-delimited strings", () => {
assert.deepStrictEqual(parseInputFiles("foo,bar"), ["foo", "bar"]);
it('parses comma-delimited strings', () => {
assert.deepStrictEqual(parseInputFiles('foo,bar'), ['foo', 'bar']);
});
it("parses newline and comma-delimited (and then some)", () => {
assert.deepStrictEqual(
parseInputFiles("foo,bar\nbaz,boom,\n\ndoom,loom "),
["foo", "bar", "baz", "boom", "doom", "loom"],
);
it('parses newline and comma-delimited (and then some)', () => {
assert.deepStrictEqual(parseInputFiles('foo,bar\nbaz,boom,\n\ndoom,loom '), [
'foo',
'bar',
'baz',
'boom',
'doom',
'loom',
]);
});
it('handles globs with brace groups containing commas', () => {
assert.deepStrictEqual(parseInputFiles('./**/*.{exe,deb,tar.gz}\nfoo,bar'), [
'./**/*.{exe,deb,tar.gz}',
'foo',
'bar',
]);
});
it('handles single-line brace pattern correctly', () => {
assert.deepStrictEqual(parseInputFiles('./**/*.{exe,deb,tar.gz}'), [
'./**/*.{exe,deb,tar.gz}',
]);
});
});
describe("releaseBody", () => {
it("uses input body", () => {
describe('releaseBody', () => {
it('uses input body', () => {
assert.equal(
"foo",
'foo',
releaseBody({
github_ref: "",
github_repository: "",
github_token: "",
input_body: "foo",
github_ref: '',
github_repository: '',
github_token: '',
input_body: 'foo',
input_body_path: undefined,
input_draft: false,
input_prerelease: false,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: undefined,
input_name: undefined,
input_tag_name: undefined,
input_target_commitish: undefined,
@@ -60,19 +76,20 @@ describe("util", () => {
}),
);
});
it("uses input body path", () => {
it('uses input body path', () => {
assert.equal(
"bar",
'bar',
releaseBody({
github_ref: "",
github_repository: "",
github_token: "",
github_ref: '',
github_repository: '',
github_token: '',
input_body: undefined,
input_body_path: "__tests__/release.txt",
input_body_path: '__tests__/release.txt',
input_draft: false,
input_prerelease: false,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: undefined,
input_name: undefined,
input_tag_name: undefined,
input_target_commitish: undefined,
@@ -82,19 +99,20 @@ describe("util", () => {
}),
);
});
it("defaults to body path when both body and body path are provided", () => {
it('defaults to body path when both body and body path are provided', () => {
assert.equal(
"bar",
'bar',
releaseBody({
github_ref: "",
github_repository: "",
github_token: "",
input_body: "foo",
input_body_path: "__tests__/release.txt",
github_ref: '',
github_repository: '',
github_token: '',
input_body: 'foo',
input_body_path: '__tests__/release.txt',
input_draft: false,
input_prerelease: false,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: undefined,
input_name: undefined,
input_tag_name: undefined,
input_target_commitish: undefined,
@@ -104,9 +122,55 @@ describe("util", () => {
}),
);
});
it('falls back to body when body_path is missing', () => {
assert.equal(
releaseBody({
github_ref: '',
github_repository: '',
github_token: '',
input_body: 'fallback-body',
input_body_path: '__tests__/does-not-exist.txt',
input_draft: false,
input_prerelease: false,
input_files: [],
input_overwrite_files: undefined,
input_preserve_order: undefined,
input_name: undefined,
input_tag_name: undefined,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_make_latest: undefined,
}),
'fallback-body',
);
});
it('returns undefined when body_path is missing and body is not provided', () => {
assert.equal(
releaseBody({
github_ref: '',
github_repository: '',
github_token: '',
input_body: undefined,
input_body_path: '__tests__/does-not-exist.txt',
input_draft: false,
input_prerelease: false,
input_files: [],
input_overwrite_files: undefined,
input_preserve_order: undefined,
input_name: undefined,
input_tag_name: undefined,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_make_latest: undefined,
}),
undefined,
);
});
});
describe("parseConfig", () => {
it("parses basic config", () => {
describe('parseConfig', () => {
it('parses basic config', () => {
assert.deepStrictEqual(
parseConfig({
// note: inputs declared in actions.yml, even when declared not required,
@@ -115,13 +179,14 @@ describe("util", () => {
// as an empty string !== undefined in terms of what we pass to the api
// so we cover that in a test case here to ensure undefined values are actually
// resolved as undefined and not empty strings
INPUT_TARGET_COMMITISH: "",
INPUT_DISCUSSION_CATEGORY_NAME: "",
INPUT_TARGET_COMMITISH: '',
INPUT_DISCUSSION_CATEGORY_NAME: '',
}),
{
github_ref: "",
github_repository: "",
github_token: "",
github_ref: '',
github_repository: '',
github_token: '',
input_working_directory: undefined,
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
@@ -129,6 +194,7 @@ describe("util", () => {
input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: undefined,
input_name: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
@@ -140,41 +206,44 @@ describe("util", () => {
);
});
it("parses basic config with commitish", () => {
it('parses basic config with commitish', () => {
assert.deepStrictEqual(
parseConfig({
INPUT_TARGET_COMMITISH: "affa18ef97bc9db20076945705aba8c516139abd",
INPUT_TARGET_COMMITISH: 'affa18ef97bc9db20076945705aba8c516139abd',
}),
{
github_ref: "",
github_repository: "",
github_token: "",
github_ref: '',
github_repository: '',
github_token: '',
input_working_directory: undefined,
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
input_draft: undefined,
input_prerelease: undefined,
input_files: [],
input_overwrite_files: undefined,
input_preserve_order: undefined,
input_name: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
input_target_commitish: "affa18ef97bc9db20076945705aba8c516139abd",
input_target_commitish: 'affa18ef97bc9db20076945705aba8c516139abd',
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_make_latest: undefined,
},
);
});
it("supports discussion category names", () => {
it('supports discussion category names', () => {
assert.deepStrictEqual(
parseConfig({
INPUT_DISCUSSION_CATEGORY_NAME: "releases",
INPUT_DISCUSSION_CATEGORY_NAME: 'releases',
}),
{
github_ref: "",
github_repository: "",
github_token: "",
github_ref: '',
github_repository: '',
github_token: '',
input_working_directory: undefined,
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
@@ -183,25 +252,27 @@ describe("util", () => {
input_files: [],
input_preserve_order: undefined,
input_name: undefined,
input_overwrite_files: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
input_target_commitish: undefined,
input_discussion_category_name: "releases",
input_discussion_category_name: 'releases',
input_generate_release_notes: false,
input_make_latest: undefined,
},
);
});
it("supports generating release notes", () => {
it('supports generating release notes', () => {
assert.deepStrictEqual(
parseConfig({
INPUT_GENERATE_RELEASE_NOTES: "true",
INPUT_GENERATE_RELEASE_NOTES: 'true',
}),
{
github_ref: "",
github_repository: "",
github_token: "",
github_ref: '',
github_repository: '',
github_token: '',
input_working_directory: undefined,
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
@@ -209,6 +280,7 @@ describe("util", () => {
input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: undefined,
input_name: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
@@ -220,19 +292,20 @@ describe("util", () => {
);
});
it("prefers GITHUB_TOKEN over token input for backwards compatibility", () => {
it('prefers GITHUB_TOKEN over token input for backwards compatibility', () => {
assert.deepStrictEqual(
parseConfig({
INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true",
INPUT_PRESERVE_ORDER: "true",
GITHUB_TOKEN: "env-token",
INPUT_TOKEN: "input-token",
INPUT_DRAFT: 'false',
INPUT_PRERELEASE: 'true',
INPUT_PRESERVE_ORDER: 'true',
GITHUB_TOKEN: 'env-token',
INPUT_TOKEN: 'input-token',
}),
{
github_ref: "",
github_repository: "",
github_token: "env-token",
github_ref: '',
github_repository: '',
github_token: 'env-token',
input_working_directory: undefined,
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
@@ -240,6 +313,7 @@ describe("util", () => {
input_prerelease: true,
input_preserve_order: true,
input_files: [],
input_overwrite_files: undefined,
input_name: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
@@ -250,17 +324,18 @@ describe("util", () => {
},
);
});
it("uses input token as the source of GITHUB_TOKEN by default", () => {
it('uses input token as the source of GITHUB_TOKEN by default', () => {
assert.deepStrictEqual(
parseConfig({
INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true",
INPUT_TOKEN: "input-token",
INPUT_DRAFT: 'false',
INPUT_PRERELEASE: 'true',
INPUT_TOKEN: 'input-token',
}),
{
github_ref: "",
github_repository: "",
github_token: "input-token",
github_ref: '',
github_repository: '',
github_token: 'input-token',
input_working_directory: undefined,
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
@@ -268,6 +343,7 @@ describe("util", () => {
input_prerelease: true,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: undefined,
input_name: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
@@ -278,16 +354,17 @@ describe("util", () => {
},
);
});
it("parses basic config with draft and prerelease", () => {
it('parses basic config with draft and prerelease', () => {
assert.deepStrictEqual(
parseConfig({
INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true",
INPUT_DRAFT: 'false',
INPUT_PRERELEASE: 'true',
}),
{
github_ref: "",
github_repository: "",
github_token: "",
github_ref: '',
github_repository: '',
github_token: '',
input_working_directory: undefined,
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
@@ -295,6 +372,7 @@ describe("util", () => {
input_prerelease: true,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: undefined,
input_name: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
@@ -305,15 +383,16 @@ describe("util", () => {
},
);
});
it("parses basic config where make_latest is passed", () => {
it('parses basic config where make_latest is passed', () => {
assert.deepStrictEqual(
parseConfig({
INPUT_MAKE_LATEST: "false",
INPUT_MAKE_LATEST: 'false',
}),
{
github_ref: "",
github_repository: "",
github_token: "",
github_ref: '',
github_repository: '',
github_token: '',
input_working_directory: undefined,
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
@@ -322,24 +401,26 @@ describe("util", () => {
input_preserve_order: undefined,
input_files: [],
input_name: undefined,
input_overwrite_files: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_make_latest: "false",
input_make_latest: 'false',
},
);
});
it("parses basic config with append_body", () => {
it('parses basic config with append_body', () => {
assert.deepStrictEqual(
parseConfig({
INPUT_APPEND_BODY: "true",
INPUT_APPEND_BODY: 'true',
}),
{
github_ref: "",
github_repository: "",
github_token: "",
github_ref: '',
github_repository: '',
github_token: '',
input_working_directory: undefined,
input_append_body: true,
input_body: undefined,
input_body_path: undefined,
@@ -347,6 +428,7 @@ describe("util", () => {
input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [],
input_overwrite_files: undefined,
input_name: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
@@ -358,46 +440,86 @@ describe("util", () => {
);
});
});
describe("isTag", () => {
it("returns true for tags", async () => {
assert.equal(isTag("refs/tags/foo"), true);
describe('isTag', () => {
it('returns true for tags', async () => {
assert.equal(isTag('refs/tags/foo'), true);
});
it("returns false for other kinds of refs", async () => {
assert.equal(isTag("refs/heads/master"), false);
it('returns false for other kinds of refs', async () => {
assert.equal(isTag('refs/heads/master'), false);
});
});
describe("paths", () => {
it("resolves files given a set of paths", async () => {
assert.deepStrictEqual(
paths(["tests/data/**/*", "tests/data/does/not/exist/*"]),
["tests/data/foo/bar.txt"],
);
describe('paths', () => {
it('resolves files given a set of paths', async () => {
assert.deepStrictEqual(paths(['tests/data/**/*', 'tests/data/does/not/exist/*']), [
'tests/data/foo/bar.txt',
]);
});
it('resolves files relative to working_directory', async () => {
assert.deepStrictEqual(paths(['data/**/*'], 'tests'), ['tests/data/foo/bar.txt']);
});
});
describe("unmatchedPatterns", () => {
describe('unmatchedPatterns', () => {
it("returns the patterns that don't match any files", async () => {
assert.deepStrictEqual(
unmatchedPatterns(["tests/data/**/*", "tests/data/does/not/exist/*"]),
["tests/data/does/not/exist/*"],
unmatchedPatterns(['tests/data/**/*', 'tests/data/does/not/exist/*']),
['tests/data/does/not/exist/*'],
);
});
it('resolves unmatched relative to working_directory', async () => {
assert.deepStrictEqual(unmatchedPatterns(['data/does/not/exist/*'], 'tests'), [
'data/does/not/exist/*',
]);
});
});
describe("replaceSpacesWithDots", () => {
it("replaces all spaces with dots", () => {
expect(alignAssetName("John Doe.bla")).toBe("John.Doe.bla");
describe('replaceSpacesWithDots', () => {
it('replaces all spaces with dots', () => {
expect(alignAssetName('John Doe.bla')).toBe('John.Doe.bla');
});
it("handles names with multiple spaces", () => {
expect(alignAssetName("John William Doe.bla")).toBe(
"John.William.Doe.bla",
);
it('handles names with multiple spaces', () => {
expect(alignAssetName('John William Doe.bla')).toBe('John.William.Doe.bla');
});
it("returns the same string if there are no spaces", () => {
expect(alignAssetName("JohnDoe")).toBe("JohnDoe");
it('returns the same string if there are no spaces', () => {
expect(alignAssetName('JohnDoe')).toBe('JohnDoe');
});
});
});
describe('parseInputFiles edge cases', () => {
it('handles multiple brace groups on same line', () => {
assert.deepStrictEqual(parseInputFiles('./**/*.{exe,deb},./dist/**/*.{zip,tar.gz}'), [
'./**/*.{exe,deb}',
'./dist/**/*.{zip,tar.gz}',
]);
});
it('handles nested braces', () => {
assert.deepStrictEqual(parseInputFiles('path/{a,{b,c}}/file.txt'), ['path/{a,{b,c}}/file.txt']);
});
it('handles empty comma-separated values', () => {
assert.deepStrictEqual(parseInputFiles('foo,,bar'), ['foo', 'bar']);
});
it('handles commas with spaces around braces', () => {
assert.deepStrictEqual(parseInputFiles(' ./**/*.{exe,deb} , file.txt '), [
'./**/*.{exe,deb}',
'file.txt',
]);
});
it('handles mixed newlines and commas with braces', () => {
assert.deepStrictEqual(parseInputFiles('file1.txt\n./**/*.{exe,deb},file2.txt\nfile3.txt'), [
'file1.txt',
'./**/*.{exe,deb}',
'file2.txt',
'file3.txt',
]);
});
});

View File

@@ -27,6 +27,13 @@ inputs:
files:
description: "Newline-delimited list of path globs for asset files to upload"
required: false
working_directory:
description: "Base directory to resolve 'files' globs against (defaults to job working-directory)"
required: false
overwrite_files:
description: "Overwrite existing files with the same name. Defaults to true"
required: false
default: 'true'
fail_on_unmatched_files:
description: "Fails if any of the `files` globs match nothing. Defaults to false"
required: false

8
dist/index.js vendored

File diff suppressed because one or more lines are too long

1712
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "action-gh-release",
"version": "2.2.2",
"version": "2.5.0",
"private": true,
"description": "GitHub Action for creating GitHub Releases",
"main": "lib/main.js",
@@ -24,21 +24,21 @@
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.1",
"@octokit/plugin-retry": "^8.0.1",
"@octokit/plugin-throttling": "^11.0.1",
"glob": "^11.0.2",
"mime-types": "^3.0.1"
"@octokit/plugin-retry": "^8.0.3",
"@octokit/plugin-throttling": "^11.0.3",
"glob": "^13.0.0",
"mime-types": "^3.0.2"
},
"devDependencies": {
"@types/glob": "^8.1.0",
"@types/glob": "^9.0.0",
"@types/mime-types": "^3.0.1",
"@types/node": "^22.15.21",
"@vercel/ncc": "^0.38.3",
"@vitest/coverage-v8": "^3.1.4",
"prettier": "3.5.3",
"@types/node": "^20.19.25",
"@vercel/ncc": "^0.38.4",
"@vitest/coverage-v8": "^4.0.13",
"prettier": "3.6.2",
"ts-node": "^10.9.2",
"typescript": "^5.8.3",
"typescript": "^5.9.3",
"typescript-formatter": "^7.2.2",
"vitest": "^3.1.4"
"vitest": "^4.0.4"
}
}

View File

@@ -1,9 +1,9 @@
import { GitHub } from "@actions/github/lib/utils";
import { Config, isTag, releaseBody, alignAssetName } from "./util";
import { statSync } from "fs";
import { open } from "fs/promises";
import { lookup } from "mime-types";
import { basename } from "path";
import { GitHub } from '@actions/github/lib/utils';
import { statSync } from 'fs';
import { open } from 'fs/promises';
import { lookup } from 'mime-types';
import { basename } from 'path';
import { alignAssetName, Config, isTag, releaseBody } from './util';
type GitHub = InstanceType<typeof GitHub>;
@@ -27,11 +27,7 @@ export interface Release {
}
export interface Releaser {
getReleaseByTag(params: {
owner: string;
repo: string;
tag: string;
}): Promise<{ data: Release }>;
getReleaseByTag(params: { owner: string; repo: string; tag: string }): Promise<{ data: Release }>;
createRelease(params: {
owner: string;
@@ -44,7 +40,7 @@ export interface Releaser {
target_commitish: string | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined;
make_latest: 'true' | 'false' | 'legacy' | undefined;
}): Promise<{ data: Release }>;
updateRelease(params: {
@@ -59,13 +55,16 @@ export interface Releaser {
prerelease: boolean | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined;
make_latest: 'true' | 'false' | 'legacy' | undefined;
}): Promise<{ data: Release }>;
allReleases(params: {
finalizeRelease(params: {
owner: string;
repo: string;
}): AsyncIterableIterator<{ data: Release[] }>;
release_id: number;
}): Promise<{ data: Release }>;
allReleases(params: { owner: string; repo: string }): AsyncIterableIterator<{ data: Release[] }>;
}
export class GitHubReleaser implements Releaser {
@@ -82,7 +81,27 @@ export class GitHubReleaser implements Releaser {
return this.github.rest.repos.getReleaseByTag(params);
}
createRelease(params: {
async getReleaseNotes(params: {
owner: string;
repo: string;
tag_name: string;
target_commitish: string | undefined;
}): Promise<{
data: {
name: string;
body: string;
};
}> {
return await this.github.rest.repos.generateReleaseNotes(params);
}
truncateReleaseNotes(input: string): string {
// release notes can be a maximum of 125000 characters
const githubNotesMaxCharLength = 125000;
return input.substring(0, githubNotesMaxCharLength - 1);
}
async createRelease(params: {
owner: string;
repo: string;
tag_name: string;
@@ -93,19 +112,28 @@ export class GitHubReleaser implements Releaser {
target_commitish: string | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined;
make_latest: 'true' | 'false' | 'legacy' | undefined;
}): Promise<{ data: Release }> {
if (
typeof params.make_latest === "string" &&
!["true", "false", "legacy"].includes(params.make_latest)
typeof params.make_latest === 'string' &&
!['true', 'false', 'legacy'].includes(params.make_latest)
) {
params.make_latest = undefined;
}
if (params.generate_release_notes) {
const releaseNotes = await this.getReleaseNotes(params);
params.generate_release_notes = false;
if (params.body) {
params.body = `${params.body}\n\n${releaseNotes.data.body}`;
} else {
params.body = releaseNotes.data.body;
}
}
params.body = params.body ? this.truncateReleaseNotes(params.body) : undefined;
return this.github.rest.repos.createRelease(params);
}
updateRelease(params: {
async updateRelease(params: {
owner: string;
repo: string;
release_id: number;
@@ -117,22 +145,37 @@ export class GitHubReleaser implements Releaser {
prerelease: boolean | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined;
make_latest: 'true' | 'false' | 'legacy' | undefined;
}): Promise<{ data: Release }> {
if (
typeof params.make_latest === "string" &&
!["true", "false", "legacy"].includes(params.make_latest)
typeof params.make_latest === 'string' &&
!['true', 'false', 'legacy'].includes(params.make_latest)
) {
params.make_latest = undefined;
}
if (params.generate_release_notes) {
const releaseNotes = await this.getReleaseNotes(params);
params.generate_release_notes = false;
if (params.body) {
params.body = `${params.body}\n\n${releaseNotes.data.body}`;
} else {
params.body = releaseNotes.data.body;
}
}
params.body = params.body ? this.truncateReleaseNotes(params.body) : undefined;
return this.github.rest.repos.updateRelease(params);
}
allReleases(params: {
owner: string;
repo: string;
}): AsyncIterableIterator<{ data: Release[] }> {
async finalizeRelease(params: { owner: string; repo: string; release_id: number }) {
return await this.github.rest.repos.updateRelease({
owner: params.owner,
repo: params.repo,
release_id: params.release_id,
draft: false,
});
}
allReleases(params: { owner: string; repo: string }): AsyncIterableIterator<{ data: Release[] }> {
const updatedParams = { per_page: 100, ...params };
return this.github.paginate.iterator(
this.github.rest.repos.listReleases.endpoint.merge(updatedParams),
@@ -149,7 +192,7 @@ export const asset = (path: string): ReleaseAsset => {
};
export const mimeOrDefault = (path: string): string => {
return lookup(path) || "application/octet-stream";
return lookup(path) || 'application/octet-stream';
};
export const upload = async (
@@ -159,7 +202,7 @@ export const upload = async (
path: string,
currentAssets: Array<{ id: number; name: string }>,
): Promise<any> => {
const [owner, repo] = config.github_repository.split("/");
const [owner, repo] = config.github_repository.split('/');
const { name, mime, size } = asset(path);
const currentAsset = currentAssets.find(
// 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.
@@ -168,27 +211,32 @@ export const upload = async (
({ name: currentName }) => currentName == alignAssetName(name),
);
if (currentAsset) {
console.log(`♻️ Deleting previously uploaded asset ${name}...`);
await github.rest.repos.deleteReleaseAsset({
asset_id: currentAsset.id || 1,
owner,
repo,
});
if (config.input_overwrite_files === false) {
console.log(`Asset ${name} already exists and overwrite_files is false...`);
return null;
} else {
console.log(`♻️ Deleting previously uploaded asset ${name}...`);
await github.rest.repos.deleteReleaseAsset({
asset_id: currentAsset.id || 1,
owner,
repo,
});
}
}
console.log(`⬆️ Uploading ${name}...`);
const endpoint = new URL(url);
endpoint.searchParams.append("name", name);
endpoint.searchParams.append('name', name);
const fh = await open(path);
try {
const resp = await github.request({
method: "POST",
method: 'POST',
url: endpoint.toString(),
headers: {
"content-length": `${size}`,
"content-type": mime,
'content-length': `${size}`,
'content-type': mime,
authorization: `token ${config.github_token}`,
},
data: fh.readableWebStream(),
data: fh.readableWebStream({ type: 'bytes' }),
});
const json = resp.data;
if (resp.status !== 201) {
@@ -212,25 +260,18 @@ export const release = async (
): Promise<Release> => {
if (maxRetries <= 0) {
console.log(`❌ Too many retries. Aborting...`);
throw new Error("Too many retries.");
throw new Error('Too many retries.');
}
const [owner, repo] = config.github_repository.split("/");
const [owner, repo] = config.github_repository.split('/');
const tag =
config.input_tag_name ||
(isTag(config.github_ref)
? config.github_ref.replace("refs/tags/", "")
: "");
(isTag(config.github_ref) ? config.github_ref.replace('refs/tags/', '') : '');
const discussion_category_name = config.input_discussion_category_name;
const generate_release_notes = config.input_generate_release_notes;
try {
const _release: Release | undefined = await findTagFromReleases(
releaser,
owner,
repo,
tag,
);
const _release: Release | undefined = await findTagFromReleases(releaser, owner, repo, tag);
if (_release === undefined) {
return await createRelease(
@@ -246,9 +287,7 @@ export const release = async (
}
let existingRelease: Release = _release!;
console.log(
`Found release ${existingRelease.name} (with id=${existingRelease.id})`,
);
console.log(`Found release ${existingRelease.name} (with id=${existingRelease.id})`);
const release_id = existingRelease.id;
let target_commitish: string;
@@ -270,23 +309,17 @@ export const release = async (
// body parts as a release gets updated. some users will likely want this while
// others won't previously this was duplicating content for most which
// no one wants
const workflowBody = releaseBody(config) || "";
const existingReleaseBody = existingRelease.body || "";
const workflowBody = releaseBody(config) || '';
const existingReleaseBody = existingRelease.body || '';
let body: string;
if (config.input_append_body && workflowBody && existingReleaseBody) {
body = existingReleaseBody + "\n" + workflowBody;
body = existingReleaseBody + '\n' + workflowBody;
} else {
body = workflowBody || existingReleaseBody;
}
const draft =
config.input_draft !== undefined
? config.input_draft
: existingRelease.draft;
const prerelease =
config.input_prerelease !== undefined
? config.input_prerelease
: existingRelease.prerelease;
config.input_prerelease !== undefined ? config.input_prerelease : existingRelease.prerelease;
const make_latest = config.input_make_latest;
@@ -298,7 +331,7 @@ export const release = async (
target_commitish,
name,
body,
draft,
draft: existingRelease.draft,
prerelease,
discussion_category_name,
generate_release_notes,
@@ -326,6 +359,45 @@ export const release = async (
}
};
/**
* Finalizes a release by unmarking it as "draft" (if relevant)
* after all artifacts have been uploaded.
*
* @param config - Release configuration as specified by user
* @param releaser - The GitHub API wrapper for release operations
* @param release - The existing release to be finalized
* @param maxRetries - The maximum number of attempts to finalize the release
*/
export const finalizeRelease = async (
config: Config,
releaser: Releaser,
release: Release,
maxRetries: number = 3,
): Promise<Release> => {
if (config.input_draft === true) {
return release;
}
if (maxRetries <= 0) {
console.log(`❌ Too many retries. Aborting...`);
throw new Error('Too many retries.');
}
const [owner, repo] = config.github_repository.split('/');
try {
const { data } = await releaser.finalizeRelease({
owner,
repo,
release_id: release.id,
});
return data;
} catch {
console.log(`retrying... (${maxRetries - 1} retries remaining)`);
return finalizeRelease(config, releaser, release, maxRetries - 1);
}
};
/**
* Finds a release by tag name from all a repository's releases.
*
@@ -366,17 +438,14 @@ async function createRelease(
const tag_name = tag;
const name = config.input_name || tag;
const body = releaseBody(config);
const draft = config.input_draft;
const prerelease = config.input_prerelease;
const target_commitish = config.input_target_commitish;
const make_latest = config.input_make_latest;
let commitMessage: string = "";
let commitMessage: string = '';
if (target_commitish) {
commitMessage = ` using commit "${target_commitish}"`;
}
console.log(
`👩‍🏭 Creating new GitHub release for tag ${tag_name}${commitMessage}...`,
);
console.log(`👩‍🏭 Creating new GitHub release for tag ${tag_name}${commitMessage}...`);
try {
let release = await releaser.createRelease({
owner,
@@ -384,7 +453,7 @@ async function createRelease(
tag_name,
name,
body,
draft,
draft: true,
prerelease,
target_commitish,
discussion_category_name,
@@ -400,17 +469,27 @@ async function createRelease(
switch (error.status) {
case 403:
console.log(
"Skip retry — your GitHub token/PAT does not have the required permission to create a release",
'Skip retry — your GitHub token/PAT does not have the required permission to create a release',
);
throw error;
case 404:
console.log("Skip retry - discussion category mismatch");
console.log('Skip retry - discussion category mismatch');
throw error;
case 422:
console.log("Skip retry - validation failed");
throw error;
// Check if this is a race condition with "already_exists" error
const errorData = error.response?.data;
if (errorData?.errors?.[0]?.code === 'already_exists') {
console.log(
'⚠️ Release already exists (race condition detected), retrying to find and update existing release...',
);
// Don't throw - allow retry to find existing release
} else {
console.log('Skip retry - validation failed');
throw error;
}
break;
}
console.log(`retrying... (${maxRetries - 1} retries remaining)`);

View File

@@ -1,28 +1,18 @@
import {
paths,
parseConfig,
isTag,
unmatchedPatterns,
uploadUrl,
} from "./util";
import { release, upload, GitHubReleaser } from "./github";
import { getOctokit } from "@actions/github";
import { setFailed, setOutput } from "@actions/core";
import { setFailed, setOutput } from '@actions/core';
import { getOctokit } from '@actions/github';
import { GitHubReleaser, release, finalizeRelease, upload } from './github';
import { isTag, parseConfig, paths, unmatchedPatterns, uploadUrl } from './util';
import { env } from "process";
import { env } from 'process';
async function run() {
try {
const config = parseConfig(env);
if (
!config.input_tag_name &&
!isTag(config.github_ref) &&
!config.input_draft
) {
if (!config.input_tag_name && !isTag(config.github_ref) && !config.input_draft) {
throw new Error(`⚠️ GitHub Releases requires a tag`);
}
if (config.input_files) {
const patterns = unmatchedPatterns(config.input_files);
const patterns = unmatchedPatterns(config.input_files, config.input_working_directory);
patterns.forEach((pattern) => {
if (config.input_fail_on_unmatched_files) {
throw new Error(`⚠️ Pattern '${pattern}' does not match any files.`);
@@ -44,9 +34,7 @@ async function run() {
//new oktokit(
throttle: {
onRateLimit: (retryAfter, options) => {
console.warn(
`Request quota exhausted for request ${options.method} ${options.url}`,
);
console.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
if (options.request.retryCount === 0) {
// only retries once
console.log(`Retrying after ${retryAfter} seconds!`);
@@ -55,56 +43,53 @@ async function run() {
},
onAbuseLimit: (retryAfter, options) => {
// does not retry, only logs a warning
console.warn(
`Abuse detected for request ${options.method} ${options.url}`,
);
console.warn(`Abuse detected for request ${options.method} ${options.url}`);
},
},
});
//);
const rel = await release(config, new GitHubReleaser(gh));
const releaser = new GitHubReleaser(gh);
let rel = await release(config, releaser);
if (config.input_files && config.input_files.length > 0) {
const files = paths(config.input_files);
const files = paths(config.input_files, config.input_working_directory);
if (files.length == 0) {
if (config.input_fail_on_unmatched_files) {
throw new Error(
`⚠️ ${config.input_files} does not include a valid file.`,
);
throw new Error(`⚠️ ${config.input_files} does not include a valid file.`);
} else {
console.warn(
`🤔 ${config.input_files} does not include a valid file.`,
);
console.warn(`🤔 ${config.input_files} does not include a valid file.`);
}
}
const currentAssets = rel.assets;
const uploadFile = async (path) => {
const json = await upload(
config,
gh,
uploadUrl(rel.upload_url),
path,
currentAssets,
);
delete json.uploader;
const json = await upload(config, gh, uploadUrl(rel.upload_url), path, currentAssets);
if (json) {
delete json.uploader;
}
return json;
};
let assets;
let results: (any | null)[];
if (!config.input_preserve_order) {
assets = await Promise.all(files.map(uploadFile));
results = await Promise.all(files.map(uploadFile));
} else {
assets = [];
results = [];
for (const path of files) {
assets.push(await uploadFile(path));
results.push(await uploadFile(path));
}
}
setOutput("assets", assets);
const assets = results.filter(Boolean);
setOutput('assets', assets);
}
console.log('Finalizing release...');
rel = await finalizeRelease(config, releaser, rel);
console.log(`🎉 Release ready at ${rel.html_url}`);
setOutput("url", rel.html_url);
setOutput("id", rel.id.toString());
setOutput("upload_url", rel.upload_url);
setOutput('url', rel.html_url);
setOutput('id', rel.id.toString());
setOutput('upload_url', rel.upload_url);
} catch (error) {
setFailed(error.message);
}

View File

@@ -1,5 +1,6 @@
import * as glob from "glob";
import { statSync, readFileSync } from "fs";
import * as glob from 'glob';
import { statSync, readFileSync } from 'fs';
import * as pathLib from 'path';
export interface Config {
github_token: string;
@@ -12,6 +13,8 @@ export interface Config {
input_body?: string;
input_body_path?: string;
input_files?: string[];
input_working_directory?: string;
input_overwrite_files?: boolean;
input_draft?: boolean;
input_preserve_order?: boolean;
input_prerelease?: boolean;
@@ -20,11 +23,11 @@ export interface Config {
input_discussion_category_name?: string;
input_generate_release_notes?: boolean;
input_append_body?: boolean;
input_make_latest: "true" | "false" | "legacy" | undefined;
input_make_latest: 'true' | 'false' | 'legacy' | undefined;
}
export const uploadUrl = (url: string): string => {
const templateMarkerPos = url.indexOf("{");
const templateMarkerPos = url.indexOf('{');
if (templateMarkerPos > -1) {
return url.substring(0, templateMarkerPos);
}
@@ -32,84 +35,123 @@ export const uploadUrl = (url: string): string => {
};
export const releaseBody = (config: Config): string | undefined => {
return (
(config.input_body_path &&
readFileSync(config.input_body_path).toString("utf8")) ||
config.input_body
);
if (config.input_body_path) {
try {
const contents = readFileSync(config.input_body_path, 'utf8');
return contents;
} catch (err: any) {
console.warn(
`⚠️ Failed to read body_path "${config.input_body_path}" (${err?.code ?? 'ERR'}). Falling back to 'body' input.`,
);
}
}
return config.input_body;
};
type Env = { [key: string]: string | undefined };
const smartSplit = (input: string): string[] => {
const result: string[] = [];
let current = '';
let braceDepth = 0;
for (const ch of input) {
if (ch === '{') {
braceDepth++;
}
if (ch === '}') {
braceDepth--;
}
if (ch === ',' && braceDepth === 0) {
if (current.trim()) {
result.push(current.trim());
}
current = '';
} else {
current += ch;
}
}
if (current.trim()) {
result.push(current.trim());
}
return result;
};
export const parseInputFiles = (files: string): string[] => {
return files.split(/\r?\n/).reduce<string[]>(
(acc, line) =>
acc
.concat(line.split(","))
.filter((pat) => pat)
.map((pat) => pat.trim()),
[],
);
return files
.split(/\r?\n/)
.flatMap((line) => smartSplit(line))
.filter((pat) => pat.trim() !== '');
};
export const parseConfig = (env: Env): Config => {
return {
github_token: env.GITHUB_TOKEN || env.INPUT_TOKEN || "",
github_ref: env.GITHUB_REF || "",
github_repository: env.INPUT_REPOSITORY || env.GITHUB_REPOSITORY || "",
github_token: env.GITHUB_TOKEN || env.INPUT_TOKEN || '',
github_ref: env.GITHUB_REF || '',
github_repository: env.INPUT_REPOSITORY || env.GITHUB_REPOSITORY || '',
input_name: env.INPUT_NAME,
input_tag_name: env.INPUT_TAG_NAME?.trim(),
input_body: env.INPUT_BODY,
input_body_path: env.INPUT_BODY_PATH,
input_files: parseInputFiles(env.INPUT_FILES || ""),
input_draft: env.INPUT_DRAFT ? env.INPUT_DRAFT === "true" : undefined,
input_preserve_order: env.INPUT_PRESERVE_ORDER
? env.INPUT_PRESERVE_ORDER == "true"
input_files: parseInputFiles(env.INPUT_FILES || ''),
input_working_directory: env.INPUT_WORKING_DIRECTORY || undefined,
input_overwrite_files: env.INPUT_OVERWRITE_FILES
? env.INPUT_OVERWRITE_FILES == 'true'
: undefined,
input_prerelease: env.INPUT_PRERELEASE
? env.INPUT_PRERELEASE == "true"
: undefined,
input_fail_on_unmatched_files: env.INPUT_FAIL_ON_UNMATCHED_FILES == "true",
input_draft: env.INPUT_DRAFT ? env.INPUT_DRAFT === 'true' : undefined,
input_preserve_order: env.INPUT_PRESERVE_ORDER ? env.INPUT_PRESERVE_ORDER == 'true' : undefined,
input_prerelease: env.INPUT_PRERELEASE ? env.INPUT_PRERELEASE == 'true' : undefined,
input_fail_on_unmatched_files: env.INPUT_FAIL_ON_UNMATCHED_FILES == 'true',
input_target_commitish: env.INPUT_TARGET_COMMITISH || undefined,
input_discussion_category_name:
env.INPUT_DISCUSSION_CATEGORY_NAME || undefined,
input_generate_release_notes: env.INPUT_GENERATE_RELEASE_NOTES == "true",
input_append_body: env.INPUT_APPEND_BODY == "true",
input_discussion_category_name: env.INPUT_DISCUSSION_CATEGORY_NAME || undefined,
input_generate_release_notes: env.INPUT_GENERATE_RELEASE_NOTES == 'true',
input_append_body: env.INPUT_APPEND_BODY == 'true',
input_make_latest: parseMakeLatest(env.INPUT_MAKE_LATEST),
};
};
const parseMakeLatest = (
value: string | undefined,
): "true" | "false" | "legacy" | undefined => {
if (value === "true" || value === "false" || value === "legacy") {
const parseMakeLatest = (value: string | undefined): 'true' | 'false' | 'legacy' | undefined => {
if (value === 'true' || value === 'false' || value === 'legacy') {
return value;
}
return undefined;
};
export const paths = (patterns: string[]): string[] => {
export const paths = (patterns: string[], cwd?: string): string[] => {
return patterns.reduce((acc: string[], pattern: string): string[] => {
return acc.concat(
glob.sync(pattern).filter((path) => statSync(path).isFile()),
);
const matches = glob.sync(pattern, { cwd, dot: true, absolute: false });
const resolved = matches
.map((p) => (cwd ? pathLib.join(cwd, p) : p))
.filter((p) => {
try {
return statSync(p).isFile();
} catch {
return false;
}
});
return acc.concat(resolved);
}, []);
};
export const unmatchedPatterns = (patterns: string[]): string[] => {
export const unmatchedPatterns = (patterns: string[], cwd?: string): string[] => {
return patterns.reduce((acc: string[], pattern: string): string[] => {
return acc.concat(
glob.sync(pattern).filter((path) => statSync(path).isFile()).length == 0
? [pattern]
: [],
);
const matches = glob.sync(pattern, { cwd, dot: true, absolute: false });
const files = matches.filter((p) => {
try {
const full = cwd ? pathLib.join(cwd, p) : p;
return statSync(full).isFile();
} catch {
return false;
}
});
return acc.concat(files.length == 0 ? [pattern] : []);
}, []);
};
export const isTag = (ref: string): boolean => {
return ref.startsWith("refs/tags/");
return ref.startsWith('refs/tags/');
};
export const alignAssetName = (assetName: string): string => {
return assetName.replace(/ /g, ".");
return assetName.replace(/ /g, '.');
};