remove internal API client

This commit is contained in:
Sergey Dolin 2023-07-05 16:46:04 +02:00
parent ef7b227095
commit 8f54fa9cb2
9 changed files with 2 additions and 495 deletions

2
dist/index.js vendored
View File

@ -1631,7 +1631,7 @@ class StateCacheStorage {
yield resetCacheWithOctokit(CACHE_KEY);
const fileSize = fs_1.default.statSync(filePath).size;
if (fileSize === 0) {
core.info(`the cache ${CACHE_KEY} will be removed`);
core.info(`the state will be removed`);
return;
}
yield cache.saveCache([path_1.default.dirname(filePath)], CACHE_KEY);

View File

@ -1,12 +0,0 @@
import * as cache from '@actions/cache';
import path from 'path';
export const downloadFileFromActionsCache = (
destFileName: string,
cacheKey: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
cacheVersion: string
): Promise<void> =>
cache.restoreCache([path.dirname(destFileName)], cacheKey, [
cacheKey
]) as Promise<void>;

View File

@ -1,43 +0,0 @@
import fs from 'fs';
import * as core from '@actions/core';
import * as cache from '@actions/cache';
import {getOctokit} from '@actions/github';
import {retry as octokitRetry} from '@octokit/plugin-retry';
import path from 'path';
const resetCacheWithOctokit = async (cacheKey: string): Promise<void> => {
const token = core.getInput('repo-token');
const client = getOctokit(token, undefined, octokitRetry);
// TODO: better way to get repository?
const repo = process.env['GITHUB_REPOSITORY'];
core.debug(`remove cache "${cacheKey}"`);
try {
// TODO: replace with client.rest.
await client.request(
`DELETE /repos/${repo}/actions/caches?key=${cacheKey}`
);
} catch (error) {
if (error.status) {
core.debug(`Cache ${cacheKey} does not exist`);
} else {
throw error;
}
}
};
export const uploadFileToActionsCache = async (
filePath: string,
cacheKey: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
cacheVersion: string
) => {
await resetCacheWithOctokit(cacheKey);
const fileSize = fs.statSync(filePath).size;
if (fileSize === 0) {
core.info(`the cache ${cacheKey} will be removed`);
return;
}
core.debug('content: ' + fs.readFileSync(filePath).toString());
cache.saveCache([path.dirname(filePath)], cacheKey);
};

View File

@ -1,65 +0,0 @@
import {createActionsCacheClient, getCacheApiUrl} from './http-client';
import {retryTypedResponse} from './retry';
import {isSuccessStatusCode} from './http-responses';
import {HttpClient} from '@actions/http-client';
import {downloadCacheHttpClient} from '@actions/cache/lib/internal/downloadUtils';
import * as core from '@actions/core';
interface ArtifactCacheEntry {
cacheKey?: string;
scope?: string;
cacheVersion?: string;
creationTime?: string;
archiveLocation?: string;
}
const getCacheArchiveUrl = async (
httpClient: HttpClient,
cacheKey: string,
cacheVersion: string
): Promise<string | null> => {
// TODO: should work with delete?
const resource = `cache?keys=${cacheKey}&version=${cacheVersion}`;
const response = await retryTypedResponse('getCacheEntry', async () =>
httpClient.getJson<ArtifactCacheEntry>(getCacheApiUrl(resource))
);
// Cache not found
if (response.statusCode === 204) {
core.debug(
`There's no cache with key ${cacheKey} & version=${cacheVersion}`
);
// List cache for primary key only if cache miss occurs
return null;
}
if (!isSuccessStatusCode(response.statusCode)) {
throw new Error(`Cache service responded with ${response.statusCode}`);
}
const cacheResult = response.result;
core.debug(`getCacheEntry response is:\n${JSON.stringify(cacheResult)}`);
const cacheDownloadUrl = cacheResult?.archiveLocation;
if (!cacheDownloadUrl) {
// Cache archiveLocation not found. This should never happen, and hence bail out.
throw new Error('Cache not found.');
}
return cacheDownloadUrl;
};
export const downloadFileFromActionsCache = async (
destFileName: string,
cacheKey: string,
cacheVersion: string
) => {
const httpClient = createActionsCacheClient();
const archiveUrl = await getCacheArchiveUrl(
httpClient,
cacheKey,
cacheVersion
);
if (!archiveUrl) {
return undefined;
}
await downloadCacheHttpClient(archiveUrl, destFileName);
};

View File

@ -1,44 +0,0 @@
import {HttpClient} from '@actions/http-client';
import {BearerCredentialHandler} from '@actions/http-client/lib/auth';
import {RequestOptions} from '@actions/http-client/lib/interfaces';
import * as core from '@actions/core';
const createAcceptHeader = (type: string, apiVersion: string): string =>
`${type};api-version=${apiVersion}`;
const getRequestOptions = (): RequestOptions => ({
headers: {
Accept: createAcceptHeader('application/json', '6.0-preview.1')
}
});
export const createActionsCacheClient = (): HttpClient => {
const token = process.env['ACTIONS_RUNTIME_TOKEN'] || '';
const bearerCredentialHandler = new BearerCredentialHandler(token);
return new HttpClient(
'actions/cache',
[bearerCredentialHandler],
getRequestOptions()
);
};
export const getGitHubActionsApiUrl = (resource: string): string => {
const baseUrl: string = process.env['GITHUB_API_URL'] || '';
if (!baseUrl) {
throw new Error('GitHub API Url not found, unable to restore cache.');
}
const repo = process.env['GITHUB_REPOSITORY'];
const url = `${baseUrl}/repos/${repo}/actions/${resource}`;
core.debug(`Resource Url: ${url}`);
return url;
};
export const getCacheApiUrl = (resource: string): string => {
const baseUrl: string = process.env['ACTIONS_CACHE_URL'] || '';
if (!baseUrl) {
throw new Error('Cache Service Url not found, unable to restore cache.');
}
const url = `${baseUrl}_apis/artifactcache/${resource}`;
core.debug(`Resource Url: ${url}`);
return url;
};

View File

@ -1,19 +0,0 @@
import {TypedResponse} from '@actions/http-client/lib/interfaces';
import {HttpClientError} from '@actions/http-client';
export const isSuccessStatusCode = (statusCode?: number): boolean => {
if (!statusCode) {
return false;
}
return statusCode >= 200 && statusCode < 300;
};
export function isServerErrorStatusCode(statusCode?: number): boolean {
if (!statusCode) {
return true;
}
return statusCode >= 500;
}
export interface TypedResponseWithError<T> extends TypedResponse<T> {
error?: HttpClientError;
}

View File

@ -1,127 +0,0 @@
import {
HttpClientError,
HttpClientResponse,
HttpCodes
} from '@actions/http-client';
import {
isServerErrorStatusCode,
TypedResponseWithError
} from './http-responses';
import * as core from '@actions/core';
const isRetryableStatusCode = (statusCode?: number): boolean => {
if (!statusCode) {
return false;
}
const retryableStatusCodes = [
HttpCodes.BadGateway,
HttpCodes.ServiceUnavailable,
HttpCodes.GatewayTimeout
];
return retryableStatusCodes.includes(statusCode);
};
const sleep = (milliseconds: number): Promise<void> =>
new Promise(resolve => setTimeout(resolve, milliseconds));
// The default number of retry attempts.
const DefaultRetryAttempts = 2;
// The default delay in milliseconds between retry attempts.
const DefaultRetryDelay = 5000;
const retry = async <T>(
name: string,
method: () => Promise<T>,
getStatusCode: (arg0: T) => number | undefined,
maxAttempts = DefaultRetryAttempts,
delay = DefaultRetryDelay,
onError: ((arg0: Error) => T | undefined) | undefined = undefined
): Promise<T> => {
let errorMessage = '';
let attempt = 1;
while (attempt <= maxAttempts) {
let response: T | undefined = undefined;
let statusCode: number | undefined = undefined;
let isRetryable = false;
try {
response = await method();
} catch (error) {
if (onError) {
response = onError(error);
}
isRetryable = true;
errorMessage = error.message;
}
if (response) {
statusCode = getStatusCode(response);
if (!isServerErrorStatusCode(statusCode)) {
return response;
}
}
if (statusCode) {
isRetryable = isRetryableStatusCode(statusCode);
errorMessage = `Cache service responded with ${statusCode}`;
}
core.debug(
`${name} - Attempt ${attempt} of ${maxAttempts} failed with error: ${errorMessage}`
);
if (!isRetryable) {
core.debug(`${name} - Error is not retryable`);
break;
}
await sleep(delay);
attempt++;
}
throw Error(`${name} failed: ${errorMessage}`);
};
export const retryHttpClientResponse = async (
name: string,
method: () => Promise<HttpClientResponse>,
maxAttempts = DefaultRetryAttempts,
delay = DefaultRetryDelay
): Promise<HttpClientResponse> => {
return await retry(
name,
method,
(response: HttpClientResponse) => response.message.statusCode,
maxAttempts,
delay
);
};
export const retryTypedResponse = <T>(
name: string,
method: () => Promise<TypedResponseWithError<T>>,
maxAttempts = DefaultRetryAttempts,
delay = DefaultRetryDelay
): Promise<TypedResponseWithError<T>> =>
retry(
name,
method,
(response: TypedResponseWithError<T>) => response.statusCode,
maxAttempts,
delay,
// If the error object contains the statusCode property, extract it and return
// an TypedResponse<T> so it can be processed by the retry logic.
(error: Error) => {
if (error instanceof HttpClientError) {
return {
statusCode: error.statusCode,
result: null,
headers: {},
error
};
} else {
return undefined;
}
}
);

View File

@ -1,183 +0,0 @@
import * as core from '@actions/core';
import fs from 'fs';
import {HttpClient} from '@actions/http-client';
import {TypedResponse} from '@actions/http-client/lib/interfaces';
import {ReserveCacheError, ValidationError} from '@actions/cache';
import {isSuccessStatusCode} from './http-responses';
import {retryHttpClientResponse, retryTypedResponse} from './retry';
import {getOctokit} from '@actions/github';
import {retry as octokitRetry} from '@octokit/plugin-retry';
import {createActionsCacheClient, getCacheApiUrl} from './http-client';
const uploadChunk = async (httpClient: HttpClient): Promise<void> => {};
const uploadFile = async (
httpClient: HttpClient,
cacheId: number,
filePath: string,
fileSize: number
): Promise<void> => {
if (fileSize <= 0) return;
const start = 0;
const end = fileSize - 1;
const contentRange = `bytes ${start}-${end}/*`;
core.debug(
`Uploading chunk of size ${
end - start + 1
} bytes at offset ${start} with content range: ${contentRange}`
);
const additionalHeaders = {
'Content-Type': 'application/octet-stream',
'Content-Range': contentRange
};
const resourceUrl = getCacheApiUrl(`caches/${cacheId.toString()}`);
const fd = fs.openSync(filePath, 'r');
const openStream = () =>
fs
.createReadStream(filePath, {
fd,
start,
end,
autoClose: false
})
.on('error', error => {
throw new Error(
`Cache upload failed because file read failed with ${error.message}`
);
});
try {
const uploadChunkResponse = await retryHttpClientResponse(
`uploadChunk (start: ${start}, end: ${end})`,
async () =>
httpClient.sendStream(
'PATCH',
resourceUrl,
openStream(),
additionalHeaders
)
);
if (!isSuccessStatusCode(uploadChunkResponse.message.statusCode)) {
throw new Error(
`Cache service responded with ${uploadChunkResponse.message.statusCode} during upload chunk.`
);
}
} finally {
fs.closeSync(fd);
}
};
const resetCacheWithOctokit = async (cacheKey: string): Promise<void> => {
const token = core.getInput('repo-token');
const client = getOctokit(token, undefined, octokitRetry);
// TODO: better way to get repository?
const repo = process.env['GITHUB_REPOSITORY'];
core.debug(`remove cache "${cacheKey}"`);
try {
// TODO: replace with client.rest.
await client.request(
`DELETE /repos/${repo}/actions/caches?key=${cacheKey}`
);
} catch (error) {
if (error.status) {
core.debug(`Cache ${cacheKey} does not exist`);
} else {
throw error;
}
}
};
const reserveCache = async (
httpClient: HttpClient,
fileSize: number,
cacheKey: string,
cacheVersion: string
): Promise<number> => {
const reserveCacheRequest = {
key: cacheKey,
version: cacheVersion,
cacheSize: fileSize
};
const response = await retryTypedResponse('reserveCache', async () =>
httpClient.postJson<{cacheId: number}>(
getCacheApiUrl('caches'),
reserveCacheRequest
)
);
// handle 400 in the special way
if (response?.statusCode === 400)
throw new Error(
response?.error?.message ??
`Cache size of ~${Math.round(
fileSize / (1024 * 1024)
)} MB (${fileSize} B) is over the data cap limit, not saving cache.`
);
const cacheId = response?.result?.cacheId;
if (cacheId === undefined)
throw new ReserveCacheError(
`Unable to reserve cache with key ${cacheKey}, another job may be creating this cache. More details: ${response?.error?.message}`
);
return cacheId;
};
const commitCache = async (
httpClient: HttpClient,
cacheId: number,
filesize: number
): Promise<void> => {
const response = (await retryTypedResponse('commitCache', async () =>
httpClient.postJson<null>(getCacheApiUrl(`caches/${cacheId.toString()}`), {
size: filesize
})
)) as TypedResponse<null>;
if (!isSuccessStatusCode(response.statusCode)) {
throw new Error(
`Cache service responded with ${response.statusCode} during commit cache.`
);
}
};
export const uploadFileToActionsCache = async (
filePath: string,
cacheKey: string,
cacheVersion: string
) => {
try {
await resetCacheWithOctokit(cacheKey);
const fileSize = fs.statSync(filePath).size;
if (fileSize === 0) {
core.info(`the cache ${cacheKey} will be removed`);
return;
}
const httpClient = createActionsCacheClient();
const cacheId = await reserveCache(
httpClient,
fileSize,
cacheKey,
cacheVersion
);
await uploadFile(httpClient, cacheId, filePath, fileSize);
await commitCache(httpClient, cacheId, fileSize);
} catch (error) {
const typedError = error as Error;
if (typedError.name === ValidationError.name) {
throw error;
}
if (typedError.name === ReserveCacheError.name) {
core.info(`Failed to save: ${typedError.message}`);
return;
}
core.warning(`Failed to save: ${typedError.message}`);
}
};

View File

@ -87,7 +87,7 @@ export class StateCacheStorage implements IStateStorage {
const fileSize = fs.statSync(filePath).size;
if (fileSize === 0) {
core.info(`the cache ${CACHE_KEY} will be removed`);
core.info(`the state will be removed`);
return;
}