2 Commits

Author SHA1 Message Date
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
11 changed files with 191 additions and 30 deletions

View File

@@ -51,6 +51,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. | | 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). | | 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). | | 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 ## Example
This example will create a release when a tag is pushed: This example will create a release when a tag is pushed:

View File

@@ -15,6 +15,7 @@ const TEST_URLS = {
} as const } as const
const applyReleaseDataMock = jest.fn() const applyReleaseDataMock = jest.fn()
const applyAssetUrlsMock = jest.fn()
const artifactDestroyMock = jest.fn() const artifactDestroyMock = jest.fn()
const createMock = jest.fn() const createMock = jest.fn()
const deleteMock = jest.fn() const deleteMock = jest.fn()
@@ -51,6 +52,8 @@ const generatedReleaseBody = "test release notes"
describe("Action", () => { describe("Action", () => {
beforeEach(() => { beforeEach(() => {
applyReleaseDataMock.mockClear()
applyAssetUrlsMock.mockClear()
createMock.mockClear() createMock.mockClear()
getMock.mockClear() getMock.mockClear()
listMock.mockClear() listMock.mockClear()
@@ -77,6 +80,7 @@ describe("Action", () => {
) )
expect(uploadMock).not.toHaveBeenCalled() expect(uploadMock).not.toHaveBeenCalled()
assertOutputApplied() assertOutputApplied()
assertAssetUrlsApplied({})
}) })
it("creates release if no release exists to update", async () => { it("creates release if no release exists to update", async () => {
@@ -99,6 +103,10 @@ describe("Action", () => {
) )
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url) expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied() 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 () => { it("creates release if no draft releases", async () => {
@@ -124,6 +132,10 @@ describe("Action", () => {
) )
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url) expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied() 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 () => { it("creates release then uploads artifact", async () => {
@@ -144,6 +156,10 @@ describe("Action", () => {
) )
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url) expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied() 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 () => { it("removes all artifacts when artifact destroyer is enabled", async () => {
@@ -153,6 +169,10 @@ describe("Action", () => {
expect(artifactDestroyMock).toHaveBeenCalledWith(releaseId) expect(artifactDestroyMock).toHaveBeenCalledWith(releaseId)
assertOutputApplied() 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 () => { it("removes no artifacts when artifact destroyer is disabled", async () => {
@@ -162,6 +182,10 @@ describe("Action", () => {
expect(artifactDestroyMock).not.toHaveBeenCalled() expect(artifactDestroyMock).not.toHaveBeenCalled()
assertOutputApplied() 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 () => { it("skips action", async () => {
@@ -326,6 +350,10 @@ describe("Action", () => {
) )
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url) expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied() 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 () => { it("updates draft release with static body", async () => {
@@ -354,6 +382,10 @@ describe("Action", () => {
) )
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url) expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied() 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 () => { it("updates release but does not upload if no artifact", async () => {
@@ -374,6 +406,7 @@ describe("Action", () => {
) )
expect(uploadMock).not.toHaveBeenCalled() expect(uploadMock).not.toHaveBeenCalled()
assertOutputApplied() assertOutputApplied()
assertAssetUrlsApplied({})
}) })
it("updates release then uploads artifact", async () => { it("updates release then uploads artifact", async () => {
@@ -394,6 +427,10 @@ describe("Action", () => {
) )
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url) expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied() 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 () => { it("updates release with static body when generateReleaseNotes is true but omitBodyDuringUpdate is true", async () => {
@@ -422,6 +459,10 @@ describe("Action", () => {
) )
expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url) expect(uploadMock).toHaveBeenCalledWith(artifacts, releaseId, url)
assertOutputApplied() 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",
})
}) })
function assertOutputApplied() { function assertOutputApplied() {
@@ -434,6 +475,10 @@ describe("Action", () => {
}) })
} }
function assertAssetUrlsApplied(expectedUrls: Record<string, string>) {
expect(applyAssetUrlsMock).toHaveBeenCalledWith(expectedUrls)
}
function createAction( function createAction(
allowUpdates: boolean, allowUpdates: boolean,
hasArtifact: boolean, hasArtifact: boolean,
@@ -495,7 +540,10 @@ describe("Action", () => {
zipball_url: TEST_URLS.ZIPBALL_URL, 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>(() => { const MockInputs = jest.fn<Inputs, any>(() => {
return { return {
@@ -528,6 +576,7 @@ describe("Action", () => {
const MockOutputs = jest.fn<Outputs, any>(() => { const MockOutputs = jest.fn<Outputs, any>(() => {
return { return {
applyReleaseData: applyReleaseDataMock, applyReleaseData: applyReleaseDataMock,
applyAssetUrls: applyAssetUrlsMock,
} }
}) })
const MockUploader = jest.fn<ArtifactUploader, any>(() => { const MockUploader = jest.fn<ArtifactUploader, any>(() => {

View File

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

View File

@@ -70,6 +70,9 @@ describe.skip("Integration Test", () => {
applyReleaseData(releaseData: ReleaseData) { applyReleaseData(releaseData: ReleaseData) {
console.log(`Release Data: ${releaseData}`) console.log(`Release Data: ${releaseData}`)
}, },
applyAssetUrls(assetUrls: Record<string, string>) {
console.log(`Asset URLs: ${JSON.stringify(assetUrls)}`)
},
} }
}) })
return new MockOutputs() 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" jest.mock("@actions/core")
import { ReleaseData } from "../src/Releases" const { setOutput: mockSetOutput } = jest.mocked(require("@actions/core"))
jest.mock("@actions/core", () => {
return { setOutput: mockSetOutput }
})
const TEST_URLS = { const TEST_URLS = {
HTML_URL: "https://api.example.com/assets", HTML_URL: "https://api.example.com/assets",
@@ -48,4 +45,19 @@ describe("Outputs", () => {
expect(mockSetOutput).toHaveBeenCalledWith("tarball_url", "") expect(mockSetOutput).toHaveBeenCalledWith("tarball_url", "")
expect(mockSetOutput).toHaveBeenCalledWith("zipball_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

@@ -122,6 +122,12 @@ outputs:
description: 'The HTML URL of the release.' description: 'The HTML URL of the release.'
upload_url: upload_url:
description: 'The URL for uploading assets to the release.' 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: runs:
using: 'node20' using: 'node20'
main: 'dist/index.js' main: 'dist/index.js'

21
dist/index.js vendored
View File

@@ -64,10 +64,12 @@ class Action {
await this.artifactDestroyer.destroyArtifacts(releaseId); await this.artifactDestroyer.destroyArtifacts(releaseId);
} }
const artifacts = this.inputs.artifacts; const artifacts = this.inputs.artifacts;
let assetUrls = {};
if (artifacts.length > 0) { if (artifacts.length > 0) {
await this.uploader.uploadArtifacts(artifacts, releaseId, uploadUrl); assetUrls = await this.uploader.uploadArtifacts(artifacts, releaseId, uploadUrl);
} }
this.outputs.applyReleaseData(releaseData); this.outputs.applyReleaseData(releaseData);
this.outputs.applyAssetUrls(assetUrls);
} }
async createOrUpdateRelease() { async createOrUpdateRelease() {
if (this.inputs.allowUpdates) { if (this.inputs.allowUpdates) {
@@ -454,19 +456,25 @@ class GithubArtifactUploader {
if (this.replacesExistingArtifacts) { if (this.replacesExistingArtifacts) {
await this.deleteUpdatedArtifacts(artifacts, releaseId); await this.deleteUpdatedArtifacts(artifacts, releaseId);
} }
const assetUrls = {};
for (const artifact of artifacts) { 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;
} }
async uploadArtifact(artifact, releaseId, uploadUrl, retry = 3) { async uploadArtifact(artifact, releaseId, uploadUrl, retry = 3) {
try { try {
core.debug(`Uploading artifact ${artifact.name}...`); core.debug(`Uploading artifact ${artifact.name}...`);
await this.releases.uploadArtifact(uploadUrl, artifact.contentLength, artifact.contentType, artifact.readFile(), artifact.name, releaseId); const assetResponse = await this.releases.uploadArtifact(uploadUrl, artifact.contentLength, artifact.contentType, artifact.readFile(), artifact.name, releaseId);
return assetResponse.data.browser_download_url;
} }
catch (error) { catch (error) {
if (error.status >= 500 && retry > 0) { if (error.status >= 500 && retry > 0) {
core.warning(`Failed to upload artifact ${artifact.name}. ${error.message}. Retrying...`); 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 { else {
if (this.throwsUploadErrors) { if (this.throwsUploadErrors) {
@@ -474,6 +482,7 @@ class GithubArtifactUploader {
} }
else { else {
core.warning(`Failed to upload artifact ${artifact.name}. ${error.message}.`); core.warning(`Failed to upload artifact ${artifact.name}. ${error.message}.`);
return null;
} }
} }
} }
@@ -892,6 +901,10 @@ class CoreOutputs {
core.setOutput("tarball_url", releaseData.tarball_url || ""); core.setOutput("tarball_url", releaseData.tarball_url || "");
core.setOutput("zipball_url", releaseData.zipball_url || ""); core.setOutput("zipball_url", releaseData.zipball_url || "");
} }
applyAssetUrls(assetUrls) {
const assetUrlsJson = JSON.stringify(assetUrls);
core.setOutput("assets", assetUrlsJson);
}
} }
exports.CoreOutputs = CoreOutputs; exports.CoreOutputs = CoreOutputs;

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

View File

@@ -57,11 +57,13 @@ export class Action {
} }
const artifacts = this.inputs.artifacts const artifacts = this.inputs.artifacts
let assetUrls: Record<string, string> = {}
if (artifacts.length > 0) { if (artifacts.length > 0) {
await this.uploader.uploadArtifacts(artifacts, releaseId, uploadUrl) assetUrls = await this.uploader.uploadArtifacts(artifacts, releaseId, uploadUrl)
} }
this.outputs.applyReleaseData(releaseData) this.outputs.applyReleaseData(releaseData)
this.outputs.applyAssetUrls(assetUrls)
} }
private async createOrUpdateRelease(): Promise<CreateOrUpdateReleaseResponse> { private async createOrUpdateRelease(): Promise<CreateOrUpdateReleaseResponse> {

View File

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

View File

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