9 Commits

Author SHA1 Message Date
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
Nick Cipollo
bcfe547070 preparing release 1.18.0
Some checks failed
Release / Create Release (push) Has been cancelled
Test / check_pr (push) Has been cancelled
2025-06-29 16:28:21 -04:00
Nick Cipollo
707331a88d Fixes #529 Collect asset URLs into output 2025-06-29 16:12:11 -04:00
22 changed files with 1690 additions and 1128 deletions

View File

@@ -11,7 +11,7 @@ jobs:
check_pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:

View File

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

View File

@@ -5,7 +5,7 @@ jobs:
check_pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:

View File

@@ -25,6 +25,7 @@ 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 |
| 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 | true |
| 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 |
@@ -51,6 +52,7 @@ This action will create a GitHub release and optionally upload an artifact to it
| upload_url | The URL for [uploading assets](https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset) to the release. |
| tarball_url | The URL for downloading the release as a tarball (.tar.gz). |
| zipball_url | The URL for downloading the release as a zipball (.zip). |
| assets | JSON string containing a map of asset names to download URLs for uploaded assets. |
## Example
This example will create a release when a tag is pushed:

View File

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

View File

@@ -15,6 +15,7 @@ const TEST_URLS = {
} as const
const applyReleaseDataMock = jest.fn()
const applyAssetUrlsMock = jest.fn()
const artifactDestroyMock = jest.fn()
const createMock = jest.fn()
const deleteMock = jest.fn()
@@ -51,7 +52,10 @@ const generatedReleaseBody = "test release notes"
describe("Action", () => {
beforeEach(() => {
applyReleaseDataMock.mockClear()
applyAssetUrlsMock.mockClear()
createMock.mockClear()
genReleaseNotesMock.mockClear()
getMock.mockClear()
listMock.mockClear()
shouldSkipMock.mockClear()
@@ -59,24 +63,88 @@ 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)
expect(createMock).toHaveBeenCalledWith(
tag,
createBody,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
generateReleaseNotes,
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, "existing body")
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag)
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 but does not upload if no artifact", async () => {
const action = createAction(false, false)
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag)
expect(createMock).toHaveBeenCalledWith(
tag,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
makeLatest,
createName,
createPrerelease
)
expect(uploadMock).not.toHaveBeenCalled()
assertOutputApplied()
assertAssetUrlsApplied({})
})
it("creates release if no release exists to update", async () => {
@@ -86,19 +154,23 @@ describe("Action", () => {
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag)
expect(createMock).toHaveBeenCalledWith(
tag,
createBody,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
generateReleaseNotes,
makeLatest,
createName,
createPrerelease
)
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("creates release if no draft releases", async () => {
@@ -111,19 +183,23 @@ describe("Action", () => {
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag)
expect(createMock).toHaveBeenCalledWith(
tag,
createBody,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
generateReleaseNotes,
makeLatest,
createName,
createPrerelease
)
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("creates release then uploads artifact", async () => {
@@ -131,19 +207,23 @@ describe("Action", () => {
await action.perform()
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag)
expect(createMock).toHaveBeenCalledWith(
tag,
createBody,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
generateReleaseNotes,
makeLatest,
createName,
createPrerelease
)
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("removes all artifacts when artifact destroyer is enabled", async () => {
@@ -153,6 +233,10 @@ 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",
})
})
it("removes no artifacts when artifact destroyer is disabled", async () => {
@@ -162,6 +246,10 @@ 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",
})
})
it("skips action", async () => {
@@ -185,13 +273,13 @@ describe("Action", () => {
expect(error).toEqual("error")
}
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag)
expect(createMock).toHaveBeenCalledWith(
tag,
createBody,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
generateReleaseNotes,
makeLatest,
createName,
createPrerelease
@@ -286,13 +374,13 @@ describe("Action", () => {
expect(error).toEqual(expectedError)
}
expect(genReleaseNotesMock).toHaveBeenCalledWith(tag)
expect(createMock).toHaveBeenCalledWith(
tag,
createBody,
generatedReleaseBody,
commit,
discussionCategory,
createDraft,
generateReleaseNotes,
makeLatest,
createName,
createPrerelease
@@ -326,6 +414,10 @@ 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",
})
})
it("updates draft release with static body", async () => {
@@ -354,6 +446,10 @@ 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",
})
})
it("updates release but does not upload if no artifact", async () => {
@@ -374,6 +470,7 @@ describe("Action", () => {
)
expect(uploadMock).not.toHaveBeenCalled()
assertOutputApplied()
assertAssetUrlsApplied({})
})
it("updates release then uploads artifact", async () => {
@@ -394,6 +491,10 @@ 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",
})
})
it("updates release with static body when generateReleaseNotes is true but omitBodyDuringUpdate is true", async () => {
@@ -422,6 +523,149 @@ 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",
})
})
it("does not publish immutable release when immutableCreate is false", async () => {
const action = createAction(false, true, false, true, false, createBody, 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, createBody, 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, createBody, 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, createBody, 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",
})
})
function assertOutputApplied() {
@@ -434,12 +678,19 @@ describe("Action", () => {
})
}
function assertAssetUrlsApplied(expectedUrls: Record<string, string>) {
expect(applyAssetUrlsMock).toHaveBeenCalledWith(expectedUrls)
}
function createAction(
allowUpdates: boolean,
hasArtifact: boolean,
removeArtifacts = false,
generateReleaseNotes = true,
omitBodyDuringUpdate = false
omitBodyDuringUpdate = false,
createdReleaseBody = createBody,
immutableCreate = true,
createdDraft = createDraft
): Action {
let inputArtifact: Artifact[]
@@ -495,19 +746,23 @@ describe("Action", () => {
zipball_url: TEST_URLS.ZIPBALL_URL,
},
})
uploadMock.mockResolvedValue({})
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",
})
const MockInputs = jest.fn<Inputs, any>(() => {
return {
allowUpdates,
artifactErrorsFailBuild: true,
artifacts: inputArtifact,
createdDraft: createDraft,
createdReleaseBody: createBody,
createdDraft: createdDraft,
createdReleaseBody: createdReleaseBody,
createdReleaseName: createName,
commit,
discussionCategory,
generateReleaseNotes,
immutableCreate: immutableCreate,
makeLatest: makeLatest,
owner: "owner",
createdPrerelease: createPrerelease,
@@ -528,6 +783,7 @@ describe("Action", () => {
const MockOutputs = jest.fn<Outputs, any>(() => {
return {
applyReleaseData: applyReleaseDataMock,
applyAssetUrls: applyAssetUrlsMock,
}
})
const MockUploader = jest.fn<ArtifactUploader, any>(() => {

View File

@@ -13,6 +13,15 @@ const deleteMock = jest.fn()
const listArtifactsMock = jest.fn()
const uploadMock = jest.fn()
// Mock response with browser_download_url
const mockUploadResponse = (name: string) => ({
data: {
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")
return {
@@ -32,13 +41,48 @@ describe("ArtifactUploader", () => {
uploadMock.mockClear()
})
it("returns asset URLs when upload succeeds", async () => {
mockListWithoutAssets()
mockUploadSuccess()
const uploader = createUploader(true)
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",
})
expect(uploadMock).toHaveBeenCalledTimes(2)
})
it("returns empty object when no artifacts are uploaded", async () => {
const uploader = createUploader(true)
const result = await uploader.uploadArtifacts([], releaseId, url)
expect(result).toEqual({})
expect(uploadMock).toHaveBeenCalledTimes(0)
})
it("excludes failed uploads from returned URLs", async () => {
mockListWithoutAssets()
mockUploadArtifact(401, 2)
const uploader = createUploader(true)
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
expect(result).toEqual({})
expect(uploadMock).toHaveBeenCalledTimes(2)
})
it("abort when upload failed with non-5xx response", async () => {
mockListWithoutAssets()
mockUploadArtifact(401, 2)
const uploader = createUploader(true)
await uploader.uploadArtifacts(artifacts, releaseId, url)
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
expect(result).toEqual({})
expect(uploadMock).toHaveBeenCalledTimes(2)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art2", releaseId)
@@ -51,8 +95,9 @@ describe("ArtifactUploader", () => {
mockUploadArtifact(500, 4)
const uploader = createUploader(true)
await uploader.uploadArtifacts(artifacts, releaseId, url)
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
expect(result).toEqual({})
expect(uploadMock).toHaveBeenCalledTimes(5)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
@@ -66,11 +111,15 @@ describe("ArtifactUploader", () => {
it("replaces all artifacts", async () => {
mockDeleteSuccess()
mockListWithAssets()
mockUploadArtifact()
mockUploadSuccess()
const uploader = createUploader(true)
await uploader.uploadArtifacts(artifacts, releaseId, url)
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",
})
expect(uploadMock).toHaveBeenCalledTimes(2)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art2", releaseId)
@@ -83,11 +132,15 @@ describe("ArtifactUploader", () => {
it("replaces no artifacts when previous asset list empty", async () => {
mockDeleteSuccess()
mockListWithoutAssets()
mockUploadArtifact()
mockUploadSuccess()
const uploader = createUploader(true)
await uploader.uploadArtifacts(artifacts, releaseId, url)
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",
})
expect(uploadMock).toHaveBeenCalledTimes(2)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art2", releaseId)
@@ -100,8 +153,9 @@ describe("ArtifactUploader", () => {
mockUploadArtifact(500, 2)
const uploader = createUploader(true)
await uploader.uploadArtifacts(artifacts, releaseId, url)
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
expect(result).toEqual({})
expect(uploadMock).toHaveBeenCalledTimes(4)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
@@ -127,7 +181,7 @@ describe("ArtifactUploader", () => {
it("throws error from replace", async () => {
mockDeleteError()
mockListWithAssets()
mockUploadArtifact()
mockUploadSuccess()
const uploader = createUploader(true)
expect.hasAssertions()
@@ -141,11 +195,15 @@ describe("ArtifactUploader", () => {
it("updates all artifacts, delete none", async () => {
mockDeleteError()
mockListWithAssets()
mockUploadArtifact()
mockUploadSuccess()
const uploader = createUploader(false)
await uploader.uploadArtifacts(artifacts, releaseId, url)
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",
})
expect(uploadMock).toHaveBeenCalledTimes(2)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art2", releaseId)
@@ -194,6 +252,10 @@ describe("ArtifactUploader", () => {
listArtifactsMock.mockResolvedValue([])
}
function mockUploadSuccess() {
uploadMock.mockImplementation((_, __, ___, ____, name) => Promise.resolve(mockUploadResponse(name)))
}
function mockUploadArtifact(status = 200, failures = 0) {
const error = new RequestError(`HTTP ${status}`, status, {
headers: {},

View File

@@ -200,6 +200,23 @@ describe("Inputs", () => {
})
})
describe("immutableCreate", () => {
it("returns false by default", function () {
mockGetInput.mockReturnValue("")
expect(inputs.immutableCreate).toBe(false)
})
it("returns true when explicitly set", function () {
mockGetInput.mockReturnValue("true")
expect(inputs.immutableCreate).toBe(true)
})
it("returns false when explicitly disabled", function () {
mockGetInput.mockReturnValue("false")
expect(inputs.immutableCreate).toBe(false)
})
})
describe("makeLatest", () => {
it("returns legacy", () => {
mockGetInput.mockReturnValueOnce("legacy")

View File

@@ -45,6 +45,7 @@ describe.skip("Integration Test", () => {
commit: undefined,
discussionCategory: "Release",
generateReleaseNotes: true,
immutableCreate: true,
omitBodyDuringUpdate: false,
owner: "ncipollo",
createdPrerelease: false,
@@ -70,6 +71,9 @@ describe.skip("Integration Test", () => {
applyReleaseData(releaseData: ReleaseData) {
console.log(`Release Data: ${releaseData}`)
},
applyAssetUrls(assetUrls: Record<string, string>) {
console.log(`Asset URLs: ${JSON.stringify(assetUrls)}`)
},
}
})
return new MockOutputs()

View File

@@ -1,11 +1,8 @@
const mockSetOutput = jest.fn()
import { CoreOutputs, type Outputs } from "../src/Outputs"
import type { ReleaseData } from "../src/Releases"
import { CoreOutputs, Outputs } from "../src/Outputs"
import { ReleaseData } from "../src/Releases"
jest.mock("@actions/core", () => {
return { setOutput: mockSetOutput }
})
jest.mock("@actions/core")
const { setOutput: mockSetOutput } = jest.mocked(require("@actions/core"))
const TEST_URLS = {
HTML_URL: "https://api.example.com/assets",
@@ -48,4 +45,19 @@ describe("Outputs", () => {
expect(mockSetOutput).toHaveBeenCalledWith("tarball_url", "")
expect(mockSetOutput).toHaveBeenCalledWith("zipball_url", "")
})
it("Applies asset URLs to the action output", () => {
const assetUrls = {
"example.zip": "https://github.com/owner/repo/releases/download/v1.0.0/example.zip",
"example.tar.gz": "https://github.com/owner/repo/releases/download/v1.0.0/example.tar.gz",
}
outputs.applyAssetUrls(assetUrls)
expect(mockSetOutput).toHaveBeenCalledWith("assets", JSON.stringify(assetUrls))
})
it("Applies empty asset URLs to the action output", () => {
const assetUrls = {}
outputs.applyAssetUrls(assetUrls)
expect(mockSetOutput).toHaveBeenCalledWith("assets", JSON.stringify(assetUrls))
})
})

View File

@@ -47,6 +47,10 @@ inputs:
description: 'Indicates if release notes should be automatically generated.'
required: false
default: 'false'
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: 'true'
makeLatest:
description: 'Indicates if the release should be the "latest" release or not.'
required: false
@@ -122,6 +126,12 @@ outputs:
description: 'The HTML URL of the release.'
upload_url:
description: 'The URL for uploading assets to the release.'
tarball_url:
description: 'The URL for downloading the release as a tarball (.tar.gz).'
zipball_url:
description: 'The URL for downloading the release as a zipball (.zip).'
assets:
description: 'JSON string containing a map of asset names to download URLs for uploaded assets.'
runs:
using: 'node20'
main: 'dist/index.js'

2012
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

104
dist/licenses.txt vendored
View File

@@ -93,6 +93,60 @@ 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.
@isaacs/balanced-match
MIT
(MIT)
Original code Copyright Julian Gruber <julian@juliangruber.com>
Port to TypeScript Copyright Isaac Z. Schlueter <i@izs.me>
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.
@isaacs/brace-expansion
MIT
MIT License
Copyright Julian Gruber <julian@juliangruber.com>
TypeScript port Copyright Isaac Z. Schlueter <i@izs.me>
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 +319,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,31 +524,6 @@ Apache-2.0
limitations under the License.
brace-expansion
MIT
MIT License
Copyright (c) 2013 Julian Gruber <julian@juliangruber.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.
deprecation
ISC
The ISC License

View File

@@ -41,7 +41,9 @@
"testMatch": ["**/*.test.ts"],
"testRunner": "jest-circus/runner",
"transform": {
"^.+\\.ts$": "ts-jest"
"^.+\\.ts$": ["ts-jest", {
"tsconfig": "tsconfig.test.json"
}]
},
"verbose": true
},
@@ -49,7 +51,7 @@
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.1",
"@types/node": "^22.15.29",
"glob": "^11.0.2",
"glob": "^11.0.3",
"untildify": "^4.0.0"
},
"devDependencies": {

View File

@@ -47,81 +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
if (artifacts.length > 0) {
await this.uploader.uploadArtifacts(artifacts, releaseId, uploadUrl)
}
this.outputs.applyReleaseData(releaseData)
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()
}
}
@@ -138,17 +104,101 @@ export class Action {
return draftRelease?.id
}
private async createRelease(): Promise<CreateReleaseResponse> {
return await this.releases.create(
private async updateRelease(id: number) {
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
}
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() {
let releaseBody = this.inputs.createdReleaseBody
if (this.inputs.generateReleaseNotes) {
const response = await this.releases.generateReleaseNotes(this.inputs.tag)
releaseBody = response.data.body
}
// 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 setOutputs(releaseData: any, assetUrls: Record<string, string>): void {
this.outputs.applyReleaseData(releaseData)
this.outputs.applyAssetUrls(assetUrls)
}
}

View File

@@ -3,7 +3,7 @@ import { Artifact } from "./Artifact"
import { Releases } from "./Releases"
export interface ArtifactUploader {
uploadArtifacts(artifacts: Artifact[], releaseId: number, uploadUrl: string): Promise<void>
uploadArtifacts(artifacts: Artifact[], releaseId: number, uploadUrl: string): Promise<Record<string, string>>
}
export class GithubArtifactUploader implements ArtifactUploader {
@@ -13,19 +13,24 @@ export class GithubArtifactUploader implements ArtifactUploader {
private throwsUploadErrors: boolean = false
) {}
async uploadArtifacts(artifacts: Artifact[], releaseId: number, uploadUrl: string): Promise<void> {
async uploadArtifacts(artifacts: Artifact[], releaseId: number, uploadUrl: string): Promise<Record<string, string>> {
if (this.replacesExistingArtifacts) {
await this.deleteUpdatedArtifacts(artifacts, releaseId)
}
const assetUrls: Record<string, string> = {}
for (const artifact of artifacts) {
await this.uploadArtifact(artifact, releaseId, uploadUrl)
const assetUrl = await this.uploadArtifact(artifact, releaseId, uploadUrl)
if (assetUrl !== null) {
assetUrls[artifact.name] = assetUrl
}
}
return assetUrls
}
private async uploadArtifact(artifact: Artifact, releaseId: number, uploadUrl: string, retry = 3) {
private async uploadArtifact(artifact: Artifact, releaseId: number, uploadUrl: string, retry = 3): Promise<string | null> {
try {
core.debug(`Uploading artifact ${artifact.name}...`)
await this.releases.uploadArtifact(
const assetResponse = await this.releases.uploadArtifact(
uploadUrl,
artifact.contentLength,
artifact.contentType,
@@ -33,15 +38,17 @@ export class GithubArtifactUploader implements ArtifactUploader {
artifact.name,
releaseId
)
return assetResponse.data.browser_download_url
} catch (error: any) {
if (error.status >= 500 && retry > 0) {
core.warning(`Failed to upload artifact ${artifact.name}. ${error.message}. Retrying...`)
await this.uploadArtifact(artifact, releaseId, uploadUrl, retry - 1)
return this.uploadArtifact(artifact, releaseId, uploadUrl, retry - 1)
} else {
if (this.throwsUploadErrors) {
throw Error(`Failed to upload artifact ${artifact.name}. ${error.message}.`)
} else {
core.warning(`Failed to upload artifact ${artifact.name}. ${error.message}.`)
return null
}
}
}

View File

@@ -15,6 +15,7 @@ export interface Inputs {
readonly createdReleaseName?: string
readonly discussionCategory?: string
readonly generateReleaseNotes: boolean
readonly immutableCreate: boolean
readonly makeLatest?: "legacy" | "true" | "false" | undefined
readonly omitBodyDuringUpdate: boolean
readonly owner: string
@@ -137,6 +138,11 @@ export class CoreInputs implements Inputs {
return generate == "true"
}
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") {

View File

@@ -3,6 +3,7 @@ import { ReleaseData } from "./Releases"
export interface Outputs {
applyReleaseData(releaseData: ReleaseData): void
applyAssetUrls(assetUrls: Record<string, string>): void
}
export class CoreOutputs implements Outputs {
@@ -13,4 +14,9 @@ export class CoreOutputs implements Outputs {
core.setOutput("tarball_url", releaseData.tarball_url || "")
core.setOutput("zipball_url", releaseData.zipball_url || "")
}
applyAssetUrls(assetUrls: Record<string, string>) {
const assetUrlsJson = JSON.stringify(assetUrls)
core.setOutput("assets", assetUrlsJson)
}
}

View File

@@ -27,7 +27,6 @@ export interface Releases {
commitHash?: string,
discussionCategory?: string,
draft?: boolean,
generateReleaseNotes?: boolean,
makeLatest?: "legacy" | "true" | "false" | undefined,
name?: string,
prerelease?: boolean
@@ -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,

7
tsconfig.test.json Normal file
View File

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

View File

@@ -633,6 +633,18 @@
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff"
integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==
"@isaacs/balanced-match@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29"
integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==
"@isaacs/brace-expansion@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3"
integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==
dependencies:
"@isaacs/balanced-match" "^4.0.1"
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
@@ -1047,11 +1059,6 @@
dependencies:
"@octokit/openapi-types" "^23.0.1"
"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@sinclair/typebox@^0.27.8":
version "0.27.8"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"
@@ -1494,7 +1501,7 @@ create-jest@^29.7.0:
jest-util "^29.7.0"
prompts "^2.0.1"
cross-spawn@^7.0.0, cross-spawn@^7.0.3:
cross-spawn@^7.0.3, cross-spawn@^7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
@@ -1664,12 +1671,12 @@ find-up@^4.0.0, find-up@^4.1.0:
locate-path "^5.0.0"
path-exists "^4.0.0"
foreground-child@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d"
integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==
foreground-child@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
dependencies:
cross-spawn "^7.0.0"
cross-spawn "^7.0.6"
signal-exit "^4.0.1"
fs.realpath@^1.0.0:
@@ -1707,14 +1714,14 @@ get-stream@^6.0.0:
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
glob@^11.0.2:
version "11.0.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.2.tgz#3261e3897bbc603030b041fd77ba636022d51ce0"
integrity sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==
glob@^11.0.3:
version "11.0.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.3.tgz#9d8087e6d72ddb3c4707b1d2778f80ea3eaefcd6"
integrity sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==
dependencies:
foreground-child "^3.1.0"
jackspeak "^4.0.1"
minimatch "^10.0.0"
foreground-child "^3.3.1"
jackspeak "^4.1.1"
minimatch "^10.0.3"
minipass "^7.1.2"
package-json-from-dist "^1.0.0"
path-scurry "^2.0.0"
@@ -1889,14 +1896,12 @@ istanbul-reports@^3.1.3:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
jackspeak@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.0.1.tgz#9fca4ce961af6083e259c376e9e3541431f5287b"
integrity sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==
jackspeak@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae"
integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==
dependencies:
"@isaacs/cliui" "^8.0.2"
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
jake@^10.8.5:
version "10.9.2"
@@ -2368,12 +2373,12 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
minimatch@^10.0.0:
version "10.0.1"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b"
integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==
minimatch@^10.0.3:
version "10.0.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa"
integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==
dependencies:
brace-expansion "^2.0.1"
"@isaacs/brace-expansion" "^5.0.0"
minimatch@^3.0.4, minimatch@^3.1.2:
version "3.1.2"