Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 64 additions & 33 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import AdmZip from 'adm-zip'
import { filesize } from 'filesize'
import pathname from 'node:path'
import fs from 'node:fs'
import os from 'node:os'
import { Readable } from 'node:stream'
import { pipeline } from 'node:stream/promises'

async function downloadAction(name, path) {
const artifactClient = artifact.create()
Expand All @@ -20,6 +23,31 @@ async function downloadAction(name, path) {
core.setOutput("found_artifact", true)
}

/**
* Downloads an artifact ZIP directly to disk via Octokit, following redirects
* without buffering the body in RAM. Non-2xx responses are returned as-is so
* Octokit can parse and throw the appropriate error (e.g. "Artifact has expired").
*/
async function downloadArtifactToFile(client, owner, repo, artifactId, destPath) {
await client.request(
"GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/{archive_format}",
{
owner,
repo,
artifact_id: artifactId,
archive_format: "zip",
request: {
fetch: async (url, options) => {
const res = await fetch(url, options)
if (!res.ok) return res
await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(destPath))
return new Response(null, { status: 200, headers: res.headers })
},
},
}
)
}

async function getWorkflow(client, owner, repo, runID) {
const run = await client.rest.actions.getWorkflowRun({
owner: owner,
Expand Down Expand Up @@ -264,49 +292,52 @@ async function main() {

core.info(`==> Downloading: ${artifact.name}.zip (${size})`)

let zip
// Determine the destination path up front.
const destPath = skipUnpack
? pathname.join(path, `${artifact.name}.zip`)
: pathname.join(os.tmpdir(), `artifact-${artifact.id}.zip`)

if (skipUnpack) {
fs.mkdirSync(path, { recursive: true })
}

try {
zip = await client.rest.actions.downloadArtifact({
owner: owner,
repo: repo,
artifact_id: artifact.id,
archive_format: "zip",
})
await downloadArtifactToFile(client, owner, repo, artifact.id, destPath)
} catch (error) {
if (error.message.startsWith("Artifact has expired")) {
if (error.message?.startsWith("Artifact has expired")) {
return setExitMessage(ifNoArtifactFound, "no downloadable artifacts found (expired)")
} else {
throw new Error(error.message)
}
throw error
}

if (skipUnpack) {
fs.mkdirSync(path, { recursive: true })
fs.writeFileSync(`${pathname.join(path, artifact.name)}.zip`, Buffer.from(zip.data), 'binary')
continue
}

const dir = name && (!nameIsRegExp || mergeMultiple) ? path : pathname.join(path, artifact.name)
if (skipUnpack) continue

fs.mkdirSync(dir, { recursive: true })
// Stream the ZIP to a temp file for extraction.
try {
const dir = name && (!nameIsRegExp || mergeMultiple) ? path : pathname.join(path, artifact.name)

core.startGroup(`==> Extracting: ${artifact.name}.zip`)
if (useUnzip) {
const zipPath = `${pathname.join(dir, artifact.name)}.zip`
fs.writeFileSync(zipPath, Buffer.from(zip.data), 'binary')
await exec.exec("unzip", [zipPath, "-d", dir])
fs.rmSync(zipPath)
} else {
const adm = new AdmZip(Buffer.from(zip.data))
adm.getEntries().forEach((entry) => {
const action = entry.isDirectory ? "creating" : "inflating"
const filepath = pathname.join(dir, entry.entryName)
fs.mkdirSync(dir, { recursive: true })

core.info(` ${action}: ${filepath}`)
})
adm.extractAllTo(dir, true)
core.startGroup(`==> Extracting: ${artifact.name}.zip`)
if (useUnzip) {
// Temp file is already on disk – hand it straight to unzip.
await exec.exec("unzip", ["-o", destPath, "-d", dir])
} else {
// AdmZip file-path constructor: reads entries one at a time, not all into RAM.
const adm = new AdmZip(destPath)
adm.getEntries().forEach((entry) => {
const action = entry.isDirectory ? "creating" : "inflating"
const filepath = pathname.join(dir, entry.entryName)

core.info(` ${action}: ${filepath}`)
})
adm.extractAllTo(dir, true)
}
core.endGroup()
} finally {
// Always clean up the temp file, even if extraction failed.
try { fs.rmSync(destPath) } catch (e) { core.debug(`Failed to remove temp file ${destPath}: ${e.message}`) }
}
core.endGroup()
}
} catch (error) {
core.setOutput("found_artifact", false)
Expand Down