From 2bccdbc690735bb8a65b1d5030592340352469be Mon Sep 17 00:00:00 2001 From: Konrad Pabjan Date: Wed, 16 Aug 2023 20:06:35 -0400 Subject: [PATCH] actions/artifact preparation for download-artifact v4 --- packages/artifact/package-lock.json | 173 +++++++++++++- packages/artifact/package.json | 8 +- packages/artifact/src/artifact.ts | 3 +- packages/artifact/src/internal/client.ts | 215 ++++++++++++++++-- .../internal/download/download-artifact.ts | 14 ++ .../src/internal/find/get-artifact.ts | 11 + .../src/internal/find/list-artifacts.ts | 108 +++++++++ .../src/internal/find/retry-options.ts | 44 ++++ .../src/internal/shared/interfaces.ts | 152 +++++++++++++ .../src/internal/shared/user-agent.ts | 8 + .../src/internal/upload/blob-upload.ts | 11 +- .../src/internal/upload/upload-artifact.ts | 3 +- .../src/internal/upload/upload-options.ts | 18 -- .../src/internal/upload/upload-response.ts | 17 -- 14 files changed, 726 insertions(+), 59 deletions(-) create mode 100644 packages/artifact/src/internal/download/download-artifact.ts create mode 100644 packages/artifact/src/internal/find/get-artifact.ts create mode 100644 packages/artifact/src/internal/find/list-artifacts.ts create mode 100644 packages/artifact/src/internal/find/retry-options.ts create mode 100644 packages/artifact/src/internal/shared/interfaces.ts create mode 100644 packages/artifact/src/internal/shared/user-agent.ts delete mode 100644 packages/artifact/src/internal/upload/upload-options.ts delete mode 100644 packages/artifact/src/internal/upload/upload-response.ts diff --git a/packages/artifact/package-lock.json b/packages/artifact/package-lock.json index 05b66fdf..6c8438ca 100644 --- a/packages/artifact/package-lock.json +++ b/packages/artifact/package-lock.json @@ -6,12 +6,16 @@ "packages": { "": { "name": "@actions/artifact", - "version": "2.0.0", + "version": "1.1.1", "license": "MIT", "dependencies": { "@actions/core": "^1.10.0", + "@actions/github": "^5.1.1", "@actions/http-client": "^2.1.0", "@azure/storage-blob": "^12.15.0", + "@octokit/core": "^3.5.1", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-retry": "^3.0.9", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^5.3.1", "crypto": "^1.0.1", @@ -32,6 +36,17 @@ "uuid": "^8.3.2" } }, + "node_modules/@actions/github": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.1.1.tgz", + "integrity": "sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g==", + "dependencies": { + "@actions/http-client": "^2.0.1", + "@octokit/core": "^3.6.0", + "@octokit/plugin-paginate-rest": "^2.17.0", + "@octokit/plugin-rest-endpoint-methods": "^5.13.0" + } + }, "node_modules/@actions/http-client": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.1.0.tgz", @@ -166,6 +181,134 @@ "node": ">=14.0.0" } }, + "node_modules/@octokit/auth-token": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", + "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", + "dependencies": { + "@octokit/types": "^6.0.3" + } + }, + "node_modules/@octokit/core": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", + "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", + "dependencies": { + "@octokit/auth-token": "^2.4.4", + "@octokit/graphql": "^4.5.8", + "@octokit/request": "^5.6.3", + "@octokit/request-error": "^2.0.5", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "dependencies": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "dependencies": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", + "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", + "dependencies": { + "@octokit/types": "^6.40.0" + }, + "peerDependencies": { + "@octokit/core": ">=2" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz", + "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==", + "dependencies": { + "@octokit/types": "^6.39.0", + "deprecation": "^2.3.1" + }, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-3.0.9.tgz", + "integrity": "sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ==", + "dependencies": { + "@octokit/types": "^6.0.3", + "bottleneck": "^2.15.3" + } + }, + "node_modules/@octokit/request": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", + "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "dependencies": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.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", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "dependencies": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@octokit/types": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "dependencies": { + "@octokit/openapi-types": "^12.11.0" + } + }, "node_modules/@opentelemetry/api": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", @@ -395,6 +538,11 @@ } ] }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -405,6 +553,11 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -534,6 +687,11 @@ "node": ">=0.4.0" } }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, "node_modules/dot-object": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/dot-object/-/dot-object-2.1.4.tgz", @@ -642,6 +800,14 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1006,6 +1172,11 @@ "node": ">=4.2.0" } }, + "node_modules/universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/packages/artifact/package.json b/packages/artifact/package.json index 25fbc984..b9e86639 100644 --- a/packages/artifact/package.json +++ b/packages/artifact/package.json @@ -10,8 +10,8 @@ ], "homepage": "https://github.com/actions/toolkit/tree/main/packages/artifact", "license": "MIT", - "main": "lib/artifact-client.js", - "types": "lib/artifact-client.d.ts", + "main": "lib/artifact.js", + "types": "lib/artifact.d.ts", "directories": { "lib": "lib", "test": "__tests__" @@ -40,8 +40,12 @@ }, "dependencies": { "@actions/core": "^1.10.0", + "@actions/github": "^5.1.1", "@actions/http-client": "^2.1.0", "@azure/storage-blob": "^12.15.0", + "@octokit/core": "^3.5.1", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-retry": "^3.0.9", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^5.3.1", "crypto": "^1.0.1", diff --git a/packages/artifact/src/artifact.ts b/packages/artifact/src/artifact.ts index b53733f3..b566df8e 100644 --- a/packages/artifact/src/artifact.ts +++ b/packages/artifact/src/artifact.ts @@ -1,6 +1,5 @@ import {ArtifactClient, Client} from './internal/client' -import {UploadOptions} from './internal/upload/upload-options' -import {UploadResponse} from './internal/upload/upload-response' +import {UploadOptions, UploadResponse} from './internal/shared/interfaces' /** * Exported functionality that we want to expose for any users of @actions/artifact diff --git a/packages/artifact/src/internal/client.ts b/packages/artifact/src/internal/client.ts index f827d882..0036e1e5 100644 --- a/packages/artifact/src/internal/client.ts +++ b/packages/artifact/src/internal/client.ts @@ -1,18 +1,27 @@ -import {UploadOptions} from './upload/upload-options' -import {UploadResponse} from './upload/upload-response' -import {uploadArtifact} from './upload/upload-artifact' import {warning} from '@actions/core' import {isGhes} from './shared/config' +import { + UploadOptions, + UploadResponse, + DownloadSingleArtifactOptions, + GetArtifactResponse, + ListArtifactsResponse, + DownloadArtifactResponse +} from './shared/interfaces' +import {uploadArtifact} from './upload/upload-artifact' +import {downloadArtifact} from './download/download-artifact' +import {getArtifact} from './find/get-artifact' +import {listArtifacts} from './find/list-artifacts' export interface ArtifactClient { /** * Uploads an artifact * - * @param name the name of the artifact, required - * @param files a list of absolute or relative paths that denote what files should be uploaded - * @param rootDirectory an absolute or relative file path that denotes the root parent directory of the files being uploaded - * @param options extra options for customizing the upload behavior - * @returns single UploadInfo object + * @param name The name of the artifact, required + * @param files A list of absolute or relative paths that denote what files should be uploaded + * @param rootDirectory An absolute or relative file path that denotes the root parent directory of the files being uploaded + * @param options Extra options for customizing the upload behavior + * @returns single UploadResponse object */ uploadArtifact( name: string, @@ -21,7 +30,64 @@ export interface ArtifactClient { options?: UploadOptions ): Promise - // TODO Download functionality + /** + * Lists all artifacts that are part of a workflow run. + * + * This calls the public List-Artifacts API https://docs.github.com/en/rest/actions/artifacts?apiVersion=2022-11-28#list-workflow-run-artifacts + * Due to paginated responses from the public API. This function will return at most 1000 artifacts per workflow run (100 per page * maximum 10 calls) + * + * @param workflowRunId The workflow run id that the artifact belongs to + * @param repositoryOwner The owner of the repository that the artifact belongs to + * @param repositoryName The name of the repository that the artifact belongs to + * @param token A token with the appropriate permission to the repository to list artifacts + * @returns ListArtifactResponse object + */ + listArtifacts( + workflowRunId: number, + repositoryOwner: string, + repositoryName: string, + token: string + ): Promise + + /** + * Finds an artifact by name given a repository and workflow run id. + * + * This calls the public List-Artifacts API with a name filter https://docs.github.com/en/rest/actions/artifacts?apiVersion=2022-11-28#list-workflow-run-artifacts + * @actions/artifact > 2.0.0 does not allow for creating multiple artifacts with the same name in the same workflow run. + * It is possible to have multiple artifacts with the same name in the same workflow run by using old versions of upload-artifact (v1,v2 and v3) or @actions/artifact < v2.0.0 + * If there are multiple artifacts with the same name in the same workflow run this function will return the first artifact that matches the name. + * + * @param artifactName The name of the artifact to find + * @param workflowRunId The workflow run id that the artifact belongs to + * @param repositoryOwner The owner of the repository that the artifact belongs to + * @param repositoryName The name of the repository that the artifact belongs to + * @param token A token with the appropriate permission to the repository to find the artifact + */ + getArtifact( + artifactName: string, + workflowRunId: number, + repositoryOwner: string, + repositoryName: string, + token: string + ): Promise + + /** + * Downloads an artifact and unzips the content + * + * @param artifactId The name of the artifact to download + * @param repositoryOwner The owner of the repository that the artifact belongs to + * @param repositoryName The name of the repository that the artifact belongs to + * @param token A token with the appropriate permission to the repository to download the artifact + * @param options Extra options that allow for the customization of the download behavior + * @returns single DownloadArtifactResponse object + */ + downloadArtifact( + artifactId: number, + repositoryOwner: string, + repositoryName: string, + token: string, + options?: DownloadSingleArtifactOptions + ): Promise } export class Client implements ArtifactClient { @@ -33,7 +99,7 @@ export class Client implements ArtifactClient { } /** - * Uploads an artifact + * Upload Artifact */ async uploadArtifact( name: string, @@ -43,7 +109,7 @@ export class Client implements ArtifactClient { ): Promise { if (isGhes()) { warning( - `@actions/artifact v2 and upload-artifact v4 are not currently supported on GHES.` + `@actions/artifact v2.0.0+ and upload-artifact@v4+ are not currently supported on GHES.` ) return { success: false @@ -56,9 +122,132 @@ export class Client implements ArtifactClient { warning( `Artifact upload failed with error: ${error}. -Errors can be temporary, so please try again and optionally run the action with debug enabled for more information. +Errors can be temporary, so please try again and optionally run the action with debug mode enabled for more information. -If the error persists, please check whether Actions is running normally at [https://githubstatus.com](https://www.githubstatus.com).` +If the error persists, please check whether Actions is operating normally at [https://githubstatus.com](https://www.githubstatus.com).` + ) + return { + success: false + } + } + } + + /** + * Download Artifact + */ + async downloadArtifact( + artifactId: number, + repositoryOwner: string, + repositoryName: string, + token: string, + options?: DownloadSingleArtifactOptions + ): Promise { + if (isGhes()) { + warning( + `@actions/artifact v2.0.0+ and download-artifact@v4+ are not currently supported on GHES.` + ) + return { + success: false + } + } + + try { + return downloadArtifact( + artifactId, + repositoryOwner, + repositoryName, + token, + options + ) + } catch (error) { + warning( + `Artifact download failed with error: ${error}. + +Errors can be temporary, so please try again and optionally run the action with debug mode enabled for more information. + +If the error persists, please check whether Actions and API requests are operating normally at [https://githubstatus.com](https://www.githubstatus.com).` + ) + + return { + success: false + } + } + } + + /** + * List Artifacts + */ + async listArtifacts( + workflowRunId: number, + repositoryOwner: string, + repositoryName: string, + token: string + ): Promise { + if (isGhes()) { + warning( + `@actions/artifact v2.0.0+ and download-artifact@v4+ are not currently supported on GHES.` + ) + return { + artifacts: [] + } + } + + try { + return listArtifacts( + workflowRunId, + repositoryOwner, + repositoryName, + token + ) + } catch (error: unknown) { + warning( + `Listing Artifacts failed with error: ${error}. + +Errors can be temporary, so please try again and optionally run the action with debug mode enabled for more information. + +If the error persists, please check whether Actions and API requests are operating normally at [https://githubstatus.com](https://www.githubstatus.com).` + ) + + return { + artifacts: [] + } + } + } + + /** + * Get Artifact + */ + async getArtifact( + artifactName: string, + workflowRunId: number, + repositoryOwner: string, + repositoryName: string, + token: string + ): Promise { + if (isGhes()) { + warning( + `@actions/artifact v2.0.0+ and download-artifact@v4+ are not currently supported on GHES.` + ) + return { + success: false + } + } + + try { + return getArtifact( + artifactName, + workflowRunId, + repositoryOwner, + repositoryName, + token + ) + } catch (error: unknown) { + warning( + `Fetching Artifact failed with error: ${error}. + +Errors can be temporary, so please try again and optionally run the action with debug mode enabled for more information. + +If the error persists, please check whether Actions and API requests are operating normally at [https://githubstatus.com](https://www.githubstatus.com).` ) return { success: false diff --git a/packages/artifact/src/internal/download/download-artifact.ts b/packages/artifact/src/internal/download/download-artifact.ts new file mode 100644 index 00000000..922a849a --- /dev/null +++ b/packages/artifact/src/internal/download/download-artifact.ts @@ -0,0 +1,14 @@ +import { + DownloadSingleArtifactOptions, + DownloadArtifactResponse +} from '../shared/interfaces' + +export async function downloadArtifact( + artifactId: number, + repositoryOwner: string, + repositoryName: string, + token: string, + options?: DownloadSingleArtifactOptions +): Promise { + throw new Error('Not implemented') +} diff --git a/packages/artifact/src/internal/find/get-artifact.ts b/packages/artifact/src/internal/find/get-artifact.ts new file mode 100644 index 00000000..3b28ff91 --- /dev/null +++ b/packages/artifact/src/internal/find/get-artifact.ts @@ -0,0 +1,11 @@ +import {GetArtifactResponse} from '../shared/interfaces' + +export async function getArtifact( + artifactName: string, + workflowRunId: number, + repositoryOwner: string, + repositoryName: string, + token: string +): Promise { + throw new Error('Not implemented') +} diff --git a/packages/artifact/src/internal/find/list-artifacts.ts b/packages/artifact/src/internal/find/list-artifacts.ts new file mode 100644 index 00000000..4c1e94d0 --- /dev/null +++ b/packages/artifact/src/internal/find/list-artifacts.ts @@ -0,0 +1,108 @@ +import {warning} 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 {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 +} + +// 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, + repositoryOwner: string, + repositoryName: string, + token: string +): Promise { + const artifacts: Artifact[] = [] + const [retryOpts, requestOpts] = getRetryOptions( + maxRetryNumber, + exemptStatusCodes, + defaultGitHubOptions + ) + + const opts: Options = { + log: undefined, + userAgent: getUserAgentString(), + previews: undefined, + retry: retryOpts, + request: requestOpts + } + + const github = getOctokit(token, opts, retry, requestLog) + + let currentPageNumber = 1 + const {data: listArtifactResponse} = + await github.rest.actions.listWorkflowRunArtifacts({ + owner: repositoryOwner, + repo: repositoryName, + run_id: workflowRunId, + per_page: paginationCount, + page: currentPageNumber + }) + + let numberOfPages = Math.ceil( + listArtifactResponse.total_count / paginationCount + ) + const totalArtifactCount = listArtifactResponse.total_count + if (totalArtifactCount > maximumArtifactCount) { + warning( + `Workflow run ${workflowRunId} has more than 1000 artifacts. Results will be incomplete as only the first ${maximumArtifactCount} artifacts will be returned` + ) + numberOfPages = maxNumberOfPages + } + + // Iterate over the first page + listArtifactResponse.artifacts.forEach(artifact => { + artifacts.push({ + artifactName: artifact.name, + artifactId: artifact.id, + url: artifact.url, + size: artifact.size_in_bytes + }) + }) + + for ( + currentPageNumber; + currentPageNumber < numberOfPages; + currentPageNumber++ + ) { + currentPageNumber++ + const {data: listArtifactResponse} = + await github.rest.actions.listWorkflowRunArtifacts({ + owner: repositoryOwner, + repo: repositoryName, + run_id: workflowRunId, + per_page: paginationCount, + page: currentPageNumber + }) + + listArtifactResponse.artifacts.forEach(artifact => { + artifacts.push({ + artifactName: artifact.name, + artifactId: artifact.id, + url: artifact.url, + size: artifact.size_in_bytes + }) + }) + } + + return { + artifacts: artifacts + } +} diff --git a/packages/artifact/src/internal/find/retry-options.ts b/packages/artifact/src/internal/find/retry-options.ts new file mode 100644 index 00000000..3fe51b5a --- /dev/null +++ b/packages/artifact/src/internal/find/retry-options.ts @@ -0,0 +1,44 @@ +import * as core from '@actions/core' +import {OctokitOptions} from '@octokit/core/dist-types/types' +import {RequestRequestOptions} from '@octokit/types' + +export type RetryOptions = { + doNotRetry?: number[] + enabled?: boolean +} + +export function getRetryOptions( + retries: number, + exemptStatusCodes: number[], + defaultOptions: OctokitOptions +): [RetryOptions, RequestRequestOptions | undefined] { + if (retries <= 0) { + return [{enabled: false}, defaultOptions.request] + } + + const retryOptions: RetryOptions = { + enabled: true + } + + if (exemptStatusCodes.length > 0) { + retryOptions.doNotRetry = exemptStatusCodes + } + + // The GitHub type has some defaults for `options.request` + // see: https://github.com/actions/toolkit/blob/4fbc5c941a57249b19562015edbd72add14be93d/packages/github/src/utils.ts#L15 + // We pass these in here so they are not overridden. + const requestOptions: RequestRequestOptions = { + ...defaultOptions.request, + retries + } + + core.debug( + `GitHub client configured with: (retries: ${ + requestOptions.retries + }, retry-exempt-status-code: ${ + retryOptions.doNotRetry ?? 'octokit default: [400, 401, 403, 404, 422]' + })` + ) + + return [retryOptions, requestOptions] +} diff --git a/packages/artifact/src/internal/shared/interfaces.ts b/packages/artifact/src/internal/shared/interfaces.ts new file mode 100644 index 00000000..3eb9f221 --- /dev/null +++ b/packages/artifact/src/internal/shared/interfaces.ts @@ -0,0 +1,152 @@ +/***************************************************************************** + * * + * UploadArtifact * + * * + *****************************************************************************/ +export interface UploadResponse { + /** + * Denotes if an artifact was successfully uploaded + */ + success: boolean + + /** + * Total size of the artifact in bytes. Not provided if no artifact was uploaded + */ + size?: number + + /** + * The id of the artifact that was created. Not provided if no artifact was uploaded + * This ID can be used as input to other APIs to download, delete or get more information about an artifact: https://docs.github.com/en/rest/actions/artifacts + */ + id?: number +} + +export interface UploadOptions { + /** + * Duration after which artifact will expire in days. + * + * By default artifact expires after 90 days: + * https://docs.github.com/en/actions/configuring-and-managing-workflows/persisting-workflow-data-using-artifacts#downloading-and-deleting-artifacts-after-a-workflow-run-is-complete + * + * Use this option to override the default expiry. + * + * Min value: 1 + * Max value: 90 unless changed by repository setting + * + * If this is set to a greater value than the retention settings allowed, the retention on artifacts + * will be reduced to match the max value allowed on server, and the upload process will continue. An + * input of 0 assumes default retention setting. + */ + retentionDays?: number +} + +/***************************************************************************** + * * + * GetArtifact * + * * + *****************************************************************************/ + +export interface GetArtifactResponse { + /** + * If an artifact was found + */ + success: boolean + + /** + * Metadata about the artifact that was found + */ + artifact?: Artifact +} + +/***************************************************************************** + * * + * ListArtifact * + * * + *****************************************************************************/ +export interface ListArtifactsResponse { + /** + * A list of artifacts that were found + */ + artifacts: Artifact[] +} + +/***************************************************************************** + * * + * DownloadArtifact * + * * + *****************************************************************************/ +export interface DownloadArtifactResponse { + /** + * If the artifact download was successful + */ + success: boolean + + /** + * Metadata about the artifact that was downloaded + */ + artifact?: Artifact +} + +export interface DownloadAllArtifactsResponse { + /** + * If the artifact download was successful + */ + success: boolean + + /** + * The cumulative size of all the artifacts that were downloaded + */ + size: number + + /** + * Metadata about the artifact that was downloaded + */ + artifacts: Artifact[] +} + +export interface DownloadSingleArtifactOptions { + /** + * 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 +} + +export interface DownloadAllArtifactsOptions { + /** + * Denotes where the artifact will be downloaded to. If not specified then the artifact is download to GITHUB_WORKSPACE + */ + path?: string +} + +/***************************************************************************** + * * + * Shared * + * * + *****************************************************************************/ +export interface Artifact { + /** + * The name of the artifact + */ + artifactName: string + + /** + * The ID of the artifact + */ + artifactId: number + + /** + * The URL of the artifact + */ + url: string + + /** + * The size of the artifact in bytes + */ + size: number +} diff --git a/packages/artifact/src/internal/shared/user-agent.ts b/packages/artifact/src/internal/shared/user-agent.ts new file mode 100644 index 00000000..eee01446 --- /dev/null +++ b/packages/artifact/src/internal/shared/user-agent.ts @@ -0,0 +1,8 @@ +var 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 + */ +export function getUserAgentString(): string { + return `@actions/artifact-${packageJson.version}` +} diff --git a/packages/artifact/src/internal/upload/blob-upload.ts b/packages/artifact/src/internal/upload/blob-upload.ts index ede3f00a..82af468a 100644 --- a/packages/artifact/src/internal/upload/blob-upload.ts +++ b/packages/artifact/src/internal/upload/blob-upload.ts @@ -70,18 +70,21 @@ export async function uploadZipToBlobStorage( hashStream.end() md5Hash = hashStream.read() as string core.info(`MD5 hash of uploaded artifact zip is ${md5Hash}`) - } catch (error) { - core.warning(`Failed to upload artifact zip to blob storage, error: ${error}`) + core.warning( + `Failed to upload artifact zip to blob storage, error: ${error}` + ) return { isSuccess: false } } if (uploadByteCount === 0) { - core.warning(`No data was uploaded to blob storage. Reported upload byte count is 0`) + core.warning( + `No data was uploaded to blob storage. Reported upload byte count is 0` + ) return { - isSuccess: false, + isSuccess: false } } diff --git a/packages/artifact/src/internal/upload/upload-artifact.ts b/packages/artifact/src/internal/upload/upload-artifact.ts index 87e687fc..35cd9ac3 100644 --- a/packages/artifact/src/internal/upload/upload-artifact.ts +++ b/packages/artifact/src/internal/upload/upload-artifact.ts @@ -1,6 +1,5 @@ import * as core from '@actions/core' -import {UploadOptions} from './upload-options' -import {UploadResponse} from './upload-response' +import {UploadOptions, UploadResponse} from '../shared/interfaces' import {getExpiration} from './retention' import {validateArtifactName} from './path-and-artifact-name-validation' import {createArtifactTwirpClient} from '../shared/artifact-twirp-client' diff --git a/packages/artifact/src/internal/upload/upload-options.ts b/packages/artifact/src/internal/upload/upload-options.ts deleted file mode 100644 index 66df123a..00000000 --- a/packages/artifact/src/internal/upload/upload-options.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface UploadOptions { - /** - * Duration after which artifact will expire in days. - * - * By default artifact expires after 90 days: - * https://docs.github.com/en/actions/configuring-and-managing-workflows/persisting-workflow-data-using-artifacts#downloading-and-deleting-artifacts-after-a-workflow-run-is-complete - * - * Use this option to override the default expiry. - * - * Min value: 1 - * Max value: 90 unless changed by repository setting - * - * If this is set to a greater value than the retention settings allowed, the retention on artifacts - * will be reduced to match the max value allowed on server, and the upload process will continue. An - * input of 0 assumes default retention setting. - */ - retentionDays?: number -} diff --git a/packages/artifact/src/internal/upload/upload-response.ts b/packages/artifact/src/internal/upload/upload-response.ts deleted file mode 100644 index 0c968cfb..00000000 --- a/packages/artifact/src/internal/upload/upload-response.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface UploadResponse { - /** - * Denotes if an artifact was successfully uploaded - */ - success: boolean - - /** - * Total size of the artifact in bytes. Not provided if no artifact was uploaded - */ - size?: number - - /** - * The id of the artifact that was created. Not provided if no artifact was uploaded - * This ID can be used as input to other APIs to download, delete or get more information about an artifact: https://docs.github.com/en/rest/actions/artifacts - */ - id?: number -}