diff --git a/packages/artifact/package-lock.json b/packages/artifact/package-lock.json index 90338dff..83971346 100644 --- a/packages/artifact/package-lock.json +++ b/packages/artifact/package-lock.json @@ -16,7 +16,9 @@ "@octokit/core": "^3.5.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", + "@octokit/request-error": "^5.0.0", "@protobuf-ts/plugin": "^2.2.3-alpha.1", + "@types/unzipper": "^0.10.6", "archiver": "^5.3.1", "crypto": "^1.0.1", "jwt-decode": "^3.1.2", @@ -292,6 +294,32 @@ "universal-user-agent": "^6.0.0" } }, + "node_modules/@octokit/request-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.0.tgz", + "integrity": "sha512-1ue0DH0Lif5iEqT52+Rf/hf0RmGO9NWFjrzmrkArpG9trFfDM/efx00BJHdLGuro4BR/gECxCU2Twf5OKrRFsQ==", + "dependencies": { + "@octokit/types": "^11.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.0.0.tgz", + "integrity": "sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw==" + }, + "node_modules/@octokit/request-error/node_modules/@octokit/types": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-11.1.0.tgz", + "integrity": "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ==", + "dependencies": { + "@octokit/openapi-types": "^18.0.0" + } + }, "node_modules/@octokit/request/node_modules/@octokit/request-error": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", @@ -441,6 +469,14 @@ "@types/node": "*" } }, + "node_modules/@types/unzipper": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.6.tgz", + "integrity": "sha512-zcBj329AHgKLQyz209N/S9R0GZqXSkUQO4tJSYE3x02qg4JuDFpgKMj50r82Erk1natCWQDIvSccDddt7jPzjA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/archiver": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz", diff --git a/packages/artifact/package.json b/packages/artifact/package.json index b6d0ae1c..dc776716 100644 --- a/packages/artifact/package.json +++ b/packages/artifact/package.json @@ -46,7 +46,9 @@ "@octokit/core": "^3.5.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", + "@octokit/request-error": "^5.0.0", "@protobuf-ts/plugin": "^2.2.3-alpha.1", + "@types/unzipper": "^0.10.6", "archiver": "^5.3.1", "crypto": "^1.0.1", "jwt-decode": "^3.1.2", diff --git a/packages/artifact/src/internal/download/download-artifact.ts b/packages/artifact/src/internal/download/download-artifact.ts index 17699dec..32ac56ad 100644 --- a/packages/artifact/src/internal/download/download-artifact.ts +++ b/packages/artifact/src/internal/download/download-artifact.ts @@ -1,6 +1,4 @@ -import path from 'path' import fs from 'fs/promises' -import {PathLike} from 'fs' import * as github from '@actions/github' import * as core from '@actions/core' import * as httpClient from '@actions/http-client' @@ -10,6 +8,7 @@ import { DownloadArtifactResponse } from '../shared/interfaces' import {getUserAgentString} from '../shared/user-agent' +import {getGitHubWorkspaceDir} from '../shared/config' const scrubQueryParameters = (url: string): string => { const parsed = new URL(url) @@ -30,7 +29,7 @@ async function exists(path: string): Promise { } } -async function streamExtract(url: string, directory: PathLike): Promise { +async function streamExtract(url: string, directory: string): Promise { const client = new httpClient.HttpClient(getUserAgentString()) const response = await client.get(url) @@ -55,13 +54,12 @@ export async function downloadArtifact( token: string, options?: DownloadArtifactOptions ): Promise { - let downloadPath = options?.path || process.cwd() // TODO: make this align with GITHUB_WORKSPACE - if (options?.createArtifactFolder) { - downloadPath = path.join(downloadPath, 'my-artifact') // TODO: need to pass artifact name - } + const downloadPath = options?.path || getGitHubWorkspaceDir() if (!(await exists(downloadPath))) { - core.debug(`Artifact destination folder does not exist, creating: ${downloadPath}`) + core.debug( + `Artifact destination folder does not exist, creating: ${downloadPath}` + ) await fs.mkdir(downloadPath, {recursive: true}) } else { core.debug(`Artifact destination folder already exists: ${downloadPath}`) diff --git a/packages/artifact/src/internal/find/get-artifact.ts b/packages/artifact/src/internal/find/get-artifact.ts index 3b28ff91..24c38a99 100644 --- a/packages/artifact/src/internal/find/get-artifact.ts +++ b/packages/artifact/src/internal/find/get-artifact.ts @@ -1,4 +1,12 @@ import {GetArtifactResponse} from '../shared/interfaces' +import {getOctokit} from '@actions/github' +import {getUserAgentString} from '../shared/user-agent' +import {defaults as defaultGitHubOptions} from '@actions/github/lib/utils' +import {getRetryOptions} from './retry-options' +import {requestLog} from '@octokit/plugin-request-log' +import {retry} from '@octokit/plugin-retry' +import * as core from '@actions/core' +import {OctokitOptions} from '@octokit/core/dist-types/types' export async function getArtifact( artifactName: string, @@ -7,5 +15,55 @@ export async function getArtifact( repositoryName: string, token: string ): Promise { - throw new Error('Not implemented') + const [retryOpts, requestOpts] = getRetryOptions(defaultGitHubOptions) + + const opts: OctokitOptions = { + log: undefined, + userAgent: getUserAgentString(), + previews: undefined, + retry: retryOpts, + request: requestOpts + } + + const github = getOctokit(token, opts, retry, requestLog) + + const getArtifactResp = await github.request( + 'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts{?name}', + { + owner: repositoryOwner, + repo: repositoryName, + run_id: workflowRunId, + name: artifactName + } + ) + + if (getArtifactResp.status !== 200) { + core.warning(`non-200 response from GitHub API: ${getArtifactResp.status}`) + return { + success: false + } + } + + if (getArtifactResp.data.artifacts.length === 0) { + core.warning('no artifacts found') + return { + success: false + } + } + + if (getArtifactResp.data.artifacts.length > 1) { + core.warning( + 'more than one artifact found for a single name, returning first' + ) + } + + return { + success: true, + artifact: { + name: getArtifactResp.data.artifacts[0].name, + id: getArtifactResp.data.artifacts[0].id, + url: getArtifactResp.data.artifacts[0].url, + size: getArtifactResp.data.artifacts[0].size_in_bytes + } + } } diff --git a/packages/artifact/src/internal/find/list-artifacts.ts b/packages/artifact/src/internal/find/list-artifacts.ts index 7fd96838..ea15fb36 100644 --- a/packages/artifact/src/internal/find/list-artifacts.ts +++ b/packages/artifact/src/internal/find/list-artifacts.ts @@ -2,26 +2,16 @@ import {info, warning, debug} from '@actions/core' import {getOctokit} from '@actions/github' import {ListArtifactsResponse, Artifact} from '../shared/interfaces' import {getUserAgentString} from '../shared/user-agent' -import {RetryOptions, getRetryOptions} from './retry-options' +import {getRetryOptions} from './retry-options' import {defaults as defaultGitHubOptions} from '@actions/github/lib/utils' import {requestLog} from '@octokit/plugin-request-log' import {retry} from '@octokit/plugin-retry' -import {RequestRequestOptions} from '@octokit/types' - -type Options = { - log?: Console - userAgent?: string - previews?: string[] - retry?: RetryOptions - request?: RequestRequestOptions -} +import {OctokitOptions} from '@octokit/core/dist-types/types' // Limiting to 1000 for perf reasons const maximumArtifactCount = 1000 const paginationCount = 100 const maxNumberOfPages = maximumArtifactCount / paginationCount -const maxRetryNumber = 5 -const exemptStatusCodes = [400, 401, 403, 404, 422] // https://github.com/octokit/plugin-retry.js/blob/9a2443746c350b3beedec35cf26e197ea318a261/src/index.ts#L14 export async function listArtifacts( workflowRunId: number, @@ -29,16 +19,14 @@ export async function listArtifacts( repositoryName: string, token: string ): Promise { - info(`Fetching artifact list for workflow run ${workflowRunId} in repository ${repositoryOwner}/${repositoryName}`) - - const artifacts: Artifact[] = [] - const [retryOpts, requestOpts] = getRetryOptions( - maxRetryNumber, - exemptStatusCodes, - defaultGitHubOptions + info( + `Fetching artifact list for workflow run ${workflowRunId} in repository ${repositoryOwner}/${repositoryName}` ) - const opts: Options = { + const artifacts: Artifact[] = [] + const [retryOpts, requestOpts] = getRetryOptions(defaultGitHubOptions) + + const opts: OctokitOptions = { log: undefined, userAgent: getUserAgentString(), previews: undefined, @@ -70,14 +58,14 @@ export async function listArtifacts( } // Iterate over the first page - listArtifactResponse.artifacts.forEach(artifact => { + for (const artifact of listArtifactResponse.artifacts) { artifacts.push({ name: artifact.name, id: artifact.id, url: artifact.url, size: artifact.size_in_bytes }) - }) + } // Iterate over any remaining pages for ( @@ -97,19 +85,19 @@ export async function listArtifacts( page: currentPageNumber }) - listArtifactResponse.artifacts.forEach(artifact => { + for (const artifact of listArtifactResponse.artifacts) { artifacts.push({ name: artifact.name, id: artifact.id, url: artifact.url, size: artifact.size_in_bytes }) - }) + } } info(`Finished fetching artifact list`) return { - artifacts: artifacts + artifacts } } diff --git a/packages/artifact/src/internal/find/retry-options.ts b/packages/artifact/src/internal/find/retry-options.ts index 3fe51b5a..ab10309b 100644 --- a/packages/artifact/src/internal/find/retry-options.ts +++ b/packages/artifact/src/internal/find/retry-options.ts @@ -7,10 +7,14 @@ export type RetryOptions = { enabled?: boolean } +// Defaults for fetching artifacts +const defaultMaxRetryNumber = 5 +const defaultExemptStatusCodes = [400, 401, 403, 404, 422] // https://github.com/octokit/plugin-retry.js/blob/9a2443746c350b3beedec35cf26e197ea318a261/src/index.ts#L14 + export function getRetryOptions( - retries: number, - exemptStatusCodes: number[], - defaultOptions: OctokitOptions + defaultOptions: OctokitOptions, + retries: number = defaultMaxRetryNumber, + exemptStatusCodes: number[] = defaultExemptStatusCodes ): [RetryOptions, RequestRequestOptions | undefined] { if (retries <= 0) { return [{enabled: false}, defaultOptions.request] diff --git a/packages/artifact/src/internal/shared/config.ts b/packages/artifact/src/internal/shared/config.ts index dabb853b..8d8a1668 100644 --- a/packages/artifact/src/internal/shared/config.ts +++ b/packages/artifact/src/internal/shared/config.ts @@ -26,3 +26,11 @@ export function isGhes(): boolean { ) return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM' } + +export function getGitHubWorkspaceDir(): string { + const ghWorkspaceDir = process.env['GITHUB_WORKSPACE'] + if (!ghWorkspaceDir) { + throw new Error('Unable to get the GITHUB_WORKSPACE env variable') + } + return ghWorkspaceDir +} diff --git a/packages/artifact/src/internal/shared/interfaces.ts b/packages/artifact/src/internal/shared/interfaces.ts index 4f9a5c43..7e3e862f 100644 --- a/packages/artifact/src/internal/shared/interfaces.ts +++ b/packages/artifact/src/internal/shared/interfaces.ts @@ -91,12 +91,6 @@ export interface DownloadArtifactOptions { * Denotes where the artifact will be downloaded to. If not specified then the artifact is download to GITHUB_WORKSPACE */ path?: string - - /** - * Specifies if a root folder with the artifact name is created for the artifact that is downloaded - * Zip contents are expanded into this folder. Defaults to false if not specified - * */ - createArtifactFolder?: boolean } /***************************************************************************** diff --git a/packages/artifact/src/internal/shared/user-agent.ts b/packages/artifact/src/internal/shared/user-agent.ts index eee01446..94c07e89 100644 --- a/packages/artifact/src/internal/shared/user-agent.ts +++ b/packages/artifact/src/internal/shared/user-agent.ts @@ -1,4 +1,5 @@ -var packageJson = require('../../../package.json') +// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports +const packageJson = require('../../../package.json') /** * Ensure that this User Agent String is used in all HTTP calls so that we can monitor telemetry between different versions of this package diff --git a/packages/artifact/src/internal/upload/blob-upload.ts b/packages/artifact/src/internal/upload/blob-upload.ts index 82af468a..42a3fbd5 100644 --- a/packages/artifact/src/internal/upload/blob-upload.ts +++ b/packages/artifact/src/internal/upload/blob-upload.ts @@ -91,6 +91,6 @@ export async function uploadZipToBlobStorage( return { isSuccess: true, uploadSize: uploadByteCount, - md5Hash: md5Hash + md5Hash } } diff --git a/packages/artifact/src/internal/upload/upload-artifact.ts b/packages/artifact/src/internal/upload/upload-artifact.ts index 35cd9ac3..55911476 100644 --- a/packages/artifact/src/internal/upload/upload-artifact.ts +++ b/packages/artifact/src/internal/upload/upload-artifact.ts @@ -61,7 +61,7 @@ export async function uploadArtifact( const createArtifactReq: CreateArtifactRequest = { workflowRunBackendId: backendIds.workflowRunBackendId, workflowJobRunBackendId: backendIds.workflowJobRunBackendId, - name: name, + name, version: 4 } @@ -96,13 +96,13 @@ export async function uploadArtifact( const finalizeArtifactReq: FinalizeArtifactRequest = { workflowRunBackendId: backendIds.workflowRunBackendId, workflowJobRunBackendId: backendIds.workflowJobRunBackendId, - name: name, - size: uploadResult.uploadSize!.toString() + name, + size: uploadResult.uploadSize ? uploadResult.uploadSize.toString() : '0' } if (uploadResult.md5Hash) { finalizeArtifactReq.hash = StringValue.create({ - value: `md5:${uploadResult.md5Hash!}` + value: `md5:${uploadResult.md5Hash}` }) }