50 Commits
v1.18.0 ... v1

Author SHA1 Message Date
Nick Cipollo
339a81892b preparing release 1.21.0
Some checks failed
Release / Create Release (push) Has been cancelled
Test / check_pr (push) Has been cancelled
2026-03-14 16:42:44 -04:00
Nick Cipollo
df17233557 Resolve pnpm audit results
- bump vitest
- override undici
2026-03-14 16:36:59 -04:00
Nick Cipollo
813e942459 Update release script 2026-03-14 16:27:03 -04:00
Nick Cipollo
7df3a0e749 Update sheepit to use pnpm 2026-03-14 16:13:52 -04:00
Nick Cipollo
caacf56b56 Fixes #595 Bump to node 24 2026-03-11 08:37:45 -04:00
Nick Cipollo
c074b5e19f Fixes #593 Pass commitish into release notes request when present (#594) 2026-03-04 22:51:50 -05:00
dependabot[bot]
9e0366240f Bump @biomejs/biome from 2.3.13 to 2.4.4 (#591)
Bumps [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) from 2.3.13 to 2.4.4.
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.4/packages/@biomejs/biome)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.4.4
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 16:43:14 -05:00
dependabot[bot]
5b4b1954b0 Bump glob from 13.0.0 to 13.0.6 (#592)
Bumps [glob](https://github.com/isaacs/node-glob) from 13.0.0 to 13.0.6.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v13.0.0...v13.0.6)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 13.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 16:43:00 -05:00
Nick Cipollo
89bab4d0a7 debug build 2026-02-01 18:20:16 -05:00
dependabot[bot]
00cbfdc960 Bump @actions/github from 6.0.1 to 9.0.0 (#585)
Bumps [@actions/github](https://github.com/actions/toolkit/tree/HEAD/packages/github) from 6.0.1 to 9.0.0.
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/github/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/github)

---
updated-dependencies:
- dependency-name: "@actions/github"
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-01 18:18:42 -05:00
Nick Cipollo
3f973430f2 Bump to core-3.0.0, move to vitest, support ESM modules (#587) 2026-02-01 17:51:43 -05:00
dependabot[bot]
5b3313d377 Bump @types/node from 25.0.3 to 25.1.0 (#583) 2026-02-01 09:22:25 -05:00
dependabot[bot]
7886be8054 Bump @biomejs/biome from 2.3.10 to 2.3.13 (#586) 2026-02-01 08:40:16 -05:00
Nick Cipollo
411d95d6a3 Rebuild after bumping dependencies 2026-01-06 07:40:00 -05:00
dependabot[bot]
72a3e11e23 Bump @types/node from 24.10.1 to 25.0.3 (#580)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.10.1 to 25.0.3.
- [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: 25.0.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-06 07:28:46 -05:00
dependabot[bot]
fc196c3dda Bump @actions/core from 1.11.1 to 2.0.1 (#579)
Bumps [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) from 1.11.1 to 2.0.1.
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/@actions/artifact@2.0.1/packages/core)

---
updated-dependencies:
- dependency-name: "@actions/core"
  dependency-version: 2.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-06 07:28:36 -05:00
dependabot[bot]
788c17510f Bump @biomejs/biome from 2.3.8 to 2.3.10 (#578)
Bumps [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) from 2.3.8 to 2.3.10.
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.3.10/packages/@biomejs/biome)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.3.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-06 07:27:42 -05:00
dependabot[bot]
93682e08f2 Bump @octokit/types from 13.10.0 to 16.0.0 (#577)
Bumps [@octokit/types](https://github.com/octokit/types.ts) from 13.10.0 to 16.0.0.
- [Release notes](https://github.com/octokit/types.ts/releases)
- [Commits](https://github.com/octokit/types.ts/compare/v13.10.0...v16.0.0)

---
updated-dependencies:
- dependency-name: "@octokit/types"
  dependency-version: 16.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-06 07:27:27 -05:00
Nick Cipollo
5f55df31e1 Fixes #552 Add missing readme entry for generateReleaseNotesPreviousTag 2025-12-06 11:05:53 -05:00
Nick Cipollo
be9a4fac3a Remove untildify 2025-12-02 08:29:48 -05:00
Nick Cipollo
23d2cc4c27 Bump jest types 2025-12-02 07:42:05 -05:00
dependabot[bot]
c15e4e52f5 Bump glob from 11.1.0 to 13.0.0 (#573)
Bumps [glob](https://github.com/isaacs/node-glob) from 11.1.0 to 13.0.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v11.1.0...v13.0.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 13.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 07:39:42 -05:00
dependabot[bot]
b9387a1970 Bump @biomejs/biome from 2.3.2 to 2.3.8 (#575)
Bumps [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) from 2.3.2 to 2.3.8.
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.3.8/packages/@biomejs/biome)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.3.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 07:39:13 -05:00
Nick Cipollo
327ba10342 Fix phantom dependencies 2025-12-01 20:21:27 -05:00
Nick Cipollo
5e58a022dc Migrate form yarn to pnpm 2025-12-01 20:07:09 -05:00
Nick Cipollo
7ffaa93f91 Install dependencies 2025-12-01 18:10:32 -05:00
Nick Cipollo
c266243d9d Bump glob dependencies 2025-11-24 10:59:40 -05:00
dependabot[bot]
29d665a1e2 Bump actions/checkout from 5 to 6 (#569) 2025-11-23 12:07:31 -05:00
dependabot[bot]
8c0975efae Bump glob from 11.0.3 to 11.1.0 (#568) 2025-11-17 19:49:35 -05:00
dependabot[bot]
015a5adb5c Bump js-yaml from 3.14.1 to 3.14.2 (#567) 2025-11-17 15:28:39 -05:00
dependabot[bot]
73edeebfa1 Bump @types/node from 24.6.1 to 24.9.2 (#563) 2025-11-02 09:48:05 -05:00
dependabot[bot]
cacc3525a9 Bump @biomejs/biome from 1.9.4 to 2.3.2 (#564) 2025-11-02 09:45:38 -05:00
dependabot[bot]
0dda22e486 Bump ts-jest from 29.3.4 to 29.4.5 (#561) 2025-11-02 09:44:33 -05:00
dependabot[bot]
e00665902f Bump actions/setup-node from 5 to 6 (#559) 2025-11-02 09:44:21 -05:00
dependabot[bot]
a0e7822741 Bump @types/node from 22.15.29 to 24.6.1 (#555)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.29 to 24.6.1.
- [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: 24.6.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 10:51:17 -04:00
dependabot[bot]
206b0f552d Bump actions/setup-node from 4 to 5 (#551)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 10:50:49 -04:00
dependabot[bot]
fb82a3ec11 Bump typescript from 5.8.3 to 5.9.3 (#556)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.8.3 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.8.3...v5.9.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 5.9.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 10:49:10 -04:00
dependabot[bot]
4207a7677e Bump jest-circus from 29.7.0 to 30.2.0 (#557) 2025-10-01 09:28:00 -04:00
Nick Cipollo
b7eabc95ff preparing release 1.20.0
Some checks failed
Test / check_pr (push) Has been cancelled
Release / Create Release (push) Has been cancelled
2025-09-02 16:01:27 -04:00
Nick Cipollo
e87de4c2e4 Fixes #542 Add previous tag option (#550) 2025-09-02 15:56:40 -04:00
Nick Cipollo
98d25d4189 preparing release 1.19.2
Some checks failed
Release / Create Release (push) Has been cancelled
Test / check_pr (push) Has been cancelled
2025-09-02 12:53:03 -04:00
Nick Cipollo
d3601261f8 Fixes #548 Add support body + generated release notes (#549) 2025-09-02 12:27:56 -04:00
Benoît Blanchon
571fe81601 Update immutableCreate's default in the documentation (#547)
The default of `immutableCreate` changed from `true` to `false` in version 1.9.1
2025-09-02 08:11:01 -04:00
Nick Cipollo
1c89adf398 preparing release 1.19.1
Some checks failed
Release / Create Release (push) Has been cancelled
Test / check_pr (push) Has been cancelled
2025-09-01 12:57:13 -04:00
Nick Cipollo
36bf8dd70f References #545 Default immutable builds to false 2025-09-01 12:47:09 -04:00
Nick Cipollo
b12185d71f preparing release 1.19.0
Some checks failed
Release / Create Release (push) Has been cancelled
Test / check_pr (push) Has been cancelled
2025-09-01 10:28:36 -04:00
Nick Cipollo
defcf131e4 Fixes #540 Add support for immutable releases (#544) 2025-08-28 13:15:38 -04:00
Nick Cipollo
05013d58ed Standardize on separate call to generate release notes 2025-08-21 20:33:00 -04:00
dependabot[bot]
20ce211d17 Bump glob from 11.0.2 to 11.0.3 (#534)
Bumps [glob](https://github.com/isaacs/node-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)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 11.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-17 16:53:58 -04:00
dependabot[bot]
a262ce99ce Bump actions/checkout from 4 to 5 (#541)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-17 16:53:46 -04:00
47 changed files with 45057 additions and 41467 deletions

View File

@@ -11,22 +11,27 @@ jobs:
check_pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v6
with:
node-version: 20
cache: 'pnpm'
- name: "yarn install"
run: yarn install
- name: "pnpm install"
run: pnpm install
- name: "yarn build"
run: yarn build
- name: "pnpm build"
run: pnpm build
- name: "check for uncommitted changes"
# Ensure no changes, but ignore node_modules dir since dev/fresh ci deps installed.
run: |
git diff --exit-code --stat -- . ':!node_modules' \
|| (echo "##[error] found changed files after build. please 'yarn build && npm run format'" \
|| (echo "##[error] found changed files after build. please 'pnpm build && pnpm run format'" \
"and check in all changes" \
&& exit 1)

View File

@@ -12,7 +12,7 @@ jobs:
name: Create Release
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Create Release
id: create_release
uses: ncipollo/release-action@v1

View File

@@ -5,14 +5,19 @@ jobs:
check_pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v6
with:
node-version: 20
cache: 'pnpm'
- name: "yarn install"
run: yarn install
- name: "pnpm install"
run: pnpm install
- name: "yarn test"
run: yarn test
- name: "pnpm test"
run: pnpm test

5
.gitignore vendored
View File

@@ -92,7 +92,7 @@ fabric.properties
# Markdown Navigator plugin
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator/
.idea/copilot.*
# End of https://www.gitignore.io/api/webstorm
# Coverage
@@ -100,3 +100,6 @@ coverage
# Ignore lib, it contains intermediates
/lib
# Claude
.claude/settings.local.json

View File

@@ -25,6 +25,8 @@ This action will create a GitHub release and optionally upload an artifact to it
| discussionCategory | When provided this will generate a discussion of the specified category. The category must exist otherwise this will cause the action to fail. This isn't used with draft releases. The default "Announcements" category is not supported via the API and will cause an error if used here. | false | "" |
| draft | Optionally marks this release as a draft release. Set to true to enable. | false | "" |
| generateReleaseNotes | Indicates if release notes should be automatically generated. | false | false |
| generateReleaseNotesPreviousTag | An optional previous tag to use when generating release notes. This will limit the release notes to changes between the two tags. | false | "" |
| immutableCreate | Indicates if immutable release creation should be used. When enabled, the action will first create a draft, upload artifacts, then publish the release. | false | false |
| makeLatest | Indicates if the release should be the "latest" release or not. legacy specifies that the latest release should be determined based on the release creation date and higher semantic version. | false | "legacy" |
| name | An optional name for the release. If this is omitted the tag will be used. | false | "" |
| omitBody | Indicates if the release body should be omitted. | false | false |

View File

@@ -26,6 +26,7 @@
| discussionCategory | 当提供该选项时,将生成指定类别的 discussion。指定的类别必须存在否则将导致 Action 失败。这对草案状态的发布不生效。API 不支持默认的 Announcement 分类,使用这个分类会引发错误。 | false | "" |
| draft | 可选择将此版本标记为草稿版本。 设置为 true 以启用。 | false | "" |
| generateReleaseNotes | 指示是否应自动生成发行说明。 | false | false |
| immutableCreate | 指示是否应使用不可变发布创建。启用时,操作将首先创建草稿,上传产出文件,然后发布版本。 | false | false |
| makeLatest | 指示是否将这个发布设置为 latest。使用"lagecy"值则会根据发布时间和语义化版本号自动决定。 | false | "legacy" |
| name | 版本的可选名称。 如果省略,将使用标签。 | false | "" |
| omitBody | 指示是否应省略发布主体。 | false | false |

View File

@@ -0,0 +1,9 @@
import { vi } from "vitest"
export const debug = vi.fn()
export const getBooleanInput = vi.fn()
export const getInput = vi.fn()
export const notice = vi.fn()
export const setFailed = vi.fn()
export const setOutput = vi.fn()
export const warning = vi.fn()

8
__mocks__/fs.ts Normal file
View File

@@ -0,0 +1,8 @@
import { vi } from "vitest"
export const createReadStream = vi.fn()
export const statSync = vi.fn()
export const readFileSync = vi.fn()
export const writeFileSync = vi.fn()
export const existsSync = vi.fn()
export const realpathSync = vi.fn()

View File

@@ -1,11 +1,12 @@
import { Action } from "../src/Action"
import type { ActionSkipper } from "../src/ActionSkipper"
import { Artifact } from "../src/Artifact"
import type { ArtifactDestroyer } from "../src/ArtifactDestroyer"
import type { ArtifactUploader } from "../src/ArtifactUploader"
import type { Inputs } from "../src/Inputs"
import type { Outputs } from "../src/Outputs"
import type { Releases } from "../src/Releases"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { Action } from "../src/Action.js"
import type { ActionSkipper } from "../src/ActionSkipper.js"
import { Artifact } from "../src/Artifact.js"
import type { ArtifactDestroyer } from "../src/ArtifactDestroyer.js"
import type { ArtifactUploader } from "../src/ArtifactUploader.js"
import type { Inputs } from "../src/Inputs.js"
import type { Outputs } from "../src/Outputs.js"
import type { Releases } from "../src/Releases.js"
const TEST_URLS = {
UPLOAD_URL: "http://api.example.com",
@@ -14,18 +15,18 @@ const TEST_URLS = {
ZIPBALL_URL: "https://api.github.com/repos/owner/repo/zipball/v1.0.0",
} as const
const applyReleaseDataMock = jest.fn()
const applyAssetUrlsMock = jest.fn()
const artifactDestroyMock = jest.fn()
const createMock = jest.fn()
const deleteMock = jest.fn()
const getMock = jest.fn()
const listArtifactsMock = jest.fn()
const listMock = jest.fn()
const shouldSkipMock = jest.fn()
const updateMock = jest.fn()
const uploadMock = jest.fn()
const genReleaseNotesMock = jest.fn()
const applyReleaseDataMock = vi.fn()
const applyAssetUrlsMock = vi.fn()
const artifactDestroyMock = vi.fn()
const createMock = vi.fn()
const deleteMock = vi.fn()
const getMock = vi.fn()
const listArtifactsMock = vi.fn()
const listMock = vi.fn()
const shouldSkipMock = vi.fn()
const updateMock = vi.fn()
const uploadMock = vi.fn()
const genReleaseNotesMock = vi.fn()
const artifacts = [new Artifact("a/art1"), new Artifact("b/art2")]
@@ -34,7 +35,7 @@ const createDraft = true
const createName = "createName"
const commit = "commit"
const discussionCategory = "discussionCategory"
const generateReleaseNotes = true
const _generateReleaseNotes = true
const id = 100
const createPrerelease = true
const releaseId = 101
@@ -49,12 +50,15 @@ const updateOnlyUnreleased = false
const url = TEST_URLS.UPLOAD_URL
const makeLatest = "legacy"
const generatedReleaseBody = "test release notes"
const previousTag = "v1.0.0"
describe("Action", () => {
beforeEach(() => {
applyReleaseDataMock.mockClear()
applyAssetUrlsMock.mockClear()
artifactDestroyMock.mockClear()
createMock.mockClear()
genReleaseNotesMock.mockClear()
getMock.mockClear()
listMock.mockClear()
shouldSkipMock.mockClear()
@@ -62,18 +66,132 @@ describe("Action", () => {
uploadMock.mockClear()
})
it("creates release but does not upload if no artifact", async () => {
const action = createAction(false, false)
it("creates release with generated release notes when no body provided", async () => {
const action = createAction(false, false, false, true, false, "")
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag, undefined, commit)
expect(createMock).toHaveBeenCalledWith(
tag,
createBody,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
makeLatest,
createName,
createPrerelease
)
expect(uploadMock).not.toHaveBeenCalled()
assertOutputApplied()
assertAssetUrlsApplied({})
})
it("creates release with generated release notes that override existing body", async () => {
const action = createAction(false, false, false, true, false, "")
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag, undefined, commit)
expect(createMock).toHaveBeenCalledWith(
tag,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
makeLatest,
createName,
createPrerelease
)
expect(uploadMock).not.toHaveBeenCalled()
assertOutputApplied()
assertAssetUrlsApplied({})
})
it("creates release with static body when generateReleaseNotes is false", async () => {
const action = createAction(false, false, false, false, false, "static body")
await action.perform()
expect(genReleaseNotesMock).not.toHaveBeenCalled()
expect(createMock).toHaveBeenCalledWith(
tag,
"static body",
commit,
discussionCategory,
createDraft,
makeLatest,
createName,
createPrerelease
)
expect(uploadMock).not.toHaveBeenCalled()
assertOutputApplied()
assertAssetUrlsApplied({})
})
it("creates release with combined body and generated release notes", async () => {
const action = createAction(false, false, false, true, false, createBody)
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag, undefined, commit)
expect(createMock).toHaveBeenCalledWith(
tag,
`${createBody}\n${generatedReleaseBody}`,
commit,
discussionCategory,
createDraft,
makeLatest,
createName,
createPrerelease
)
expect(uploadMock).not.toHaveBeenCalled()
assertOutputApplied()
assertAssetUrlsApplied({})
})
it("creates release with combined body and generated release notes using previous tag", async () => {
const action = createAction(
false,
false,
false,
true,
false,
createBody,
true,
createDraft,
updateBody,
previousTag
)
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag, previousTag, commit)
expect(createMock).toHaveBeenCalledWith(
tag,
`${createBody}\n${generatedReleaseBody}`,
commit,
discussionCategory,
createDraft,
makeLatest,
createName,
createPrerelease
)
expect(uploadMock).not.toHaveBeenCalled()
})
it("creates release but does not upload if no artifact", async () => {
const action = createAction(false, false, false, true, false, "")
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag, undefined, commit)
expect(createMock).toHaveBeenCalledWith(
tag,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
generateReleaseNotes,
makeLatest,
createName,
createPrerelease
@@ -84,19 +202,19 @@ describe("Action", () => {
})
it("creates release if no release exists to update", async () => {
const action = createAction(true, true)
const action = createAction(true, true, false, true, false, "")
const error = { status: 404 }
getMock.mockRejectedValue(error)
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag, undefined, commit)
expect(createMock).toHaveBeenCalledWith(
tag,
createBody,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
generateReleaseNotes,
makeLatest,
createName,
createPrerelease
@@ -104,13 +222,13 @@ describe("Action", () => {
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied()
assertAssetUrlsApplied({
"art1": "https://github.com/owner/repo/releases/download/v1.0.0/art1",
"art2": "https://github.com/owner/repo/releases/download/v1.0.0/art2",
art1: "https://github.com/owner/repo/releases/download/v1.0.0/art1",
art2: "https://github.com/owner/repo/releases/download/v1.0.0/art2",
})
})
it("creates release if no draft releases", async () => {
const action = createAction(true, true)
const action = createAction(true, true, false, true, false, "")
const error = { status: 404 }
getMock.mockRejectedValue(error)
listMock.mockResolvedValue({
@@ -119,13 +237,13 @@ describe("Action", () => {
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag, undefined, commit)
expect(createMock).toHaveBeenCalledWith(
tag,
createBody,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
generateReleaseNotes,
makeLatest,
createName,
createPrerelease
@@ -133,23 +251,23 @@ describe("Action", () => {
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied()
assertAssetUrlsApplied({
"art1": "https://github.com/owner/repo/releases/download/v1.0.0/art1",
"art2": "https://github.com/owner/repo/releases/download/v1.0.0/art2",
art1: "https://github.com/owner/repo/releases/download/v1.0.0/art1",
art2: "https://github.com/owner/repo/releases/download/v1.0.0/art2",
})
})
it("creates release then uploads artifact", async () => {
const action = createAction(false, true)
const action = createAction(false, true, false, true, false, "")
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag, undefined, commit)
expect(createMock).toHaveBeenCalledWith(
tag,
createBody,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
generateReleaseNotes,
makeLatest,
createName,
createPrerelease
@@ -157,8 +275,8 @@ describe("Action", () => {
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied()
assertAssetUrlsApplied({
"art1": "https://github.com/owner/repo/releases/download/v1.0.0/art1",
"art2": "https://github.com/owner/repo/releases/download/v1.0.0/art2",
art1: "https://github.com/owner/repo/releases/download/v1.0.0/art1",
art2: "https://github.com/owner/repo/releases/download/v1.0.0/art2",
})
})
@@ -170,8 +288,8 @@ describe("Action", () => {
expect(artifactDestroyMock).toHaveBeenCalledWith(releaseId)
assertOutputApplied()
assertAssetUrlsApplied({
"art1": "https://github.com/owner/repo/releases/download/v1.0.0/art1",
"art2": "https://github.com/owner/repo/releases/download/v1.0.0/art2",
art1: "https://github.com/owner/repo/releases/download/v1.0.0/art1",
art2: "https://github.com/owner/repo/releases/download/v1.0.0/art2",
})
})
@@ -183,8 +301,8 @@ describe("Action", () => {
expect(artifactDestroyMock).not.toHaveBeenCalled()
assertOutputApplied()
assertAssetUrlsApplied({
"art1": "https://github.com/owner/repo/releases/download/v1.0.0/art1",
"art2": "https://github.com/owner/repo/releases/download/v1.0.0/art2",
art1: "https://github.com/owner/repo/releases/download/v1.0.0/art1",
art2: "https://github.com/owner/repo/releases/download/v1.0.0/art2",
})
})
@@ -199,7 +317,7 @@ describe("Action", () => {
})
it("throws error when create fails", async () => {
const action = createAction(false, true)
const action = createAction(false, true, false, true, false, "")
createMock.mockRejectedValue("error")
expect.hasAssertions()
@@ -209,13 +327,13 @@ describe("Action", () => {
expect(error).toEqual("error")
}
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag, undefined, commit)
expect(createMock).toHaveBeenCalledWith(
tag,
createBody,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
generateReleaseNotes,
makeLatest,
createName,
createPrerelease
@@ -273,7 +391,7 @@ describe("Action", () => {
})
it("throws error when update fails", async () => {
const action = createAction(true, true)
const action = createAction(true, true, false, true, false, "", true, createDraft, "")
updateMock.mockRejectedValue("error")
@@ -299,7 +417,7 @@ describe("Action", () => {
})
it("throws error when upload fails", async () => {
const action = createAction(false, true)
const action = createAction(false, true, false, true, false, "")
const expectedError = { status: 404 }
uploadMock.mockRejectedValue(expectedError)
@@ -310,13 +428,13 @@ describe("Action", () => {
expect(error).toEqual(expectedError)
}
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag, undefined, commit)
expect(createMock).toHaveBeenCalledWith(
tag,
createBody,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
generateReleaseNotes,
makeLatest,
createName,
createPrerelease
@@ -325,7 +443,7 @@ describe("Action", () => {
})
it("updates draft release", async () => {
const action = createAction(true, true)
const action = createAction(true, true, false, true, false, "", true, createDraft, "")
const error = { status: 404 }
getMock.mockRejectedValue(error)
listMock.mockResolvedValue({
@@ -351,8 +469,8 @@ describe("Action", () => {
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied()
assertAssetUrlsApplied({
"art1": "https://github.com/owner/repo/releases/download/v1.0.0/art1",
"art2": "https://github.com/owner/repo/releases/download/v1.0.0/art2",
art1: "https://github.com/owner/repo/releases/download/v1.0.0/art1",
art2: "https://github.com/owner/repo/releases/download/v1.0.0/art2",
})
})
@@ -383,13 +501,58 @@ describe("Action", () => {
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied()
assertAssetUrlsApplied({
"art1": "https://github.com/owner/repo/releases/download/v1.0.0/art1",
"art2": "https://github.com/owner/repo/releases/download/v1.0.0/art2",
art1: "https://github.com/owner/repo/releases/download/v1.0.0/art1",
art2: "https://github.com/owner/repo/releases/download/v1.0.0/art2",
})
})
it("updates release with combined body and generated release notes", async () => {
const action = createAction(true, true, false, true, false, "", true, createDraft, updateBody)
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag, undefined, commit)
expect(updateMock).toHaveBeenCalledWith(
id,
tag,
`${updateBody}\n${generatedReleaseBody}`,
commit,
discussionCategory,
updateDraft,
makeLatest,
updateName,
updatePrerelease
)
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied()
assertAssetUrlsApplied({
art1: "https://github.com/owner/repo/releases/download/v1.0.0/art1",
art2: "https://github.com/owner/repo/releases/download/v1.0.0/art2",
})
})
it("updates release with combined body and generated release notes using previous tag", async () => {
const action = createAction(true, true, false, true, false, "", true, createDraft, updateBody, previousTag)
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag, previousTag, commit)
expect(updateMock).toHaveBeenCalledWith(
id,
tag,
`${updateBody}\n${generatedReleaseBody}`,
commit,
discussionCategory,
updateDraft,
makeLatest,
updateName,
updatePrerelease
)
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
})
it("updates release but does not upload if no artifact", async () => {
const action = createAction(true, false)
const action = createAction(true, false, false, true, false, "", true, createDraft, "")
await action.perform()
@@ -410,7 +573,7 @@ describe("Action", () => {
})
it("updates release then uploads artifact", async () => {
const action = createAction(true, true)
const action = createAction(true, true, false, true, false, "", true, createDraft, "")
await action.perform()
@@ -428,8 +591,8 @@ describe("Action", () => {
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied()
assertAssetUrlsApplied({
"art1": "https://github.com/owner/repo/releases/download/v1.0.0/art1",
"art2": "https://github.com/owner/repo/releases/download/v1.0.0/art2",
art1: "https://github.com/owner/repo/releases/download/v1.0.0/art1",
art2: "https://github.com/owner/repo/releases/download/v1.0.0/art2",
})
})
@@ -460,8 +623,147 @@ describe("Action", () => {
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied()
assertAssetUrlsApplied({
"art1": "https://github.com/owner/repo/releases/download/v1.0.0/art1",
"art2": "https://github.com/owner/repo/releases/download/v1.0.0/art2",
art1: "https://github.com/owner/repo/releases/download/v1.0.0/art1",
art2: "https://github.com/owner/repo/releases/download/v1.0.0/art2",
})
})
it("does not publish immutable release when immutableCreate is false", async () => {
const action = createAction(false, true, false, true, false, "", false, false)
await action.perform()
expect(createMock).toHaveBeenCalledWith(
tag,
generatedReleaseBody,
commit,
discussionCategory,
false, // draft should be false when createdDraft is false
makeLatest,
createName,
createPrerelease
)
// Should only call update once for regular create, not for publishImmutableRelease
expect(updateMock).not.toHaveBeenCalled()
assertOutputApplied()
})
it("does not publish immutable release when createdDraft is true", async () => {
const action = createAction(false, true, false, true, false, "", true, true)
await action.perform()
expect(createMock).toHaveBeenCalledWith(
tag,
generatedReleaseBody,
commit,
discussionCategory,
true, // draft should be true when createdDraft is true or immutableCreate is true
makeLatest,
createName,
createPrerelease
)
// Should only call update once for regular create, not for publishImmutableRelease
expect(updateMock).not.toHaveBeenCalled()
assertOutputApplied()
})
it("publishes immutable release when immutableCreate is true and createdDraft is false", async () => {
const action = createAction(false, true, false, true, false, "", true, false)
const immutableReleaseResponse = {
data: {
id: 999,
upload_url: "http://immutable.example.com",
html_url: "https://github.com/owner/repo/releases/tag/v1.0.0-immutable",
tarball_url: "https://api.github.com/repos/owner/repo/tarball/v1.0.0-immutable",
zipball_url: "https://api.github.com/repos/owner/repo/zipball/v1.0.0-immutable",
},
}
updateMock.mockResolvedValueOnce(immutableReleaseResponse)
await action.perform()
expect(createMock).toHaveBeenCalledWith(
tag,
generatedReleaseBody,
commit,
discussionCategory,
true, // draft should be true when immutableCreate is true
makeLatest,
createName,
createPrerelease
)
// Should call update for publishImmutableRelease
expect(updateMock).toHaveBeenCalledWith(
releaseId,
tag,
undefined, // body is omitted
undefined, // commit is omitted
discussionCategory,
false, // draft is set to false to publish the release
makeLatest,
createName,
createPrerelease
)
// Should apply the immutable release data instead of the original
expect(applyReleaseDataMock).toHaveBeenCalledWith(immutableReleaseResponse.data)
assertAssetUrlsApplied({
art1: "https://github.com/owner/repo/releases/download/v1.0.0/art1",
art2: "https://github.com/owner/repo/releases/download/v1.0.0/art2",
})
})
it("publishes immutable release when allowUpdates is true but release does not exist", async () => {
const action = createAction(true, true, false, true, false, "", true, false)
const error = { status: 404 }
getMock.mockRejectedValue(error)
listMock.mockResolvedValue({ data: [] }) // No draft releases found
const immutableReleaseResponse = {
data: {
id: 888,
upload_url: "http://immutable-update.example.com",
html_url: "https://github.com/owner/repo/releases/tag/v1.0.0-immutable-update",
tarball_url: "https://api.github.com/repos/owner/repo/tarball/v1.0.0-immutable-update",
zipball_url: "https://api.github.com/repos/owner/repo/zipball/v1.0.0-immutable-update",
},
}
updateMock.mockResolvedValueOnce(immutableReleaseResponse)
await action.perform()
// Should try to get the release first (allowUpdates=true)
expect(getMock).toHaveBeenCalledWith(tag)
// Should check for draft releases when get fails with 404
expect(listMock).toHaveBeenCalled()
// Should create a new release when no drafts found
expect(createMock).toHaveBeenCalledWith(
tag,
generatedReleaseBody,
commit,
discussionCategory,
true, // draft should be true when immutableCreate is true
makeLatest,
createName,
createPrerelease
)
// Should call update for publishImmutableRelease
expect(updateMock).toHaveBeenCalledWith(
releaseId,
tag,
undefined, // body is omitted
undefined, // commit is omitted
discussionCategory,
false, // draft is set to false to publish the release
makeLatest,
createName,
createPrerelease
)
// Should apply the immutable release data instead of the original
expect(applyReleaseDataMock).toHaveBeenCalledWith(immutableReleaseResponse.data)
assertAssetUrlsApplied({
art1: "https://github.com/owner/repo/releases/download/v1.0.0/art1",
art2: "https://github.com/owner/repo/releases/download/v1.0.0/art2",
})
})
@@ -484,7 +786,12 @@ describe("Action", () => {
hasArtifact: boolean,
removeArtifacts = false,
generateReleaseNotes = true,
omitBodyDuringUpdate = false
omitBodyDuringUpdate = false,
createdReleaseBody = createBody,
immutableCreate = true,
createdDraft = createDraft,
updatedReleaseBody = updateBody,
generateReleaseNotesPreviousTag: string | undefined = undefined
): Action {
let inputArtifact: Artifact[]
@@ -494,7 +801,7 @@ describe("Action", () => {
inputArtifact = []
}
const MockReleases = jest.fn<Releases, any>(() => {
const MockReleases = vi.fn<() => Releases>(() => {
return {
create: createMock,
deleteArtifact: deleteMock,
@@ -502,7 +809,7 @@ describe("Action", () => {
listArtifactsForRelease: listArtifactsMock,
listReleases: listMock,
update: updateMock,
uploadArtifact: jest.fn(),
uploadArtifact: vi.fn(),
generateReleaseNotes: genReleaseNotesMock,
}
})
@@ -541,21 +848,23 @@ describe("Action", () => {
},
})
uploadMock.mockResolvedValue({
"art1": "https://github.com/owner/repo/releases/download/v1.0.0/art1",
"art2": "https://github.com/owner/repo/releases/download/v1.0.0/art2",
art1: "https://github.com/owner/repo/releases/download/v1.0.0/art1",
art2: "https://github.com/owner/repo/releases/download/v1.0.0/art2",
})
const MockInputs = jest.fn<Inputs, any>(() => {
const MockInputs = vi.fn<() => Inputs>(() => {
return {
allowUpdates,
artifactErrorsFailBuild: true,
artifacts: inputArtifact,
createdDraft: createDraft,
createdReleaseBody: createBody,
createdDraft: createdDraft,
createdReleaseBody: createdReleaseBody,
createdReleaseName: createName,
commit,
discussionCategory,
generateReleaseNotes,
generateReleaseNotesPreviousTag: generateReleaseNotesPreviousTag,
immutableCreate: immutableCreate,
makeLatest: makeLatest,
owner: "owner",
createdPrerelease: createPrerelease,
@@ -566,42 +875,42 @@ describe("Action", () => {
tag,
token,
updatedDraft: updateDraft,
updatedReleaseBody: updateBody,
updatedReleaseBody: updatedReleaseBody,
updatedReleaseName: updateName,
updatedPrerelease: updatePrerelease,
updateOnlyUnreleased: updateOnlyUnreleased,
omitBodyDuringUpdate,
}
})
const MockOutputs = jest.fn<Outputs, any>(() => {
const MockOutputs = vi.fn<() => Outputs>(() => {
return {
applyReleaseData: applyReleaseDataMock,
applyAssetUrls: applyAssetUrlsMock,
}
})
const MockUploader = jest.fn<ArtifactUploader, any>(() => {
const MockUploader = vi.fn<() => ArtifactUploader>(() => {
return {
uploadArtifacts: uploadMock,
}
})
const MockArtifactDestroyer = jest.fn<ArtifactDestroyer, any>(() => {
const MockArtifactDestroyer = vi.fn<() => ArtifactDestroyer>(() => {
return {
destroyArtifacts: artifactDestroyMock,
}
})
const MockActionSkipper = jest.fn<ActionSkipper, any>(() => {
const MockActionSkipper = vi.fn<() => ActionSkipper>(() => {
return {
shouldSkip: shouldSkipMock,
}
})
const inputs = new MockInputs()
const outputs = new MockOutputs()
const releases = new MockReleases()
const uploader = new MockUploader()
const artifactDestroyer = new MockArtifactDestroyer()
const actionSkipper = new MockActionSkipper()
const inputs = MockInputs()
const outputs = MockOutputs()
const releases = MockReleases()
const uploader = MockUploader()
const artifactDestroyer = MockArtifactDestroyer()
const actionSkipper = MockActionSkipper()
return new Action(inputs, outputs, releases, uploader, artifactDestroyer, actionSkipper)
}

View File

@@ -1,19 +1,20 @@
import { ReleaseActionSkipper } from "../src/ActionSkipper"
import type { Releases } from "../src/Releases"
import { describe, expect, it, vi } from "vitest"
import { ReleaseActionSkipper } from "../src/ActionSkipper.js"
import type { Releases } from "../src/Releases.js"
describe("shouldSkip", () => {
const getMock = jest.fn()
const getMock = vi.fn()
const tag = "tag"
const MockReleases = jest.fn<Releases, any>(() => {
const MockReleases = vi.fn<() => Releases>(() => {
return {
create: jest.fn(),
deleteArtifact: jest.fn(),
create: vi.fn(),
deleteArtifact: vi.fn(),
getByTag: getMock,
listArtifactsForRelease: jest.fn(),
listReleases: jest.fn(),
update: jest.fn(),
uploadArtifact: jest.fn(),
generateReleaseNotes: jest.fn(),
listArtifactsForRelease: vi.fn(),
listReleases: vi.fn(),
update: vi.fn(),
uploadArtifact: vi.fn(),
generateReleaseNotes: vi.fn(),
}
})

View File

@@ -1,16 +1,19 @@
import { Artifact } from "../src/Artifact"
import * as fs from "node:fs"
import { describe, expect, it, vi } from "vitest"
import { Artifact } from "../src/Artifact.js"
vi.mock("fs")
const contentLength = 42
const fakeReadStream = {}
jest.mock("fs", () => {
return {
createReadStream: () => fakeReadStream,
statSync: () => {
return { size: contentLength }
},
}
})
const mockCreateReadStream = vi.mocked(fs.createReadStream)
const mockStatSync = vi.mocked(fs.statSync)
// biome-ignore lint/suspicious/noExplicitAny: Mock object for testing
mockCreateReadStream.mockReturnValue(fakeReadStream as any)
// biome-ignore lint/suspicious/noExplicitAny: Partial Stats object for testing
mockStatSync.mockReturnValue({ size: contentLength } as any)
describe("Artifact", () => {
it("defaults contentType to raw", () => {

View File

@@ -1,10 +1,11 @@
import { GithubArtifactDestroyer } from "../src/ArtifactDestroyer"
import type { Releases } from "../src/Releases"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { GithubArtifactDestroyer } from "../src/ArtifactDestroyer.js"
import type { Releases } from "../src/Releases.js"
const releaseId = 100
const deleteMock = jest.fn()
const listArtifactsMock = jest.fn()
const deleteMock = vi.fn()
const listArtifactsMock = vi.fn()
describe("ArtifactDestroyer", () => {
beforeEach(() => {
@@ -45,26 +46,26 @@ describe("ArtifactDestroyer", () => {
})
function createDestroyer(): GithubArtifactDestroyer {
const MockReleases = jest.fn<Releases, any>(() => {
const MockReleases = vi.fn<() => Releases>(() => {
return {
create: jest.fn(),
create: vi.fn(),
deleteArtifact: deleteMock,
getByTag: jest.fn(),
getByTag: vi.fn(),
listArtifactsForRelease: listArtifactsMock,
listReleases: jest.fn(),
update: jest.fn(),
uploadArtifact: jest.fn(),
generateReleaseNotes: jest.fn(),
listReleases: vi.fn(),
update: vi.fn(),
uploadArtifact: vi.fn(),
generateReleaseNotes: vi.fn(),
}
})
return new GithubArtifactDestroyer(new MockReleases())
return new GithubArtifactDestroyer(MockReleases())
}
function mockDeleteError(): any {
function mockDeleteError(): void {
deleteMock.mockRejectedValue("error")
}
function mockDeleteSuccess(): any {
function mockDeleteSuccess(): void {
deleteMock.mockResolvedValue({})
}

View File

@@ -1,32 +1,33 @@
const warnMock = jest.fn()
import * as fs from "node:fs"
import * as core from "@actions/core"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { FileArtifactGlobber } from "../src/ArtifactGlobber"
import { Globber } from "../src/Globber"
import { Artifact } from "../src/Artifact"
import untildify = require("untildify")
vi.mock("@actions/core")
vi.mock("fs")
import { Artifact } from "../src/Artifact.js"
import { FileArtifactGlobber } from "../src/ArtifactGlobber.js"
import type { Globber } from "../src/Globber.js"
import { expandTilde } from "../src/PathExpander.js"
const warnMock = vi.mocked(core.warning)
const mockStatSync = vi.mocked(fs.statSync)
// biome-ignore lint/suspicious/noExplicitAny: fs.realpathSync has overloads that are difficult to type
const mockRealpathSync = vi.mocked(fs.realpathSync as any)
const contentType = "raw"
const globMock = jest.fn()
const globMock = vi.fn()
const globResults = ["file1", "file2"]
jest.mock("@actions/core", () => {
return { warning: warnMock }
})
mockStatSync.mockReturnValue({
isDirectory(): boolean {
return false
},
// biome-ignore lint/suspicious/noExplicitAny: Partial Stats object for testing
} as any)
jest.mock("fs", () => {
return {
statSync: () => {
return {
isDirectory(): boolean {
return false
},
}
},
realpathSync: () => {
return false
},
}
})
// biome-ignore lint/suspicious/noExplicitAny: Mock return value for testing
mockRealpathSync.mockReturnValue(false as any)
describe("ArtifactGlobber", () => {
beforeEach(() => {
@@ -39,7 +40,7 @@ describe("ArtifactGlobber", () => {
const expectedArtifacts = globResults.map((path) => new Artifact(path, contentType))
expect(globber.globArtifactString("~/path", "raw", false)).toEqual(expectedArtifacts)
expect(globMock).toHaveBeenCalledWith(untildify("~/path"))
expect(globMock).toHaveBeenCalledWith(expandTilde("~/path"))
expect(warnMock).not.toHaveBeenCalled()
})
@@ -90,12 +91,12 @@ describe("ArtifactGlobber", () => {
})
function createArtifactGlobber(results: string[] = globResults): FileArtifactGlobber {
const MockGlobber = jest.fn<Globber, any>(() => {
const MockGlobber = vi.fn<() => Globber>(() => {
return {
glob: globMock,
}
})
globMock.mockReturnValue(results)
return new FileArtifactGlobber(new MockGlobber())
return new FileArtifactGlobber(MockGlobber())
}
})

View File

@@ -1,22 +1,21 @@
const directoryMock = jest.fn()
const warnMock = jest.fn()
import * as fs from "node:fs"
import * as core from "@actions/core"
import { describe, expect, it, vi } from "vitest"
import { ArtifactPathValidator } from "../src/ArtifactPathValidator"
vi.mock("@actions/core")
vi.mock("fs")
import { ArtifactPathValidator } from "../src/ArtifactPathValidator.js"
const warnMock = vi.mocked(core.warning)
const mockStatSync = vi.mocked(fs.statSync)
const directoryMock = vi.fn()
// biome-ignore lint/suspicious/noExplicitAny: Partial Stats object for testing
mockStatSync.mockReturnValue({ isDirectory: directoryMock } as any)
const pattern = "pattern"
jest.mock("@actions/core", () => {
return { warning: warnMock }
})
jest.mock("fs", () => {
return {
statSync: () => {
return { isDirectory: directoryMock }
},
}
})
describe("ArtifactPathValidator", () => {
beforeEach(() => {
warnMock.mockClear()
@@ -36,7 +35,7 @@ describe("ArtifactPathValidator", () => {
it("warns when no glob results are produced and empty results shouldn't throw", () => {
const validator = new ArtifactPathValidator(false, [], pattern)
const result = validator.validate()
const _result = validator.validate()
expect(warnMock).toHaveBeenCalled()
})

View File

@@ -1,7 +1,8 @@
import { RequestError } from "@octokit/request-error"
import { Artifact } from "../src/Artifact"
import { GithubArtifactUploader } from "../src/ArtifactUploader"
import type { Releases } from "../src/Releases"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { Artifact } from "../src/Artifact.js"
import { GithubArtifactUploader } from "../src/ArtifactUploader.js"
import type { Releases } from "../src/Releases.js"
const artifacts = [new Artifact("a/art1"), new Artifact("b/art2")]
const fakeReadStream = {}
@@ -9,9 +10,9 @@ const contentLength = 42
const releaseId = 100
const url = "http://api.example.com"
const deleteMock = jest.fn()
const listArtifactsMock = jest.fn()
const uploadMock = jest.fn()
const deleteMock = vi.fn()
const listArtifactsMock = vi.fn()
const uploadMock = vi.fn()
// Mock response with browser_download_url
const mockUploadResponse = (name: string) => ({
@@ -19,11 +20,11 @@ const mockUploadResponse = (name: string) => ({
browser_download_url: `https://github.com/octocat/Hello-World/releases/download/v1.0.0/${name}`,
name: name,
id: 1,
}
},
})
jest.mock("fs", () => {
const originalFs = jest.requireActual("fs")
vi.mock("fs", async () => {
const originalFs = await vi.importActual<typeof import("fs")>("fs")
return {
...originalFs,
promises: {},
@@ -49,8 +50,8 @@ describe("ArtifactUploader", () => {
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
expect(result).toEqual({
"art1": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art1",
"art2": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art2",
art1: "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art1",
art2: "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art2",
})
expect(uploadMock).toHaveBeenCalledTimes(2)
})
@@ -117,8 +118,8 @@ describe("ArtifactUploader", () => {
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
expect(result).toEqual({
"art1": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art1",
"art2": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art2",
art1: "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art1",
art2: "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art2",
})
expect(uploadMock).toHaveBeenCalledTimes(2)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
@@ -138,8 +139,8 @@ describe("ArtifactUploader", () => {
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
expect(result).toEqual({
"art1": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art1",
"art2": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art2",
art1: "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art1",
art2: "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art2",
})
expect(uploadMock).toHaveBeenCalledTimes(2)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
@@ -201,8 +202,8 @@ describe("ArtifactUploader", () => {
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
expect(result).toEqual({
"art1": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art1",
"art2": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art2",
art1: "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art1",
art2: "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art2",
})
expect(uploadMock).toHaveBeenCalledTimes(2)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
@@ -212,26 +213,26 @@ describe("ArtifactUploader", () => {
})
function createUploader(replaces: boolean, throws = false): GithubArtifactUploader {
const MockReleases = jest.fn<Releases, any>(() => {
const MockReleases = vi.fn<() => Releases>(() => {
return {
create: jest.fn(),
create: vi.fn(),
deleteArtifact: deleteMock,
getByTag: jest.fn(),
getByTag: vi.fn(),
listArtifactsForRelease: listArtifactsMock,
listReleases: jest.fn(),
update: jest.fn(),
listReleases: vi.fn(),
update: vi.fn(),
uploadArtifact: uploadMock,
generateReleaseNotes: jest.fn(),
generateReleaseNotes: vi.fn(),
}
})
return new GithubArtifactUploader(new MockReleases(), replaces, throws)
return new GithubArtifactUploader(MockReleases(), replaces, throws)
}
function mockDeleteError(): any {
function mockDeleteError(): void {
deleteMock.mockRejectedValue("error")
}
function mockDeleteSuccess(): any {
function mockDeleteSuccess(): void {
deleteMock.mockResolvedValue({})
}

View File

@@ -1,4 +1,5 @@
import { GithubError } from "../src/GithubError"
import { describe, expect, it } from "vitest"
import { GithubError } from "../src/GithubError.js"
describe("ErrorMessage", () => {
describe("has error with code", () => {

View File

@@ -1,4 +1,5 @@
import { GithubErrorDetail } from "../src/GithubErrorDetail"
import { describe, expect, it } from "vitest"
import { GithubErrorDetail } from "../src/GithubErrorDetail.js"
describe("GithubErrorDetail", () => {
it("provides error code", () => {

View File

@@ -1,41 +1,56 @@
const mockGetInput = jest.fn()
const mockGetBooleanInput = jest.fn()
const mockGlob = jest.fn()
const mockReadFileSync = jest.fn()
const mockStatSync = jest.fn()
import * as fs from "node:fs"
import * as core from "@actions/core"
import type * as github from "@actions/github"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { Artifact } from "../src/Artifact"
import { ArtifactGlobber } from "../src/ArtifactGlobber"
import { Context } from "@actions/github/lib/context"
import { Inputs, CoreInputs } from "../src/Inputs"
vi.mock("@actions/core")
vi.mock("fs")
import { Artifact } from "../src/Artifact.js"
import type { ArtifactGlobber } from "../src/ArtifactGlobber.js"
import { CoreInputs, type Inputs } from "../src/Inputs.js"
const mockGetInput = vi.mocked(core.getInput)
const mockGetBooleanInput = vi.mocked(core.getBooleanInput)
const mockReadFileSync = vi.mocked(fs.readFileSync)
const _mockStatSync = vi.mocked(fs.statSync)
const mockExistsSync = vi.mocked(fs.existsSync)
const mockGlob = vi.fn()
// existsSync is used by Context's constructor
mockExistsSync.mockReturnValue(false)
const artifacts = [new Artifact("a/art1"), new Artifact("b/art2")]
jest.mock("@actions/core", () => {
return {
getInput: mockGetInput,
getBooleanInput: mockGetBooleanInput,
}
})
jest.mock("fs", () => {
// existsSync is used by Context's constructor
// noinspection JSUnusedGlobalSymbols
return {
existsSync: () => {
return false
},
readFileSync: mockReadFileSync,
statSync: mockStatSync,
}
})
describe("Inputs", () => {
let context: Context
let context: typeof github.context
let inputs: Inputs
beforeEach(() => {
mockGetInput.mockReset()
context = new Context()
mockGlob.mockClear()
context = {
payload: {},
eventName: "",
sha: "",
ref: "",
workflow: "",
action: "",
actor: "",
job: "",
runNumber: 0,
runId: 0,
runAttempt: 0,
apiUrl: "",
serverUrl: "",
graphqlUrl: "",
get repo() {
const repo = process.env.GITHUB_REPOSITORY || "/"
const [owner, repoName] = repo.split("/")
return { owner: owner || "", repo: repoName || "" }
},
issue: { owner: "", repo: "", number: 0 },
// biome-ignore lint/suspicious/noExplicitAny: Partial Context object for testing
} as any
inputs = new CoreInputs(createGlobber(), context)
})
@@ -189,17 +204,46 @@ describe("Inputs", () => {
})
describe("generateReleaseNotes", () => {
it("returns returns true", function () {
it("returns returns true", () => {
mockGetInput.mockReturnValue("true")
expect(inputs.generateReleaseNotes).toBe(true)
})
it("returns false when omitted", function () {
it("returns false when omitted", () => {
mockGetInput.mockReturnValue("")
expect(inputs.generateReleaseNotes).toBe(false)
})
})
describe("generateReleaseNotesPreviousTag", () => {
it("returns the previous tag when provided", () => {
mockGetInput.mockReturnValue("v1.0.0")
expect(inputs.generateReleaseNotesPreviousTag).toBe("v1.0.0")
})
it("returns undefined when omitted", () => {
mockGetInput.mockReturnValue("")
expect(inputs.generateReleaseNotesPreviousTag).toBeUndefined()
})
})
describe("immutableCreate", () => {
it("returns false by default", () => {
mockGetInput.mockReturnValue("")
expect(inputs.immutableCreate).toBe(false)
})
it("returns true when explicitly set", () => {
mockGetInput.mockReturnValue("true")
expect(inputs.immutableCreate).toBe(true)
})
it("returns false when explicitly disabled", () => {
mockGetInput.mockReturnValue("false")
expect(inputs.immutableCreate).toBe(false)
})
})
describe("makeLatest", () => {
it("returns legacy", () => {
mockGetInput.mockReturnValueOnce("legacy")
@@ -223,12 +267,12 @@ describe("Inputs", () => {
})
describe("owner", () => {
it("returns owner from context", function () {
it("returns owner from context", () => {
process.env.GITHUB_REPOSITORY = "owner/repo"
mockGetInput.mockReturnValue("")
expect(inputs.owner).toBe("owner")
})
it("returns owner from inputs", function () {
it("returns owner from inputs", () => {
mockGetInput.mockReturnValue("owner")
expect(inputs.owner).toBe("owner")
})
@@ -268,12 +312,12 @@ describe("Inputs", () => {
})
describe("repo", () => {
it("returns repo from context", function () {
it("returns repo from context", () => {
process.env.GITHUB_REPOSITORY = "owner/repo"
mockGetInput.mockReturnValue("")
expect(inputs.repo).toBe("repo")
})
it("returns repo from inputs", function () {
it("returns repo from inputs", () => {
mockGetInput.mockReturnValue("repo")
expect(inputs.repo).toBe("repo")
})
@@ -301,11 +345,6 @@ describe("Inputs", () => {
context.ref = "refs/tags/sha-tag"
expect(inputs.tag).toBe("sha-tag")
})
it("returns context sha when input is null", () => {
mockGetInput.mockReturnValue(null)
context.ref = "refs/tags/sha-tag"
expect(inputs.tag).toBe("sha-tag")
})
it("throws if no tag", () => {
context.ref = ""
expect(() => inputs.tag).toThrow()
@@ -433,12 +472,12 @@ describe("Inputs", () => {
})
function createGlobber(): ArtifactGlobber {
const MockGlobber = jest.fn<ArtifactGlobber, any>(() => {
const MockGlobber = vi.fn<() => ArtifactGlobber>(() => {
return {
globArtifactString: mockGlob,
}
})
mockGlob.mockImplementation(() => artifacts)
return new MockGlobber()
return MockGlobber()
}
})

View File

@@ -1,13 +1,14 @@
import { Action } from "../src/Action"
import * as path from "node:path"
import * as github from "@actions/github"
import { Inputs } from "../src/Inputs"
import { GithubReleases, ReleaseData } from "../src/Releases"
import { GithubArtifactUploader } from "../src/ArtifactUploader"
import * as path from "path"
import { FileArtifactGlobber } from "../src/ArtifactGlobber"
import { Outputs } from "../src/Outputs"
import { GithubArtifactDestroyer } from "../src/ArtifactDestroyer"
import { ReleaseActionSkipper } from "../src/ActionSkipper"
import { beforeEach, describe, it, vi } from "vitest"
import { Action } from "../src/Action.js"
import { ReleaseActionSkipper } from "../src/ActionSkipper.js"
import { GithubArtifactDestroyer } from "../src/ArtifactDestroyer.js"
import { FileArtifactGlobber } from "../src/ArtifactGlobber.js"
import { GithubArtifactUploader } from "../src/ArtifactUploader.js"
import type { Inputs } from "../src/Inputs.js"
import type { Outputs } from "../src/Outputs.js"
import { GithubReleases, type ReleaseData } from "../src/Releases.js"
// This test is currently intended to be manually run during development. To run:
// - Make sure you have an environment variable named GITHUB_TOKEN assigned to your token
@@ -34,7 +35,7 @@ describe.skip("Integration Test", () => {
})
function getInputs(): Inputs {
const MockInputs = jest.fn<Inputs, any>(() => {
const MockInputs = vi.fn<() => Inputs>(() => {
return {
allowUpdates: true,
artifactErrorsFailBuild: false,
@@ -45,6 +46,7 @@ describe.skip("Integration Test", () => {
commit: undefined,
discussionCategory: "Release",
generateReleaseNotes: true,
immutableCreate: true,
omitBodyDuringUpdate: false,
owner: "ncipollo",
createdPrerelease: false,
@@ -61,11 +63,11 @@ describe.skip("Integration Test", () => {
updateOnlyUnreleased: false,
}
})
return new MockInputs()
return MockInputs()
}
function getOutputs(): Outputs {
const MockOutputs = jest.fn<Outputs, any>(() => {
const MockOutputs = vi.fn<() => Outputs>(() => {
return {
applyReleaseData(releaseData: ReleaseData) {
console.log(`Release Data: ${releaseData}`)
@@ -75,7 +77,7 @@ describe.skip("Integration Test", () => {
},
}
})
return new MockOutputs()
return MockOutputs()
}
function artifacts() {

View File

@@ -1,8 +1,10 @@
import { CoreOutputs, type Outputs } from "../src/Outputs"
import type { ReleaseData } from "../src/Releases"
import * as core from "@actions/core"
import { describe, expect, it, vi } from "vitest"
import { CoreOutputs, type Outputs } from "../src/Outputs.js"
import type { ReleaseData } from "../src/Releases.js"
jest.mock("@actions/core")
const { setOutput: mockSetOutput } = jest.mocked(require("@actions/core"))
vi.mock("@actions/core")
const mockSetOutput = vi.mocked(core.setOutput)
const TEST_URLS = {
HTML_URL: "https://api.example.com/assets",

View File

@@ -0,0 +1,42 @@
import os from "node:os"
import { describe, expect, it } from "vitest"
import { expandTilde } from "../src/PathExpander.js"
describe("PathExpander", () => {
describe("expandTilde", () => {
it("expands ~ at the start of a path", () => {
const result = expandTilde("~/documents")
expect(result).toBe(`${os.homedir()}/documents`)
})
it("expands ~ with backslash separator", () => {
const result = expandTilde("~\\documents")
expect(result).toBe(`${os.homedir()}\\documents`)
})
it("expands standalone ~", () => {
const result = expandTilde("~")
expect(result).toBe(os.homedir())
})
it("does not expand ~ in the middle of a path", () => {
const result = expandTilde("/home/~user/documents")
expect(result).toBe("/home/~user/documents")
})
it("does not expand ~username patterns", () => {
const result = expandTilde("~username/documents")
expect(result).toBe("~username/documents")
})
it("returns path unchanged when no tilde present", () => {
const result = expandTilde("/absolute/path")
expect(result).toBe("/absolute/path")
})
it("returns relative path unchanged", () => {
const result = expandTilde("relative/path")
expect(result).toBe("relative/path")
})
})
})

View File

@@ -1,4 +1,5 @@
import { ReleaseValidator } from "../src/ReleaseValidator"
import { describe, expect, it } from "vitest"
import { ReleaseValidator } from "../src/ReleaseValidator.js"
describe("validateReleaseUpdate", () => {
describe("updateOnlyUnreleased is disabled", () => {

View File

@@ -47,6 +47,14 @@ inputs:
description: 'Indicates if release notes should be automatically generated.'
required: false
default: 'false'
generateReleaseNotesPreviousTag:
description: 'An optional previous tag to use when generating release notes. This will limit the release notes to changes between the two tags.'
required: false
default: ''
immutableCreate:
description: 'Indicates if immutable release creation should be used. When enabled, the action will first create a draft, upload artifacts, then publish the release.'
required: false
default: 'false'
makeLatest:
description: 'Indicates if the release should be the "latest" release or not.'
required: false
@@ -129,7 +137,7 @@ outputs:
assets:
description: 'JSON string containing a map of asset names to download URLs for uploaded assets.'
runs:
using: 'node20'
using: 'node24'
main: 'dist/index.js'
branding:
icon: 'tag'

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
@@ -7,7 +7,7 @@
},
"files": {
"ignoreUnknown": false,
"ignore": ["node_modules/**/*", "dist/"]
"includes": ["**", "!**/node_modules/**/*", "!**/dist/"]
},
"formatter": {
"enabled": true,
@@ -17,9 +17,9 @@
"indentWidth": 4,
"lineWidth": 120
},
"organizeImports": { "enabled": true },
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"linter": {
"enabled": false,
"enabled": true,
"rules": {
"recommended": true
}

80491
dist/index.js vendored

File diff suppressed because one or more lines are too long

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

211
dist/licenses.txt vendored
View File

@@ -71,28 +71,6 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@fastify/busboy
MIT
Copyright Brian White. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
@octokit/auth-token
MIT
The MIT License
@@ -265,31 +243,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
balanced-match
MIT
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
before-after-hook
Apache-2.0
Apache License
@@ -495,11 +448,14 @@ Apache-2.0
limitations under the License.
brace-expansion
fast-content-type-parse
MIT
MIT License
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
Copyright (c) 2023 The Fastify Team
The Fastify team members are listed at https://github.com/fastify/fastify#team
and in the README file.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -519,123 +475,16 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
deprecation
ISC
The ISC License
Copyright (c) Gregor Martynus and contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
glob
ISC
The ISC License
Copyright (c) 2009-2023 Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
lru-cache
ISC
The ISC License
Copyright (c) 2010-2023 Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
minimatch
ISC
The ISC License
Copyright (c) 2011-2023 Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
minipass
ISC
The ISC License
Copyright (c) 2017-2023 npm, Inc., Isaac Z. Schlueter, and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
once
ISC
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
path-scurry
BlueOak-1.0.0
All packages under `src/` are licensed according to the terms in
their respective `LICENSE` or `LICENSE.md` files.
The remainder of this project is licensed under the Blue Oak
Model License, as follows:
-----
# Blue Oak Model License
Version 1.0.0
@@ -747,40 +596,8 @@ universal-user-agent
ISC
# [ISC License](https://spdx.org/licenses/ISC)
Copyright (c) 2018, Gregor Martynus (https://github.com/gr2m)
Copyright (c) 2018-2021, Gregor Martynus (https://github.com/gr2m)
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
untildify
MIT
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
wrappy
ISC
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

3
dist/package.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

1
dist/sourcemap-register.cjs vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,66 +1,53 @@
{
"name": "release-action",
"version": "1.1.0",
"type": "module",
"private": true,
"description": "An action which manages a github release",
"main": "lib/main.js",
"main": "lib/src/Main.js",
"scripts": {
"build": "tsc",
"clean": "rm -rf lib/*",
"coverage": "jest --coverage",
"debug": "yarn clean && yarn install && yarn build && yarn package",
"format": "yarn biome format --write .",
"coverage": "vitest run --coverage",
"debug": "pnpm clean && pnpm install && pnpm build && pnpm package",
"format": "pnpm biome format --write .",
"package": "ncc build --source-map --license licenses.txt",
"release": "yarn clean && yarn install --production && yarn build && yarn package",
"test": "jest"
"release": "pnpm clean && pnpm install && pnpm build && pnpm package",
"test": "vitest run"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ncipollo/release-action.git"
},
"keywords": ["actions", "node", "setup"],
"keywords": [
"actions",
"node",
"setup"
],
"author": "GitHub",
"license": "MIT",
"engines": {
"node": ">=20"
},
"jest": {
"clearMocks": true,
"collectCoverage": true,
"coveragePathIgnorePatterns": ["src/Globber.ts", "src/Releases.ts"],
"coverageThreshold": {
"global": {
"branches": 95,
"functions": 100,
"lines": 100,
"statements": 100
}
},
"moduleFileExtensions": ["js", "ts"],
"testEnvironment": "node",
"testMatch": ["**/*.test.ts"],
"testRunner": "jest-circus/runner",
"transform": {
"^.+\\.ts$": "ts-jest"
},
"verbose": true
},
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.1",
"@types/node": "^22.15.29",
"glob": "^11.0.2",
"untildify": "^4.0.0"
"@actions/core": "^3.0.0",
"@actions/github": "^9.0.0",
"@types/node": "^25.1.0",
"glob": "^13.0.6"
},
"pnpm": {
"overrides": {
"undici": ">=6.24.0"
}
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
"jest-circus": "^29.7.0",
"ts-jest": "^29.3.4",
"typescript": "^5.8.3"
},
"resolutions": {
"jest-cli/yargs": "^17.3.1"
"@biomejs/biome": "2.4.4",
"@octokit/plugin-rest-endpoint-methods": "^10.4.1",
"@octokit/request-error": "^7.1.0",
"@octokit/types": "^16.0.0",
"@vercel/ncc": "^0.38.4",
"@vitest/coverage-v8": "^4.1.0",
"typescript": "^5.9.3",
"vitest": "^4.1.0"
}
}

1441
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,4 +3,4 @@ enable_commit = true
tag_pattern = "v{version}"
[scripts]
before_commit = "yarn release"
before_commit = "pnpm release"

View File

@@ -1,18 +1,18 @@
import * as core from "@actions/core"
import type { ActionSkipper } from "./ActionSkipper"
import type { ArtifactDestroyer } from "./ArtifactDestroyer"
import type { ArtifactUploader } from "./ArtifactUploader"
import { GithubError } from "./GithubError"
import type { Inputs } from "./Inputs"
import type { Outputs } from "./Outputs"
import { ReleaseValidator } from "./ReleaseValidator"
import type { ActionSkipper } from "./ActionSkipper.js"
import type { ArtifactDestroyer } from "./ArtifactDestroyer.js"
import type { ArtifactUploader } from "./ArtifactUploader.js"
import { GithubError } from "./GithubError.js"
import type { Inputs } from "./Inputs.js"
import type { Outputs } from "./Outputs.js"
import { ReleaseValidator } from "./ReleaseValidator.js"
import type {
CreateOrUpdateReleaseResponse,
CreateReleaseResponse,
ReleaseByTagResponse,
Releases,
UpdateReleaseResponse,
} from "./Releases"
} from "./Releases.js"
export class Action {
private inputs: Inputs
@@ -47,83 +47,47 @@ export class Action {
return
}
const releaseResponse = await this.createOrUpdateRelease()
const releaseData = releaseResponse.data
const releaseId = releaseData.id
const uploadUrl = releaseData.upload_url
if (this.inputs.removeArtifacts) {
await this.artifactDestroyer.destroyArtifacts(releaseId)
}
const artifacts = this.inputs.artifacts
let assetUrls: Record<string, string> = {}
if (artifacts.length > 0) {
assetUrls = await this.uploader.uploadArtifacts(artifacts, releaseId, uploadUrl)
}
this.outputs.applyReleaseData(releaseData)
this.outputs.applyAssetUrls(assetUrls)
await this.createOrUpdateRelease()
}
private async createOrUpdateRelease(): Promise<CreateOrUpdateReleaseResponse> {
private async createOrUpdateRelease() {
if (this.inputs.allowUpdates) {
let getResponse: ReleaseByTagResponse
try {
getResponse = await this.releases.getByTag(this.inputs.tag)
} catch (error: any) {
return await this.checkForMissingReleaseError(error)
await this.checkForMissingReleaseError(error)
return
}
// Fail if this isn't an unreleased release & updateOnlyUnreleased is enabled.
this.releaseValidator.validateReleaseUpdate(getResponse.data)
return await this.updateRelease(getResponse.data.id)
await this.updateRelease(getResponse.data.id)
} else {
return await this.createRelease()
await this.createRelease()
}
}
private async checkForMissingReleaseError(error: Error): Promise<CreateOrUpdateReleaseResponse> {
private async checkForMissingReleaseError(error: Error): Promise<void> {
if (Action.noPublishedRelease(error)) {
return await this.updateDraftOrCreateRelease()
await this.updateDraftOrCreateRelease()
} else {
throw error
}
}
private async updateRelease(id: number): Promise<UpdateReleaseResponse> {
let releaseBody = this.inputs.updatedReleaseBody
if (this.inputs.generateReleaseNotes && !this.inputs.omitBodyDuringUpdate) {
const response = await this.releases.generateReleaseNotes(this.inputs.tag)
releaseBody = response.data.body
}
return await this.releases.update(
id,
this.inputs.tag,
releaseBody,
this.inputs.commit,
this.inputs.discussionCategory,
this.inputs.updatedDraft,
this.inputs.makeLatest,
this.inputs.updatedReleaseName,
this.inputs.updatedPrerelease
)
}
private static noPublishedRelease(error: any): boolean {
const githubError = new GithubError(error)
return githubError.status == 404
}
private async updateDraftOrCreateRelease(): Promise<CreateReleaseResponse | UpdateReleaseResponse> {
private async updateDraftOrCreateRelease(): Promise<void> {
const draftReleaseId = await this.findMatchingDraftReleaseId()
if (draftReleaseId) {
return await this.updateRelease(draftReleaseId)
await this.updateRelease(draftReleaseId)
} else {
return await this.createRelease()
await this.createRelease()
}
}
@@ -140,17 +104,111 @@ export class Action {
return draftRelease?.id
}
private async createRelease(): Promise<CreateReleaseResponse> {
return await this.releases.create(
private async updateRelease(id: number) {
const releaseBody = await this.combineBodyWithReleaseNotes(this.inputs.updatedReleaseBody, true)
const releaseResponse = await this.releases.update(
id,
this.inputs.tag,
this.inputs.createdReleaseBody,
releaseBody,
this.inputs.commit,
this.inputs.discussionCategory,
this.inputs.createdDraft,
this.inputs.generateReleaseNotes,
this.inputs.updatedDraft,
this.inputs.makeLatest,
this.inputs.updatedReleaseName,
this.inputs.updatedPrerelease
)
await this.processReleaseArtifactsAndOutputs(releaseResponse, false)
}
private async createRelease() {
const releaseBody = await this.combineBodyWithReleaseNotes(this.inputs.createdReleaseBody, false)
// If immutableCreate is enabled we need to start with a draft release
const draft = this.inputs.createdDraft || this.inputs.immutableCreate
const releaseResponse = await this.releases.create(
this.inputs.tag,
releaseBody,
this.inputs.commit,
this.inputs.discussionCategory,
draft,
this.inputs.makeLatest,
this.inputs.createdReleaseName,
this.inputs.createdPrerelease
)
await this.processReleaseArtifactsAndOutputs(releaseResponse, true)
}
private async processReleaseArtifactsAndOutputs(releaseResponse: CreateOrUpdateReleaseResponse, wasCreated: boolean) {
const releaseData = releaseResponse.data
const releaseId = releaseData.id
const uploadUrl = releaseData.upload_url
if (this.inputs.removeArtifacts) {
await this.artifactDestroyer.destroyArtifacts(releaseId)
}
const artifacts = this.inputs.artifacts
let assetUrls: Record<string, string> = {}
if (artifacts.length > 0) {
assetUrls = await this.uploader.uploadArtifacts(artifacts, releaseId, uploadUrl)
}
if (wasCreated) {
const immutableRelease = await this.publishImmutableRelease(releaseId)
if (immutableRelease) {
this.setOutputs(immutableRelease.data, assetUrls)
return
}
}
this.setOutputs(releaseData, assetUrls)
}
private async publishImmutableRelease(releaseId: number): Promise<CreateOrUpdateReleaseResponse | undefined> {
// Check if immutableCreate is on and createdDraft is off
if (!this.inputs.immutableCreate || this.inputs.createdDraft) {
return undefined
}
return await this.releases.update(
releaseId,
this.inputs.tag,
undefined, // body is omitted
undefined, // commit is omitted
this.inputs.discussionCategory,
false, // We want to publish the release, set draft to false
this.inputs.makeLatest,
this.inputs.createdReleaseName,
this.inputs.createdPrerelease
)
}
private async combineBodyWithReleaseNotes(body: string | undefined, isUpdate: boolean): Promise<string | undefined> {
// Determine if we should generate release notes based on operation type
const shouldGenerateReleaseNotes = isUpdate
? this.inputs.generateReleaseNotes && !this.inputs.omitBodyDuringUpdate
: this.inputs.generateReleaseNotes
if (!shouldGenerateReleaseNotes) {
return body
}
const response = await this.releases.generateReleaseNotes(this.inputs.tag, this.inputs.generateReleaseNotesPreviousTag, this.inputs.commit)
const releaseNotes = response.data.body
if (!body || body.trim() === "") {
return releaseNotes
}
return `${body}\n${releaseNotes}`
}
private setOutputs(releaseData: any, assetUrls: Record<string, string>): void {
this.outputs.applyReleaseData(releaseData)
this.outputs.applyAssetUrls(assetUrls)
}
}

View File

@@ -1,4 +1,4 @@
import { Releases } from "./Releases"
import { Releases } from "./Releases.js"
export interface ActionSkipper {
shouldSkip(): Promise<boolean>

View File

@@ -1,4 +1,4 @@
import { Releases } from "./Releases"
import { Releases } from "./Releases.js"
import * as core from "@actions/core"
export interface ArtifactDestroyer {

View File

@@ -1,9 +1,9 @@
import * as core from "@actions/core"
import { Globber, FileGlobber } from "./Globber"
import { Artifact } from "./Artifact"
import untildify from "untildify"
import { ArtifactPathValidator } from "./ArtifactPathValidator"
import { PathNormalizer } from "./PathNormalizer"
import { Globber, FileGlobber } from "./Globber.js"
import { Artifact } from "./Artifact.js"
import { expandTilde } from "./PathExpander.js"
import { ArtifactPathValidator } from "./ArtifactPathValidator.js"
import { PathNormalizer } from "./PathNormalizer.js"
export interface ArtifactGlobber {
globArtifactString(artifact: string, contentType: string, errorsFailBuild: boolean): Artifact[]
@@ -55,6 +55,6 @@ export class FileArtifactGlobber implements ArtifactGlobber {
}
private static expandPath(path: string): string {
return untildify(path)
return expandTilde(path)
}
}

View File

@@ -1,6 +1,6 @@
import * as core from "@actions/core"
import { Artifact } from "./Artifact"
import { Releases } from "./Releases"
import { Artifact } from "./Artifact.js"
import { Releases } from "./Releases.js"
export interface ArtifactUploader {
uploadArtifacts(artifacts: Artifact[], releaseId: number, uploadUrl: string): Promise<Record<string, string>>

View File

@@ -1,4 +1,4 @@
import { GithubErrorDetail } from "./GithubErrorDetail"
import { GithubErrorDetail } from "./GithubErrorDetail.js"
export class GithubError {
private error: any

View File

@@ -1,8 +1,8 @@
import { readFileSync } from "node:fs"
import * as core from "@actions/core"
import { Context } from "@actions/github/lib/context"
import { readFileSync } from "fs"
import { ArtifactGlobber } from "./ArtifactGlobber"
import { Artifact } from "./Artifact"
import type * as github from "@actions/github"
import type { Artifact } from "./Artifact.js"
import type { ArtifactGlobber } from "./ArtifactGlobber.js"
export interface Inputs {
readonly allowUpdates: boolean
@@ -15,6 +15,8 @@ export interface Inputs {
readonly createdReleaseName?: string
readonly discussionCategory?: string
readonly generateReleaseNotes: boolean
readonly generateReleaseNotesPreviousTag?: string
readonly immutableCreate: boolean
readonly makeLatest?: "legacy" | "true" | "false" | undefined
readonly omitBodyDuringUpdate: boolean
readonly owner: string
@@ -33,16 +35,16 @@ export interface Inputs {
export class CoreInputs implements Inputs {
private artifactGlobber: ArtifactGlobber
private context: Context
private context: typeof github.context
constructor(artifactGlobber: ArtifactGlobber, context: Context) {
constructor(artifactGlobber: ArtifactGlobber, context: typeof github.context) {
this.artifactGlobber = artifactGlobber
this.context = context
}
get allowUpdates(): boolean {
const allow = core.getInput("allowUpdates")
return allow == "true"
return allow === "true"
}
get artifacts(): Artifact[] {
@@ -62,7 +64,7 @@ export class CoreInputs implements Inputs {
get artifactErrorsFailBuild(): boolean {
const allow = core.getInput("artifactErrorsFailBuild")
return allow == "true"
return allow === "true"
}
private get body(): string | undefined {
@@ -81,12 +83,12 @@ export class CoreInputs implements Inputs {
get createdDraft(): boolean {
const draft = core.getInput("draft")
return draft == "true"
return draft === "true"
}
get createdPrerelease(): boolean {
const preRelease = core.getInput("prerelease")
return preRelease == "true"
return preRelease === "true"
}
get createdReleaseBody(): string | undefined {
@@ -95,7 +97,7 @@ export class CoreInputs implements Inputs {
}
private static get omitBody(): boolean {
return core.getInput("omitBody") == "true"
return core.getInput("omitBody") === "true"
}
get createdReleaseName(): string | undefined {
@@ -104,7 +106,7 @@ export class CoreInputs implements Inputs {
}
private static get omitName(): boolean {
return core.getInput("omitName") == "true"
return core.getInput("omitName") === "true"
}
get commit(): string | undefined {
@@ -134,12 +136,22 @@ export class CoreInputs implements Inputs {
get generateReleaseNotes(): boolean {
const generate = core.getInput("generateReleaseNotes")
return generate == "true"
return generate === "true"
}
get generateReleaseNotesPreviousTag(): string | undefined {
const previousTag = core.getInput("generateReleaseNotesPreviousTag")
return previousTag || undefined
}
get immutableCreate(): boolean {
const immutable = core.getInput("immutableCreate")
return immutable === "true"
}
get makeLatest(): "legacy" | "true" | "false" | undefined {
let latest = core.getInput("makeLatest")
if (latest == "true" || latest == "false" || latest == "legacy") {
const latest = core.getInput("makeLatest")
if (latest === "true" || latest === "false" || latest === "legacy") {
return latest
}
@@ -147,7 +159,7 @@ export class CoreInputs implements Inputs {
}
get owner(): string {
let owner = core.getInput("owner")
const owner = core.getInput("owner")
if (owner) {
return owner
}
@@ -156,16 +168,16 @@ export class CoreInputs implements Inputs {
get removeArtifacts(): boolean {
const removes = core.getInput("removeArtifacts")
return removes == "true"
return removes === "true"
}
get replacesArtifacts(): boolean {
const replaces = core.getInput("replacesArtifacts")
return replaces == "true"
return replaces === "true"
}
get repo(): string {
let repo = core.getInput("repo")
const repo = core.getInput("repo")
if (repo) {
return repo
}
@@ -184,7 +196,7 @@ export class CoreInputs implements Inputs {
const ref = this.context.ref
const tagPath = "refs/tags/"
if (ref && ref.startsWith(tagPath)) {
if (ref?.startsWith(tagPath)) {
return ref.substr(tagPath.length, ref.length)
}
@@ -201,7 +213,7 @@ export class CoreInputs implements Inputs {
}
private static get omitDraftDuringUpdate(): boolean {
return core.getInput("omitDraftDuringUpdate") == "true"
return core.getInput("omitDraftDuringUpdate") === "true"
}
get updatedPrerelease(): boolean | undefined {
@@ -210,7 +222,7 @@ export class CoreInputs implements Inputs {
}
private static get omitPrereleaseDuringUpdate(): boolean {
return core.getInput("omitPrereleaseDuringUpdate") == "true"
return core.getInput("omitPrereleaseDuringUpdate") === "true"
}
get updatedReleaseBody(): string | undefined {
@@ -219,7 +231,7 @@ export class CoreInputs implements Inputs {
}
get updateOnlyUnreleased(): boolean {
return core.getInput("updateOnlyUnreleased") == "true"
return core.getInput("updateOnlyUnreleased") === "true"
}
get updatedReleaseName(): string | undefined {
@@ -228,7 +240,7 @@ export class CoreInputs implements Inputs {
}
private static get omitBodyDuringUpdate(): boolean {
return core.getInput("omitBodyDuringUpdate") == "true"
return core.getInput("omitBodyDuringUpdate") === "true"
}
get omitBodyDuringUpdate(): boolean {
@@ -236,7 +248,7 @@ export class CoreInputs implements Inputs {
}
private static get omitNameDuringUpdate(): boolean {
return core.getInput("omitNameDuringUpdate") == "true"
return core.getInput("omitNameDuringUpdate") === "true"
}
stringFromFile(path: string): string {

View File

@@ -1,14 +1,14 @@
import * as github from "@actions/github"
import * as core from "@actions/core"
import { CoreInputs } from "./Inputs"
import { GithubReleases } from "./Releases"
import { Action } from "./Action"
import { GithubArtifactUploader } from "./ArtifactUploader"
import { FileArtifactGlobber } from "./ArtifactGlobber"
import { GithubError } from "./GithubError"
import { CoreOutputs } from "./Outputs"
import { GithubArtifactDestroyer } from "./ArtifactDestroyer"
import { ActionSkipper, ReleaseActionSkipper } from "./ActionSkipper"
import { CoreInputs } from "./Inputs.js"
import { GithubReleases } from "./Releases.js"
import { Action } from "./Action.js"
import { GithubArtifactUploader } from "./ArtifactUploader.js"
import { FileArtifactGlobber } from "./ArtifactGlobber.js"
import { GithubError } from "./GithubError.js"
import { CoreOutputs } from "./Outputs.js"
import { GithubArtifactDestroyer } from "./ArtifactDestroyer.js"
import { ActionSkipper, ReleaseActionSkipper } from "./ActionSkipper.js"
async function run() {
try {

View File

@@ -1,5 +1,5 @@
import * as core from "@actions/core"
import { ReleaseData } from "./Releases"
import { ReleaseData } from "./Releases.js"
export interface Outputs {
applyReleaseData(releaseData: ReleaseData): void

6
src/PathExpander.ts Normal file
View File

@@ -0,0 +1,6 @@
import os from "os"
export function expandTilde(path: string): string {
return path.replace(/^~(?=$|\/|\\)/, os.homedir())
}

View File

@@ -1,7 +1,7 @@
import type { GitHub } from "@actions/github/lib/utils"
import * as github from "@actions/github"
import type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods"
import type { OctokitResponse } from "@octokit/types"
import type { Inputs } from "./Inputs"
import type { Inputs } from "./Inputs.js"
export type CreateReleaseResponse = RestEndpointMethodTypes["repos"]["createRelease"]["response"]
export type ReleaseByTagResponse = RestEndpointMethodTypes["repos"]["getReleaseByTag"]["response"]
@@ -27,7 +27,6 @@ export interface Releases {
commitHash?: string,
discussionCategory?: string,
draft?: boolean,
generateReleaseNotes?: boolean,
makeLatest?: "legacy" | "true" | "false" | undefined,
name?: string,
prerelease?: boolean
@@ -37,7 +36,7 @@ export interface Releases {
getByTag(tag: string): Promise<ReleaseByTagResponse>
generateReleaseNotes(tag: string): Promise<GenerateReleaseNotesResponse>
generateReleaseNotes(tag: string, previousTag?: string, targetCommitish?: string): Promise<GenerateReleaseNotesResponse>
listArtifactsForRelease(releaseId: number): Promise<ListReleaseAssetsResponseData>
@@ -66,10 +65,10 @@ export interface Releases {
}
export class GithubReleases implements Releases {
git: InstanceType<typeof GitHub>
git: ReturnType<typeof github.getOctokit>
inputs: Inputs
constructor(inputs: Inputs, git: InstanceType<typeof GitHub>) {
constructor(inputs: Inputs, git: ReturnType<typeof github.getOctokit>) {
this.inputs = inputs
this.git = git
}
@@ -80,7 +79,6 @@ export class GithubReleases implements Releases {
commitHash?: string,
discussionCategory?: string,
draft?: boolean,
generateReleaseNotes?: boolean,
makeLatest?: "legacy" | "true" | "false" | undefined,
name?: string,
prerelease?: boolean
@@ -91,7 +89,6 @@ export class GithubReleases implements Releases {
name: name,
discussion_category_name: discussionCategory,
draft: draft,
generate_release_notes: generateReleaseNotes,
make_latest: makeLatest,
owner: this.inputs.owner,
prerelease: prerelease,
@@ -109,12 +106,22 @@ export class GithubReleases implements Releases {
})
}
async generateReleaseNotes(tag: string): Promise<GenerateReleaseNotesResponse> {
return this.git.rest.repos.generateReleaseNotes({
async generateReleaseNotes(tag: string, previousTag?: string, targetCommitish?: string): Promise<GenerateReleaseNotesResponse> {
const params: any = {
owner: this.inputs.owner,
repo: this.inputs.repo,
tag_name: tag,
})
}
if (previousTag) {
params.previous_tag_name = previousTag
}
if (targetCommitish) {
params.target_commitish = targetCommitish
}
return this.git.rest.repos.generateReleaseNotes(params)
}
async getByTag(tag: string): Promise<ReleaseByTagResponse> {

View File

@@ -4,10 +4,13 @@
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./lib",
"rootDir": ".",
"strict": true,
"skipLibCheck": true,
"noImplicitAny": true,
"esModuleInterop": true
"esModuleInterop": true,
"types": ["node"]
},
"exclude": ["node_modules", "**/*.test.ts"]
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts", "__mocks__/**/*", "vitest.config.ts"]
}

7
tsconfig.test.json Normal file
View File

@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"isolatedModules": true
},
"include": ["src/**/*", "__tests__/**/*"]
}

21
vitest.config.ts Normal file
View File

@@ -0,0 +1,21 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov'],
include: ['src/**/*.ts'],
exclude: ['src/Globber.ts', 'src/Releases.ts'],
thresholds: {
lines: 100,
functions: 100,
branches: 95,
statements: 100,
},
},
},
})

2974
yarn.lock

File diff suppressed because it is too large Load Diff