mirror of
https://github.com/actions/toolkit.git
synced 2026-08-23 00:00:19 +02:00
- Fix off-by-one error in pagination loop (< to <=) that prevented fetching last page - Add Math.ceil() to maxNumberOfPages calculation for proper limit handling - Replace hardcoded 2000 limit with configurable getMaxArtifactListCount() - Add pagination test for multi-page artifact listing - Add environment variable test for ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT - Add comprehensive test coverage for getMaxArtifactListCount() function Fixes compound bug where pagination and limit logic capped results at 900 artifacts instead of intended 1000.
188 lines
5.4 KiB
TypeScript
188 lines
5.4 KiB
TypeScript
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 {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 {OctokitOptions} from '@octokit/core/dist-types/types'
|
|
import {internalArtifactTwirpClient} from '../shared/artifact-twirp-client'
|
|
import {getBackendIdsFromToken} from '../shared/util'
|
|
import {getMaxArtifactListCount} from '../shared/config'
|
|
import {ListArtifactsRequest, Timestamp} from '../../generated'
|
|
|
|
const maximumArtifactCount = getMaxArtifactListCount()
|
|
const paginationCount = 100
|
|
const maxNumberOfPages = Math.ceil(maximumArtifactCount / paginationCount)
|
|
|
|
export async function listArtifactsPublic(
|
|
workflowRunId: number,
|
|
repositoryOwner: string,
|
|
repositoryName: string,
|
|
token: string,
|
|
latest = false
|
|
): Promise<ListArtifactsResponse> {
|
|
info(
|
|
`Fetching artifact list for workflow run ${workflowRunId} in repository ${repositoryOwner}/${repositoryName}`
|
|
)
|
|
|
|
let artifacts: Artifact[] = []
|
|
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)
|
|
|
|
let currentPageNumber = 1
|
|
|
|
const {data: listArtifactResponse} = await github.request(
|
|
'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts',
|
|
{
|
|
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 ${totalArtifactCount} artifacts, exceeding the limit of ${maximumArtifactCount}. Results will be incomplete as only the first ${maximumArtifactCount} artifacts will be returned`
|
|
)
|
|
numberOfPages = maxNumberOfPages
|
|
}
|
|
|
|
// Iterate over the first page
|
|
for (const artifact of listArtifactResponse.artifacts) {
|
|
artifacts.push({
|
|
name: artifact.name,
|
|
id: artifact.id,
|
|
size: artifact.size_in_bytes,
|
|
createdAt: artifact.created_at
|
|
? new Date(artifact.created_at)
|
|
: undefined,
|
|
digest: (artifact as ArtifactResponse).digest
|
|
})
|
|
}
|
|
// Move to the next page
|
|
currentPageNumber++
|
|
// Iterate over any remaining pages
|
|
for (
|
|
currentPageNumber;
|
|
currentPageNumber <= numberOfPages;
|
|
currentPageNumber++
|
|
) {
|
|
debug(`Fetching page ${currentPageNumber} of artifact list`)
|
|
|
|
const {data: listArtifactResponse} = await github.request(
|
|
'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts',
|
|
{
|
|
owner: repositoryOwner,
|
|
repo: repositoryName,
|
|
run_id: workflowRunId,
|
|
per_page: paginationCount,
|
|
page: currentPageNumber
|
|
}
|
|
)
|
|
|
|
for (const artifact of listArtifactResponse.artifacts) {
|
|
artifacts.push({
|
|
name: artifact.name,
|
|
id: artifact.id,
|
|
size: artifact.size_in_bytes,
|
|
createdAt: artifact.created_at
|
|
? new Date(artifact.created_at)
|
|
: undefined,
|
|
digest: (artifact as ArtifactResponse).digest
|
|
})
|
|
}
|
|
}
|
|
|
|
if (latest) {
|
|
artifacts = filterLatest(artifacts)
|
|
}
|
|
|
|
info(`Found ${artifacts.length} artifact(s)`)
|
|
|
|
return {
|
|
artifacts
|
|
}
|
|
}
|
|
|
|
export async function listArtifactsInternal(
|
|
latest = false
|
|
): Promise<ListArtifactsResponse> {
|
|
const artifactClient = internalArtifactTwirpClient()
|
|
|
|
const {workflowRunBackendId, workflowJobRunBackendId} =
|
|
getBackendIdsFromToken()
|
|
|
|
const req: ListArtifactsRequest = {
|
|
workflowRunBackendId,
|
|
workflowJobRunBackendId
|
|
}
|
|
|
|
const res = await artifactClient.ListArtifacts(req)
|
|
let artifacts: Artifact[] = res.artifacts.map(artifact => ({
|
|
name: artifact.name,
|
|
id: Number(artifact.databaseId),
|
|
size: Number(artifact.size),
|
|
createdAt: artifact.createdAt
|
|
? Timestamp.toDate(artifact.createdAt)
|
|
: undefined,
|
|
digest: artifact.digest?.value
|
|
}))
|
|
|
|
if (latest) {
|
|
artifacts = filterLatest(artifacts)
|
|
}
|
|
|
|
info(`Found ${artifacts.length} artifact(s)`)
|
|
|
|
return {
|
|
artifacts
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This exists so that we don't have to use 'any' when receiving the artifact list from the GitHub API.
|
|
* The digest field is not present in OpenAPI/types at time of writing, which necessitates this change.
|
|
*/
|
|
interface ArtifactResponse {
|
|
name: string
|
|
id: number
|
|
size_in_bytes: number
|
|
created_at?: string
|
|
digest?: string
|
|
}
|
|
|
|
/**
|
|
* Filters a list of artifacts to only include the latest artifact for each name
|
|
* @param artifacts The artifacts to filter
|
|
* @returns The filtered list of artifacts
|
|
*/
|
|
function filterLatest(artifacts: Artifact[]): Artifact[] {
|
|
artifacts.sort((a, b) => b.id - a.id)
|
|
const latestArtifacts: Artifact[] = []
|
|
const seenArtifactNames = new Set<string>()
|
|
for (const artifact of artifacts) {
|
|
if (!seenArtifactNames.has(artifact.name)) {
|
|
latestArtifacts.push(artifact)
|
|
seenArtifactNames.add(artifact.name)
|
|
}
|
|
}
|
|
return latestArtifacts
|
|
}
|