@actions/artifact
Version:
Actions artifact lib
238 lines • 12 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import fs from 'fs/promises';
import * as fsSync from 'fs';
import * as crypto from 'crypto';
import * as stream from 'stream';
import * as path from 'path';
import * as github from '@actions/github';
import * as core from '@actions/core';
import * as httpClient from '@actions/http-client';
import unzip from 'unzip-stream';
import { getUserAgentString } from '../shared/user-agent.js';
import { getGitHubWorkspaceDir } from '../shared/config.js';
import { internalArtifactTwirpClient } from '../shared/artifact-twirp-client.js';
import { Int64Value } from '../../generated/index.js';
import { getBackendIdsFromToken } from '../shared/util.js';
import { ArtifactNotFoundError } from '../shared/errors.js';
const scrubQueryParameters = (url) => {
const parsed = new URL(url);
parsed.search = '';
return parsed.toString();
};
function exists(path) {
return __awaiter(this, void 0, void 0, function* () {
try {
yield fs.access(path);
return true;
}
catch (error) {
if (error.code === 'ENOENT') {
return false;
}
else {
throw error;
}
}
});
}
function streamExtract(url, directory, skipDecompress) {
return __awaiter(this, void 0, void 0, function* () {
let retryCount = 0;
while (retryCount < 5) {
try {
return yield streamExtractExternal(url, directory, { skipDecompress });
}
catch (error) {
retryCount++;
core.debug(`Failed to download artifact after ${retryCount} retries due to ${error.message}. Retrying in 5 seconds...`);
// wait 5 seconds before retrying
yield new Promise(resolve => setTimeout(resolve, 5000));
}
}
throw new Error(`Artifact download failed after ${retryCount} retries.`);
});
}
export function streamExtractExternal(url_1, directory_1) {
return __awaiter(this, arguments, void 0, function* (url, directory, opts = {}) {
const { timeout = 30 * 1000, skipDecompress = false } = opts;
const client = new httpClient.HttpClient(getUserAgentString());
const response = yield client.get(url);
if (response.message.statusCode !== 200) {
throw new Error(`Unexpected HTTP response from blob storage: ${response.message.statusCode} ${response.message.statusMessage}`);
}
const contentType = response.message.headers['content-type'] || '';
const mimeType = contentType.split(';', 1)[0].trim().toLowerCase();
// Check if the URL path ends with .zip (ignoring query parameters)
const urlPath = new URL(url).pathname.toLowerCase();
const urlEndsWithZip = urlPath.endsWith('.zip');
const isZip = mimeType === 'application/zip' ||
mimeType === 'application/x-zip-compressed' ||
mimeType === 'application/zip-compressed' ||
urlEndsWithZip;
// Extract filename from Content-Disposition header
// Prefer filename* (RFC 5987) which supports UTF-8 encoded filenames,
// fall back to filename which may contain ASCII-only replacements
const contentDisposition = response.message.headers['content-disposition'] || '';
let fileName = 'artifact';
const filenameStar = contentDisposition.match(/filename\*\s*=\s*UTF-8''([^;\r\n]*)/i);
const filenamePlain = contentDisposition.match(/(?<!\*)filename\s*=\s*['"]?([^;\r\n"']*)['"]?/i);
const rawName = (filenameStar === null || filenameStar === void 0 ? void 0 : filenameStar[1]) || (filenamePlain === null || filenamePlain === void 0 ? void 0 : filenamePlain[1]);
if (rawName) {
// Sanitize fileName to prevent path traversal attacks
// Use path.basename to extract only the filename component
fileName = path.basename(decodeURIComponent(rawName.trim()));
}
core.debug(`Content-Type: ${contentType}, mimeType: ${mimeType}, urlEndsWithZip: ${urlEndsWithZip}, isZip: ${isZip}, skipDecompress: ${skipDecompress}`);
core.debug(`Content-Disposition: ${contentDisposition}, fileName: ${fileName}`);
let sha256Digest = undefined;
return new Promise((resolve, reject) => {
const timerFn = () => {
const timeoutError = new Error(`Blob storage chunk did not respond in ${timeout}ms`);
response.message.destroy(timeoutError);
reject(timeoutError);
};
const timer = setTimeout(timerFn, timeout);
const onError = (error) => {
core.debug(`response.message: Artifact download failed: ${error.message}`);
clearTimeout(timer);
reject(error);
};
const hashStream = crypto.createHash('sha256').setEncoding('hex');
const passThrough = new stream.PassThrough()
.on('data', () => {
timer.refresh();
})
.on('error', onError);
response.message.pipe(passThrough);
passThrough.pipe(hashStream);
const onClose = () => {
clearTimeout(timer);
if (hashStream) {
hashStream.end();
sha256Digest = hashStream.read();
core.info(`SHA256 digest of downloaded artifact is ${sha256Digest}`);
}
resolve({ sha256Digest: `sha256:${sha256Digest}` });
};
if (isZip && !skipDecompress) {
// Extract zip file
passThrough
.pipe(unzip.Extract({ path: directory }))
.on('close', onClose)
.on('error', onError);
}
else {
// Save raw file without extracting
const filePath = path.join(directory, fileName);
const writeStream = fsSync.createWriteStream(filePath);
core.info(`Downloading raw file (non-zip) to: ${filePath}`);
passThrough.pipe(writeStream).on('close', onClose).on('error', onError);
}
});
});
}
export function downloadArtifactPublic(artifactId, repositoryOwner, repositoryName, token, options) {
return __awaiter(this, void 0, void 0, function* () {
const downloadPath = yield resolveOrCreateDirectory(options === null || options === void 0 ? void 0 : options.path);
const api = github.getOctokit(token);
let digestMismatch = false;
core.info(`Downloading artifact '${artifactId}' from '${repositoryOwner}/${repositoryName}'`);
const { headers, status } = yield api.rest.actions.downloadArtifact({
owner: repositoryOwner,
repo: repositoryName,
artifact_id: artifactId,
archive_format: 'zip',
request: {
redirect: 'manual'
}
});
if (status !== 302) {
throw new Error(`Unable to download artifact. Unexpected status: ${status}`);
}
const { location } = headers;
if (!location) {
throw new Error(`Unable to redirect to artifact download url`);
}
core.info(`Redirecting to blob download url: ${scrubQueryParameters(location)}`);
try {
core.info(`Starting download of artifact to: ${downloadPath}`);
const extractResponse = yield streamExtract(location, downloadPath, options === null || options === void 0 ? void 0 : options.skipDecompress);
core.info(`Artifact download completed successfully.`);
if (options === null || options === void 0 ? void 0 : options.expectedHash) {
if ((options === null || options === void 0 ? void 0 : options.expectedHash) !== extractResponse.sha256Digest) {
digestMismatch = true;
core.debug(`Computed digest: ${extractResponse.sha256Digest}`);
core.debug(`Expected digest: ${options.expectedHash}`);
}
}
}
catch (error) {
throw new Error(`Unable to download and extract artifact: ${error.message}`);
}
return { downloadPath, digestMismatch };
});
}
export function downloadArtifactInternal(artifactId, options) {
return __awaiter(this, void 0, void 0, function* () {
const downloadPath = yield resolveOrCreateDirectory(options === null || options === void 0 ? void 0 : options.path);
const artifactClient = internalArtifactTwirpClient();
let digestMismatch = false;
const { workflowRunBackendId, workflowJobRunBackendId } = getBackendIdsFromToken();
const listReq = {
workflowRunBackendId,
workflowJobRunBackendId,
idFilter: Int64Value.create({ value: artifactId.toString() })
};
const { artifacts } = yield artifactClient.ListArtifacts(listReq);
if (artifacts.length === 0) {
throw new ArtifactNotFoundError(`No artifacts found for ID: ${artifactId}\nAre you trying to download from a different run? Try specifying a github-token with \`actions:read\` scope.`);
}
if (artifacts.length > 1) {
core.warning('Multiple artifacts found, defaulting to first.');
}
const signedReq = {
workflowRunBackendId: artifacts[0].workflowRunBackendId,
workflowJobRunBackendId: artifacts[0].workflowJobRunBackendId,
name: artifacts[0].name
};
const { signedUrl } = yield artifactClient.GetSignedArtifactURL(signedReq);
core.info(`Redirecting to blob download url: ${scrubQueryParameters(signedUrl)}`);
try {
core.info(`Starting download of artifact to: ${downloadPath}`);
const extractResponse = yield streamExtract(signedUrl, downloadPath, options === null || options === void 0 ? void 0 : options.skipDecompress);
core.info(`Artifact download completed successfully.`);
if (options === null || options === void 0 ? void 0 : options.expectedHash) {
if ((options === null || options === void 0 ? void 0 : options.expectedHash) !== extractResponse.sha256Digest) {
digestMismatch = true;
core.debug(`Computed digest: ${extractResponse.sha256Digest}`);
core.debug(`Expected digest: ${options.expectedHash}`);
}
}
}
catch (error) {
throw new Error(`Unable to download and extract artifact: ${error.message}`);
}
return { downloadPath, digestMismatch };
});
}
function resolveOrCreateDirectory() {
return __awaiter(this, arguments, void 0, function* (downloadPath = getGitHubWorkspaceDir()) {
if (!(yield exists(downloadPath))) {
core.debug(`Artifact destination folder does not exist, creating: ${downloadPath}`);
yield fs.mkdir(downloadPath, { recursive: true });
}
else {
core.debug(`Artifact destination folder already exists: ${downloadPath}`);
}
return downloadPath;
});
}
//# sourceMappingURL=download-artifact.js.map