Repository: actions/toolkit
Pull request #1495 (2023-08-17 00:07:47)

[Artifacts] @actions/artifact list artifact functionality + download interface setup
Part of https://github.com/github/c2c-actions-checks/issues/1562

## API Contracts

Simplifying much of the existing logic by having a single shared `interfaces.ts` file that defines what each function accepts and returns.

Adding the skeleton structure for `downloadArtifact` and `getArtifact`

## List Artifacts

Adding a new function called `listArtifacts` that paginates through all artifacts for a workflow run and lists at most 1000 artifacts.

Adding a new verify job that runs after all 3 separate uploads finish

<img width="551" alt="image" src="https://github.com/actions/toolkit/assets/16109154/5206d07d-b294-449f-ae26-0c5fcf22b21b">

The verify job calls the `listArtifacts` method call and we make sure that all 3 of the artifacts that were recently uploaded are present. See the output of this recent run: https://github.com/actions/toolkit/actions/runs/5894413718?pr=1495

<img width="1152" alt="image" src="https://github.com/actions/toolkit/assets/16109154/3645c63b-2f4d-4f19-83b0-c64bf1007fa6">



Patch:
diff --git a/packages/artifact/package-lock.json b/packages/artifact/package-lock.json
index 05b66fd..90c4bd6 100644
--- a/packages/artifact/package-lock.json
+++ b/packages/artifact/package-lock.json
@@ -10,8 +10,12 @@
       "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 9845862..b9e8663 100644
--- a/packages/artifact/package.json
+++ b/packages/artifact/package.json
@@ -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 b53733f..b566df8 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 f827d88..6ffc632 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,
+  DownloadArtifactOptions,
+  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<UploadResponse>
 
-  // 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<ListArtifactsResponse>
+
+  /**
+   * 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<GetArtifactResponse>
+
+  /**
+   * 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?: DownloadArtifactOptions
+  ): Promise<DownloadArtifactResponse>
 }
 
 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<UploadResponse> {
     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 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?: DownloadArtifactOptions
+  ): Promise<DownloadArtifactResponse> {
+    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<ListArtifactsResponse> {
+    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<GetArtifactResponse> {
+    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 is running normally at [https://githubstatus.com](https://www.githubstatus.com).`
+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 0000000..7e9dfbf
--- /dev/null
+++ b/packages/artifact/src/internal/download/download-artifact.ts
@@ -0,0 +1,14 @@
+import {
+  DownloadArtifactOptions,
+  DownloadArtifactResponse
+} from '../shared/interfaces'
+
+export async function downloadArtifact(
+  artifactId: number,
+  repositoryOwner: string,
+  repositoryName: string,
+  token: string,
+  options?: DownloadArtifactOptions
+): Promise<DownloadArtifactResponse> {
+  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 0000000..3b28ff9
--- /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<GetArtifactResponse> {
+  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 0000000..7fd9683
--- /dev/null
+++ b/packages/artifact/src/internal/find/list-artifacts.ts
@@ -0,0 +1,115 @@
+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 {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<ListArtifactsResponse> {
+  info(`Fetching artifact list for workflow run ${workflowRunId} in repository ${repositoryOwner}/${repositoryName}`)
+
+  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({
+      name: artifact.name,
+      id: artifact.id,
+      url: artifact.url,
+      size: artifact.size_in_bytes
+    })
+  })
+
+  // Iterate over any remaining pages
+  for (
+    currentPageNumber;
+    currentPageNumber < numberOfPages;
+    currentPageNumber++
+  ) {
+    currentPageNumber++
+    debug(`Fetching page ${currentPageNumber} of artifact list`)
+
+    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({
+        name: artifact.name,
+        id: artifact.id,
+        url: artifact.url,
+        size: artifact.size_in_bytes
+      })
+    })
+  }
+
+  info(`Finished fetching artifact list`)
+
+  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 0000000..3fe51b5
--- /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 0000000..d1f7a7a
--- /dev/null
+++ b/packages/artifact/src/internal/shared/interfaces.ts
@@ -0,0 +1,123 @@
+/*****************************************************************************
+ *                                                                           *
+ *                            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
+}
+
+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
+}
+
+/*****************************************************************************
+ *                                                                           *
+ *                                 Shared                                    *
+ *                                                                           *
+ *****************************************************************************/
+export interface Artifact {
+  /**
+   * The name of the artifact
+   */
+  name: string
+
+  /**
+   * The ID of the artifact
+   */
+  id: 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 0000000..eee0144
--- /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 ede3f00..82af468 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 87e687f..35cd9ac 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 66df123..0000000
--- 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 0c968cf..0000000
--- 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
-}


Test patch:
diff --git a/.github/workflows/artifact-tests.yml b/.github/workflows/artifact-tests.yml
index d354198..2b150fc 100644
--- a/.github/workflows/artifact-tests.yml
+++ b/.github/workflows/artifact-tests.yml
@@ -54,26 +54,82 @@ jobs:
         echo '${{ env.file1 }}' > artifact-path/first.txt
         echo '${{ env.file2 }}' > artifact-path/second.txt
 
-    - uses: actions/github-script@v6
+    - name: Upload Artifacts using actions/github-script@v6
+      uses: actions/github-script@v6
       with:
         script: |
-            const artifact = require('./packages/artifact/lib/artifact')
+          const artifact = require('./packages/artifact/lib/artifact')
 
-            const artifactName = 'my-artifact-${{ matrix.runs-on }}'
-            console.log('artifactName: ' + artifactName)
+          const artifactName = 'my-artifact-${{ matrix.runs-on }}'
+          console.log('artifactName: ' + artifactName)
 
-            const fileContents = ['artifact-path/first.txt','artifact-path/second.txt']
+          const fileContents = ['artifact-path/first.txt','artifact-path/second.txt']
 
-            const uploadResult = await artifact.create().uploadArtifact(artifactName, fileContents, './')
-            console.log(uploadResult)
+          const uploadResult = await artifact.create().uploadArtifact(artifactName, fileContents, './')
+          console.log(uploadResult)
 
-            const success = uploadResult.success
-            const size = uploadResult.size
-            const id = uploadResult.id
+          const success = uploadResult.success
+          const size = uploadResult.size
+          const id = uploadResult.id
 
-            if (!success) {
-              throw new Error('Failed to upload artifact')
-            } else {
-              console.log(`Successfully uploaded artifact ${id}`)
-            }
+          if (!success) {
+            throw new Error('Failed to upload artifact')
+          } else {
+            console.log(`Successfully uploaded artifact ${id}`)
+          }
            
+  verify:
+    runs-on: ubuntu-latest
+    needs: [build]
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v3
+
+    - name: Set Node.js 16.x
+      uses: actions/setup-node@v3
+      with:
+        node-version: 16.x
+
+    # Need root node_modules because certain npm packages like jest are configured for the entire repository and it won't be possible
+    # without these to just compile the artifacts package
+    - name: Install root npm packages
+      run: npm ci
+
+    - name: Compile artifact package
+      run: |
+        npm ci
+        npm run tsc
+      working-directory: packages/artifact
+
+    - name: List artifacts using actions/github-script@v6
+      uses: actions/github-script@v6
+      with:
+        script: |
+          const artifact = require('./packages/artifact/lib/artifact')
+
+          const workflowRunId = process.env.GITHUB_RUN_ID
+          const repository = process.env.GITHUB_REPOSITORY
+          const repositoryOwner = repository.split('/')[0]
+          const repositoryName = repository.split('/')[1]
+
+          const listResult = await artifact.create().listArtifacts(workflowRunId, repositoryOwner, repositoryName, '${{ secrets.GITHUB_TOKEN }}')
+          console.log(listResult)
+
+          const artifacts = listResult.artifacts
+
+          if (artifacts.length !== 3) {
+            throw new Error('Expected 3 artifacts but only found ' + artifacts.length + ' artifacts')
+          }
+
+          const artifactNames = artifacts.map(artifact => artifact.name)
+          if (!artifactNames.includes('my-artifact-ubuntu-latest')){
+            throw new Error("Expected artifact list to contain an artifact named my-artifact-ubuntu-latest but it's missing")
+          }
+          if (!artifactNames.includes('my-artifact-windows-latest')){
+            throw new Error("Expected artifact list to contain an artifact named my-artifact-windows-latest but it's missing")
+          }
+          if (!artifactNames.includes('my-artifact-macos-latest')){
+            throw new Error("Expected artifact list to contain an artifact named my-artifact-macos-latest but it's missing")
+          }
+
+          console.log('Successfully listed artifacts that were uploaded')