mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-07 01:28:46 +00:00
713 lines
21 KiB
JavaScript
713 lines
21 KiB
JavaScript
import { GITHUB_API, USER_AGENT } from "./constants.mjs";
|
|
import { normalizeRepository } from "./state.mjs";
|
|
|
|
const PULL_ARTIFACT_FILTERS = new Set(["all", "with", "without"]);
|
|
const PULL_CI_FILTERS = new Set(["all", "failing", "passing", "pending", "none"]);
|
|
const ARTIFACT_PRESENCE_TTL_MS = 5 * 60 * 1_000;
|
|
const MISSING_ARTIFACT_TTL_MS = 30 * 1_000;
|
|
const ARTIFACT_PRESENCE_CACHE_LIMIT = 1_000;
|
|
// Each run fans out into paginated artifact requests, so keep the initial view bounded.
|
|
const MAX_PULL_REQUEST_RUNS = 30;
|
|
const MAX_PULL_REQUEST_ARTIFACTS = 2_000;
|
|
const artifactPresenceCache = new Map();
|
|
|
|
export class GitHubApiError extends Error {
|
|
constructor(message, status, details = null) {
|
|
super(message);
|
|
this.name = "GitHubApiError";
|
|
this.status = status;
|
|
this.details = details;
|
|
}
|
|
}
|
|
|
|
function repositoryPath(repository) {
|
|
return normalizeRepository(repository).split("/").map(encodeURIComponent).join("/");
|
|
}
|
|
|
|
function headersFor(token, extra = {}) {
|
|
return {
|
|
Accept: "application/vnd.github+json",
|
|
Authorization: `Bearer ${token}`,
|
|
"User-Agent": USER_AGENT,
|
|
"X-GitHub-Api-Version": "2022-11-28",
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
async function readPayload(response) {
|
|
const text = await response.text();
|
|
if (!text) return null;
|
|
const type = response.headers.get("content-type") ?? "";
|
|
if (type.includes("json")) {
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch (error) {
|
|
throw new GitHubApiError("GitHub returned malformed JSON.", response.status, {
|
|
cause: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
}
|
|
return text;
|
|
}
|
|
|
|
export async function githubRequest(token, path, options = {}) {
|
|
if (!token) throw new GitHubApiError("No GitHub account is selected.", 401);
|
|
if (!path.startsWith("/")) throw new Error("GitHub API paths must be absolute.");
|
|
|
|
const response = await fetch(`${GITHUB_API}${path}`, {
|
|
method: options.method ?? "GET",
|
|
headers: headersFor(token, options.headers),
|
|
body: options.body,
|
|
signal: options.signal ?? AbortSignal.timeout(30_000),
|
|
});
|
|
const payload = await readPayload(response);
|
|
if (!response.ok) {
|
|
const message =
|
|
payload && typeof payload === "object" && payload.message
|
|
? payload.message
|
|
: `GitHub API request failed (${response.status}).`;
|
|
throw new GitHubApiError(message, response.status, payload);
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
function repositorySummary(repository) {
|
|
return {
|
|
fullName: repository.full_name,
|
|
name: repository.name,
|
|
owner: {
|
|
login: repository.owner?.login ?? "",
|
|
avatarUrl: repository.owner?.avatar_url ?? null,
|
|
type: repository.owner?.type ?? null,
|
|
},
|
|
description: repository.description ?? null,
|
|
private: repository.private === true,
|
|
archived: repository.archived === true,
|
|
visibility: repository.visibility ?? (repository.private ? "private" : "public"),
|
|
url: repository.html_url,
|
|
updatedAt: repository.updated_at ?? null,
|
|
};
|
|
}
|
|
|
|
export async function getRepository(token, repository) {
|
|
return repositorySummary(
|
|
await githubRequest(token, `/repos/${repositoryPath(repository)}`),
|
|
);
|
|
}
|
|
|
|
async function contributorProfilesByNodeId(token, contributors) {
|
|
const ids = contributors
|
|
.map((contributor) => contributor.node_id)
|
|
.filter(Boolean);
|
|
if (!ids.length) return new Map();
|
|
|
|
const payload = await githubRequest(token, "/graphql", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
query: `
|
|
query ContributorProfiles($ids: [ID!]!) {
|
|
nodes(ids: $ids) {
|
|
id
|
|
... on User {
|
|
login
|
|
name
|
|
}
|
|
... on Bot {
|
|
login
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
variables: { ids },
|
|
}),
|
|
});
|
|
if (payload?.errors?.length) {
|
|
throw new GitHubApiError(
|
|
payload.errors[0]?.message ?? "GitHub could not load contributor profiles.",
|
|
502,
|
|
payload.errors,
|
|
);
|
|
}
|
|
if (!Array.isArray(payload?.data?.nodes)) {
|
|
throw new GitHubApiError("GitHub returned invalid contributor profile data.", 502, payload);
|
|
}
|
|
|
|
return new Map(
|
|
payload.data.nodes
|
|
.filter((profile) => profile?.id)
|
|
.map((profile) => [profile.id, profile]),
|
|
);
|
|
}
|
|
|
|
export async function listRepositoryContributors(token, repository) {
|
|
const contributors = (
|
|
await githubRequest(
|
|
token,
|
|
`/repos/${repositoryPath(repository)}/contributors?anon=0&per_page=100`,
|
|
)
|
|
).filter((contributor) => contributor?.login);
|
|
const profilesById = await contributorProfilesByNodeId(token, contributors);
|
|
|
|
return contributors
|
|
.map((contributor) => ({
|
|
login: contributor.login,
|
|
name:
|
|
typeof profilesById.get(contributor.node_id)?.name === "string"
|
|
? profilesById.get(contributor.node_id).name.trim() || null
|
|
: null,
|
|
avatarUrl: contributor.avatar_url ?? null,
|
|
profileUrl: contributor.html_url ?? null,
|
|
type: contributor.type ?? null,
|
|
contributions: Number(contributor.contributions) || 0,
|
|
}));
|
|
}
|
|
|
|
export async function suggestRepositories(token, query) {
|
|
const value = String(query ?? "").trim();
|
|
if (!value || value.length > 100) return [];
|
|
|
|
const slash = value.indexOf("/");
|
|
if (slash > 0) {
|
|
const owner = value.slice(0, slash).trim();
|
|
const partial = value.slice(slash + 1).trim();
|
|
if (!/^[A-Za-z0-9_.-]+$/.test(owner)) return [];
|
|
|
|
const profile = await githubRequest(token, `/users/${encodeURIComponent(owner)}`);
|
|
if (partial) {
|
|
const qualifier = profile.type === "Organization" ? "org" : "user";
|
|
const result = await githubRequest(
|
|
token,
|
|
`/search/repositories?q=${encodeURIComponent(`${partial} in:name ${qualifier}:${owner}`)}&sort=updated&order=desc&per_page=12`,
|
|
);
|
|
return (result.items ?? []).map(repositorySummary);
|
|
}
|
|
|
|
const endpoint =
|
|
profile.type === "Organization"
|
|
? `/orgs/${encodeURIComponent(owner)}/repos?type=all&sort=updated&direction=desc&per_page=20`
|
|
: `/users/${encodeURIComponent(owner)}/repos?type=all&sort=updated&direction=desc&per_page=20`;
|
|
return (await githubRequest(token, endpoint)).map(repositorySummary);
|
|
}
|
|
|
|
if (value.length < 2) return [];
|
|
const result = await githubRequest(
|
|
token,
|
|
`/search/repositories?q=${encodeURIComponent(`${value} in:name`)}&sort=updated&order=desc&per_page=12`,
|
|
);
|
|
return (result.items ?? []).map(repositorySummary);
|
|
}
|
|
|
|
function mapPullRequest(item) {
|
|
return {
|
|
number: item.number,
|
|
title: item.title,
|
|
state: item.state,
|
|
draft: Boolean(item.draft),
|
|
url: item.html_url,
|
|
author: item.user?.login ?? "ghost",
|
|
authorAvatarUrl: item.user?.avatar_url ?? null,
|
|
authorType: item.user?.type ?? null,
|
|
authorAssociation: item.author_association ?? null,
|
|
comments: Number(item.comments) || 0,
|
|
createdAt: item.created_at,
|
|
updatedAt: item.updated_at,
|
|
closedAt: item.closed_at,
|
|
mergedAt: item.merged_at ?? null,
|
|
headSha: item.head?.sha ?? null,
|
|
headRef: item.head?.ref ?? null,
|
|
baseRef: item.base?.ref ?? null,
|
|
labels: (item.labels ?? []).map((label) => ({
|
|
name: label.name,
|
|
color: label.color,
|
|
description: label.description ?? null,
|
|
})),
|
|
};
|
|
}
|
|
|
|
function mapSearchPullRequest(item) {
|
|
return {
|
|
number: item.number,
|
|
title: item.title,
|
|
state: item.state,
|
|
draft: Boolean(item.draft),
|
|
url: item.html_url,
|
|
author: item.user?.login ?? "ghost",
|
|
authorAvatarUrl: item.user?.avatar_url ?? null,
|
|
authorType: item.user?.type ?? null,
|
|
authorAssociation: item.author_association ?? null,
|
|
comments: Number(item.comments) || 0,
|
|
createdAt: item.created_at,
|
|
updatedAt: item.updated_at,
|
|
closedAt: item.closed_at,
|
|
mergedAt: item.pull_request?.merged_at ?? null,
|
|
headSha: null,
|
|
headRef: null,
|
|
baseRef: null,
|
|
labels: (item.labels ?? []).map((label) => ({
|
|
name: label.name,
|
|
color: label.color,
|
|
description: label.description ?? null,
|
|
})),
|
|
};
|
|
}
|
|
|
|
function normalizePullFilters(filters = {}) {
|
|
return {
|
|
artifacts: PULL_ARTIFACT_FILTERS.has(filters.artifacts) ? filters.artifacts : "all",
|
|
ci: PULL_CI_FILTERS.has(filters.ci) ? filters.ci : "all",
|
|
};
|
|
}
|
|
|
|
function ciStatusForRollup(state) {
|
|
if (state === "FAILURE" || state === "ERROR") return "failing";
|
|
if (state === "SUCCESS") return "passing";
|
|
if (state === "PENDING" || state === "EXPECTED") return "pending";
|
|
return "none";
|
|
}
|
|
|
|
async function pullRequestSignals(token, repository, pulls) {
|
|
if (!pulls.length) return new Map();
|
|
const [owner, name] = normalizeRepository(repository).split("/");
|
|
const selections = pulls
|
|
.map(
|
|
(pull, index) => `
|
|
pull${index}: pullRequest(number: ${Number(pull.number)}) {
|
|
number
|
|
isDraft
|
|
headRefOid
|
|
reviewDecision
|
|
totalCommentsCount
|
|
commits(last: 1) {
|
|
nodes {
|
|
commit {
|
|
statusCheckRollup {
|
|
state
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`,
|
|
)
|
|
.join("\n");
|
|
const payload = await githubRequest(token, "/graphql", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
query: `
|
|
query PullRequestSignals($owner: String!, $name: String!) {
|
|
repository(owner: $owner, name: $name) {
|
|
${selections}
|
|
}
|
|
}
|
|
`,
|
|
variables: { owner, name },
|
|
}),
|
|
});
|
|
if (payload?.errors?.length) {
|
|
throw new GitHubApiError(
|
|
payload.errors[0]?.message ?? "GitHub could not load pull request signals.",
|
|
502,
|
|
payload.errors,
|
|
);
|
|
}
|
|
const result = payload?.data?.repository;
|
|
if (!result || typeof result !== "object") {
|
|
throw new GitHubApiError("GitHub returned invalid pull request signal data.", 502, payload);
|
|
}
|
|
|
|
return new Map(
|
|
pulls.map((pull, index) => {
|
|
const signal = result[`pull${index}`];
|
|
if (!signal) {
|
|
throw new GitHubApiError(
|
|
`GitHub did not return signal data for pull request #${pull.number}.`,
|
|
502,
|
|
payload,
|
|
);
|
|
}
|
|
const rollupState =
|
|
signal.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state ?? null;
|
|
return [
|
|
Number(pull.number),
|
|
{
|
|
draft: Boolean(signal.isDraft),
|
|
headSha: signal.headRefOid ?? null,
|
|
ciStatus: ciStatusForRollup(rollupState),
|
|
reviewDecision: signal.reviewDecision ?? null,
|
|
comments: Number(signal.totalCommentsCount) || 0,
|
|
},
|
|
];
|
|
}),
|
|
);
|
|
}
|
|
|
|
async function findArtifactHeads(token, repository) {
|
|
const heads = new Set();
|
|
let artifactCount = 0;
|
|
for (let page = 1; ; page++) {
|
|
const payload = await githubRequest(
|
|
token,
|
|
`/repos/${repositoryPath(repository)}/actions/artifacts?per_page=100&page=${page}`,
|
|
);
|
|
const artifacts = payload.artifacts ?? [];
|
|
artifactCount += artifacts.length;
|
|
for (const artifact of artifacts) {
|
|
const headSha = artifact.workflow_run?.head_sha;
|
|
if (headSha) heads.add(headSha);
|
|
}
|
|
const totalCount = Number(payload.total_count) || 0;
|
|
if (artifacts.length < 100 || artifactCount >= totalCount) return heads;
|
|
}
|
|
}
|
|
|
|
function pruneArtifactPresenceCache() {
|
|
const now = Date.now();
|
|
for (const [key, entry] of artifactPresenceCache) {
|
|
if (entry.positiveExpiresAt <= now) artifactPresenceCache.delete(key);
|
|
}
|
|
while (artifactPresenceCache.size >= ARTIFACT_PRESENCE_CACHE_LIMIT) {
|
|
artifactPresenceCache.delete(artifactPresenceCache.keys().next().value);
|
|
}
|
|
}
|
|
|
|
async function artifactHeadsForPulls(token, repository, headShas) {
|
|
const requestedHeads = new Set(headShas.filter(Boolean));
|
|
if (!requestedHeads.size) return new Set();
|
|
const key = normalizeRepository(repository).toLocaleLowerCase();
|
|
const now = Date.now();
|
|
const cached = artifactPresenceCache.get(key);
|
|
if (cached?.missingExpiresAt > now) return cached.promise;
|
|
if (cached?.positiveExpiresAt > now) {
|
|
const cachedHeads = await cached.promise;
|
|
if ([...requestedHeads].every((headSha) => cachedHeads.has(headSha))) {
|
|
return cachedHeads;
|
|
}
|
|
}
|
|
|
|
pruneArtifactPresenceCache();
|
|
const entry = {
|
|
missingExpiresAt: now + MISSING_ARTIFACT_TTL_MS,
|
|
positiveExpiresAt: now + ARTIFACT_PRESENCE_TTL_MS,
|
|
promise: findArtifactHeads(token, repository),
|
|
};
|
|
artifactPresenceCache.set(key, entry);
|
|
try {
|
|
return await entry.promise;
|
|
} catch (error) {
|
|
if (artifactPresenceCache.get(key) === entry) {
|
|
artifactPresenceCache.delete(key);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function listPullRequests(token, repository, state = "open") {
|
|
const normalizedState = ["open", "closed", "all"].includes(state) ? state : "open";
|
|
const query = new URLSearchParams({
|
|
state: normalizedState,
|
|
sort: "updated",
|
|
direction: "desc",
|
|
per_page: "50",
|
|
});
|
|
const payload = await githubRequest(
|
|
token,
|
|
`/repos/${repositoryPath(repository)}/pulls?${query}`,
|
|
);
|
|
return payload.map(mapPullRequest);
|
|
}
|
|
|
|
function normalizePullSearchQuery(filter, state) {
|
|
const normalizedState = ["open", "closed", "all"].includes(state) ? state : "open";
|
|
let query = String(filter ?? "")
|
|
.trim()
|
|
.slice(0, 500)
|
|
.replace(/(^|\s)(?:repo|org|user):(?:"[^"]+"|\S+)/gi, " ")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
if (!/\bis:pr\b/i.test(query)) query = `is:pr ${query}`.trim();
|
|
if (
|
|
normalizedState !== "all" &&
|
|
!/\bis:(?:open|closed|merged|unmerged)\b/i.test(query)
|
|
) {
|
|
query = `${query} is:${normalizedState}`;
|
|
}
|
|
return query;
|
|
}
|
|
|
|
export async function searchPullRequestBase(
|
|
token,
|
|
repository,
|
|
filter = "",
|
|
state = "open",
|
|
filters = {},
|
|
{ perPage = 100 } = {},
|
|
) {
|
|
const normalizedFilters = normalizePullFilters(filters);
|
|
const query = normalizePullSearchQuery(filter, state);
|
|
const normalizedPerPage = Math.max(1, Math.min(100, Number(perPage) || 100));
|
|
const parameters = new URLSearchParams({
|
|
q: `repo:${normalizeRepository(repository)} ${query}`,
|
|
sort: "updated",
|
|
order: "desc",
|
|
per_page: String(normalizedPerPage),
|
|
});
|
|
const payload = await githubRequest(token, `/search/issues?${parameters}`);
|
|
const sourcePulls = (payload.items ?? []).map(mapSearchPullRequest);
|
|
return {
|
|
query,
|
|
totalCount: Number(payload.total_count) || 0,
|
|
incomplete: payload.incomplete_results === true,
|
|
filters: normalizedFilters,
|
|
evaluatedCount: sourcePulls.length,
|
|
filtered: false,
|
|
pulls: sourcePulls,
|
|
};
|
|
}
|
|
|
|
export async function enrichPullRequests(token, repository, pulls, filters = {}) {
|
|
const normalizedFilters = normalizePullFilters(filters);
|
|
let enriched = pulls;
|
|
if (pulls.length) {
|
|
const signals = await pullRequestSignals(token, repository, pulls);
|
|
enriched = pulls.map((pull) => ({
|
|
...pull,
|
|
...signals.get(Number(pull.number)),
|
|
}));
|
|
}
|
|
if (normalizedFilters.artifacts !== "all") {
|
|
const artifactHeads = await artifactHeadsForPulls(
|
|
token,
|
|
repository,
|
|
enriched.map((pull) => pull.headSha),
|
|
);
|
|
enriched = enriched.map((pull) => ({
|
|
...pull,
|
|
hasArtifacts: Boolean(pull.headSha && artifactHeads.has(pull.headSha)),
|
|
}));
|
|
}
|
|
return enriched;
|
|
}
|
|
|
|
export function filterPullRequests(pulls, filters = {}) {
|
|
const normalizedFilters = normalizePullFilters(filters);
|
|
let filtered = pulls;
|
|
if (normalizedFilters.ci !== "all") {
|
|
filtered = filtered.filter((pull) => pull.ciStatus === normalizedFilters.ci);
|
|
}
|
|
if (normalizedFilters.artifacts !== "all") {
|
|
const expected = normalizedFilters.artifacts === "with";
|
|
filtered = filtered.filter((pull) => pull.hasArtifacts === expected);
|
|
}
|
|
return filtered;
|
|
}
|
|
|
|
export async function searchPullRequests(
|
|
token,
|
|
repository,
|
|
filter = "",
|
|
state = "open",
|
|
filters = {},
|
|
) {
|
|
const base = await searchPullRequestBase(
|
|
token,
|
|
repository,
|
|
filter,
|
|
state,
|
|
filters,
|
|
);
|
|
const enriched = await enrichPullRequests(
|
|
token,
|
|
repository,
|
|
base.pulls,
|
|
base.filters,
|
|
);
|
|
const derivedFilterActive =
|
|
base.filters.artifacts !== "all" || base.filters.ci !== "all";
|
|
return {
|
|
...base,
|
|
filtered: derivedFilterActive,
|
|
pulls: filterPullRequests(enriched, base.filters),
|
|
};
|
|
}
|
|
|
|
export async function getPullRequest(token, repository, pullNumber) {
|
|
const number = Number.parseInt(pullNumber, 10);
|
|
if (!Number.isSafeInteger(number) || number <= 0) {
|
|
throw new Error("Pull request number must be a positive integer.");
|
|
}
|
|
return mapPullRequest(
|
|
await githubRequest(token, `/repos/${repositoryPath(repository)}/pulls/${number}`),
|
|
);
|
|
}
|
|
|
|
async function mapLimit(items, limit, callback) {
|
|
const results = new Array(items.length);
|
|
let cursor = 0;
|
|
async function worker() {
|
|
while (cursor < items.length) {
|
|
const index = cursor++;
|
|
results[index] = await callback(items[index], index);
|
|
}
|
|
}
|
|
await Promise.all(
|
|
Array.from({ length: Math.min(limit, items.length) }, () => worker()),
|
|
);
|
|
return results;
|
|
}
|
|
|
|
async function listRunArtifacts(token, repository, runId) {
|
|
const artifacts = [];
|
|
for (let page = 1; page <= 10; page++) {
|
|
const payload = await githubRequest(
|
|
token,
|
|
`/repos/${repositoryPath(repository)}/actions/runs/${runId}/artifacts?per_page=100&page=${page}`,
|
|
);
|
|
const pageArtifacts = payload.artifacts ?? [];
|
|
artifacts.push(...pageArtifacts);
|
|
if (pageArtifacts.length < 100 || artifacts.length >= Number(payload.total_count ?? 0)) {
|
|
break;
|
|
}
|
|
}
|
|
return artifacts;
|
|
}
|
|
|
|
export async function listPullRequestArtifacts(token, repository, pullNumber) {
|
|
const pullRequest = await getPullRequest(token, repository, pullNumber);
|
|
if (!pullRequest.headSha) {
|
|
return {
|
|
pullRequest,
|
|
runs: [],
|
|
runCount: 0,
|
|
runsTruncated: false,
|
|
artifacts: [],
|
|
artifactCount: 0,
|
|
artifactsTruncated: false,
|
|
};
|
|
}
|
|
|
|
const query = new URLSearchParams({
|
|
head_sha: pullRequest.headSha,
|
|
per_page: String(MAX_PULL_REQUEST_RUNS),
|
|
});
|
|
const runsPayload = await githubRequest(
|
|
token,
|
|
`/repos/${repositoryPath(repository)}/actions/runs?${query}`,
|
|
);
|
|
const returnedRuns = (runsPayload.workflow_runs ?? [])
|
|
.sort((left, right) => new Date(right.created_at) - new Date(left.created_at));
|
|
const runCount = Math.max(
|
|
returnedRuns.length,
|
|
Number(runsPayload.total_count) || 0,
|
|
);
|
|
const runs = returnedRuns.slice(0, MAX_PULL_REQUEST_RUNS);
|
|
const runsTruncated = runs.length < runCount;
|
|
|
|
const artifactGroups = await mapLimit(runs, 5, async (run) => {
|
|
const runArtifacts = await listRunArtifacts(token, repository, run.id);
|
|
return runArtifacts.map((artifact) => ({
|
|
id: artifact.id,
|
|
name: artifact.name,
|
|
sizeInBytes: artifact.size_in_bytes,
|
|
expired: Boolean(artifact.expired),
|
|
createdAt: artifact.created_at,
|
|
updatedAt: artifact.updated_at,
|
|
expiresAt: artifact.expires_at,
|
|
run: {
|
|
id: run.id,
|
|
name: run.name,
|
|
number: run.run_number,
|
|
attempt: run.run_attempt,
|
|
event: run.event,
|
|
status: run.status,
|
|
conclusion: run.conclusion,
|
|
createdAt: run.created_at,
|
|
url: run.html_url,
|
|
},
|
|
}));
|
|
});
|
|
|
|
const allArtifacts = [...new Map(
|
|
artifactGroups.flat().map((artifact) => [String(artifact.id), artifact]),
|
|
).values()].sort((left, right) => new Date(right.createdAt) - new Date(left.createdAt));
|
|
const artifacts = allArtifacts.slice(0, MAX_PULL_REQUEST_ARTIFACTS);
|
|
|
|
return {
|
|
pullRequest,
|
|
runs: runs.map((run) => ({
|
|
id: run.id,
|
|
name: run.name,
|
|
number: run.run_number,
|
|
attempt: run.run_attempt,
|
|
event: run.event,
|
|
status: run.status,
|
|
conclusion: run.conclusion,
|
|
createdAt: run.created_at,
|
|
url: run.html_url,
|
|
})),
|
|
runCount,
|
|
runsTruncated,
|
|
artifacts,
|
|
artifactCount: allArtifacts.length,
|
|
artifactsTruncated:
|
|
runsTruncated || artifacts.length < allArtifacts.length,
|
|
};
|
|
}
|
|
|
|
export async function getArtifact(token, repository, artifactId) {
|
|
const id = Number.parseInt(artifactId, 10);
|
|
if (!Number.isSafeInteger(id) || id <= 0) {
|
|
throw new Error("Artifact id must be a positive integer.");
|
|
}
|
|
const artifact = await githubRequest(
|
|
token,
|
|
`/repos/${repositoryPath(repository)}/actions/artifacts/${id}`,
|
|
);
|
|
return {
|
|
id: artifact.id,
|
|
name: artifact.name,
|
|
sizeInBytes: artifact.size_in_bytes,
|
|
expired: Boolean(artifact.expired),
|
|
createdAt: artifact.created_at,
|
|
updatedAt: artifact.updated_at,
|
|
expiresAt: artifact.expires_at,
|
|
run: artifact.workflow_run
|
|
? {
|
|
id: artifact.workflow_run.id,
|
|
headBranch: artifact.workflow_run.head_branch,
|
|
headSha: artifact.workflow_run.head_sha,
|
|
}
|
|
: null,
|
|
};
|
|
}
|
|
|
|
export async function openArtifactDownload(
|
|
token,
|
|
repository,
|
|
artifactId,
|
|
{ signal } = {},
|
|
) {
|
|
if (!token) throw new GitHubApiError("No GitHub account is selected.", 401);
|
|
const id = Number.parseInt(artifactId, 10);
|
|
const response = await fetch(
|
|
`${GITHUB_API}/repos/${repositoryPath(repository)}/actions/artifacts/${id}/zip`,
|
|
{
|
|
headers: headersFor(token),
|
|
redirect: "follow",
|
|
signal,
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
const payload = await readPayload(response);
|
|
const message =
|
|
payload && typeof payload === "object" && payload.message
|
|
? payload.message
|
|
: `Artifact download failed (${response.status}).`;
|
|
throw new GitHubApiError(message, response.status, payload);
|
|
}
|
|
if (!response.body) {
|
|
throw new GitHubApiError("Artifact download returned an empty response.", response.status);
|
|
}
|
|
return response;
|
|
}
|