diff --git a/.github/workflows/e2e-cache.yml b/.github/workflows/e2e-cache.yml index 4675b869..a3c9a2ba 100644 --- a/.github/workflows/e2e-cache.yml +++ b/.github/workflows/e2e-cache.yml @@ -82,6 +82,8 @@ jobs: python-version: pypy-3.10-v7.x - os: ubuntu-22.04-arm python-version: pypy-3.11-v7.x + - os: ubuntu-22.04-arm + python-version: pypy-3.10-v7.x steps: - uses: actions/checkout@v4 - name: Setup Python diff --git a/__tests__/install-python.test.ts b/__tests__/install-python.test.ts index c3a6e7b4..51f9fa77 100644 --- a/__tests__/install-python.test.ts +++ b/__tests__/install-python.test.ts @@ -8,10 +8,29 @@ import * as tc from '@actions/tool-cache'; jest.mock('@actions/http-client'); jest.mock('@actions/tool-cache'); - -const mockManifest = [{version: '1.0.0'}]; +jest.mock('@actions/tool-cache', () => ({ + getManifestFromRepo: jest.fn() +})); +const mockManifest = [ + { + version: '1.0.0', + stable: true, + files: [ + { + filename: 'tool-v1.0.0-linux-x64.tar.gz', + platform: 'linux', + arch: 'x64', + download_url: 'https://example.com/tool-v1.0.0-linux-x64.tar.gz' + } + ] + } +]; describe('getManifest', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + it('should return manifest from repo', async () => { (tc.getManifestFromRepo as jest.Mock).mockResolvedValue(mockManifest); const manifest = await getManifest(); diff --git a/dist/setup/index.js b/dist/setup/index.js index 2cdcfaad..8dd57530 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -97461,15 +97461,36 @@ function findReleaseFromManifest(semanticVersionSpec, architecture, manifest) { }); } exports.findReleaseFromManifest = findReleaseFromManifest; +function isIToolRelease(obj) { + return (typeof obj === 'object' && + obj !== null && + typeof obj.version === 'string' && + typeof obj.stable === 'boolean' && + Array.isArray(obj.files) && + obj.files.every((file) => typeof file.filename === 'string' && + typeof file.platform === 'string' && + typeof file.arch === 'string' && + typeof file.download_url === 'string')); +} function getManifest() { return __awaiter(this, void 0, void 0, function* () { try { - return yield getManifestFromRepo(); + const repoManifest = yield getManifestFromRepo(); + if (Array.isArray(repoManifest) && + repoManifest.length && + repoManifest.every(isIToolRelease)) { + return repoManifest; + } + throw new Error('The repository manifest is invalid or does not include any valid tool release (IToolRelease) entries.'); } catch (err) { - core.debug('Fetching the manifest via the API failed.'); + core.debug('Failed to fetch the manifest from the repository API.'); if (err instanceof Error) { - core.debug(err.message); + core.debug(`Error message: ${err.message}`); + core.debug(`Error stack: ${err.stack}`); + } + else { + core.error('An unexpected error occurred while fetching the manifest.'); } } return yield getManifestFromURL(); @@ -97477,17 +97498,17 @@ function getManifest() { } exports.getManifest = getManifest; function getManifestFromRepo() { - core.debug(`Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`); + core.info(`Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`); return tc.getManifestFromRepo(MANIFEST_REPO_OWNER, MANIFEST_REPO_NAME, AUTH, MANIFEST_REPO_BRANCH); } exports.getManifestFromRepo = getManifestFromRepo; function getManifestFromURL() { return __awaiter(this, void 0, void 0, function* () { - core.debug('Falling back to fetching the manifest using raw URL.'); + core.info('Falling back to fetching the manifest using raw URL.'); const http = new httpm.HttpClient('tool-cache'); const response = yield http.getJson(exports.MANIFEST_URL); if (!response.result) { - throw new Error(`Unable to get manifest from ${exports.MANIFEST_URL}`); + throw new Error(`Unable to get manifest from ${exports.MANIFEST_URL}. HTTP status: ${response.statusCode}`); } return response.result; }); @@ -97518,6 +97539,9 @@ function installPython(workingDirectory) { } function installCpythonFromRelease(release) { return __awaiter(this, void 0, void 0, function* () { + if (!release.files || release.files.length === 0) { + throw new Error('No files found in the release to download.'); + } const downloadUrl = release.files[0].download_url; core.info(`Download from "${downloadUrl}"`); let pythonPath = ''; @@ -97537,15 +97561,22 @@ function installCpythonFromRelease(release) { } catch (err) { if (err instanceof tc.HTTPError) { - // Rate limit? - if (err.httpStatusCode === 403 || err.httpStatusCode === 429) { - core.info(`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`); + const statusCode = err.httpStatusCode; + if (statusCode === 403 || statusCode === 429) { + const rateLimitMessage = `HTTP ${statusCode} - Rate limit likely exceeded. This is typically due to too many requests or insufficient permissions.`; + core.info(rateLimitMessage); + if (err.stack) { + core.debug(err.stack); + } + throw new Error(rateLimitMessage); } else { - core.info(err.message); - } - if (err.stack) { - core.debug(err.stack); + const genericErrorMessage = `HTTP ${statusCode} - ${err.message}`; + core.error(genericErrorMessage); + if (err.stack) { + core.debug(err.stack); + } + throw new Error(genericErrorMessage); } } throw err; diff --git a/src/install-python.ts b/src/install-python.ts index d3421bf8..137dc9d9 100644 --- a/src/install-python.ts +++ b/src/install-python.ts @@ -5,6 +5,7 @@ import * as exec from '@actions/exec'; import * as httpm from '@actions/http-client'; import {ExecOptions} from '@actions/exec/lib/interfaces'; import {IS_WINDOWS, IS_LINUX, getDownloadFileName} from './utils'; +import {IToolRelease} from '@actions/tool-cache'; const TOKEN = core.getInput('token'); const AUTH = !TOKEN ? undefined : `token ${TOKEN}`; @@ -31,21 +32,49 @@ export async function findReleaseFromManifest( return foundRelease; } - +function isIToolRelease(obj: any): obj is IToolRelease { + return ( + typeof obj === 'object' && + obj !== null && + typeof obj.version === 'string' && + typeof obj.stable === 'boolean' && + Array.isArray(obj.files) && + obj.files.every( + (file: any) => + typeof file.filename === 'string' && + typeof file.platform === 'string' && + typeof file.arch === 'string' && + typeof file.download_url === 'string' + ) + ); +} export async function getManifest(): Promise { try { - return await getManifestFromRepo(); + const repoManifest = await getManifestFromRepo(); + if ( + Array.isArray(repoManifest) && + repoManifest.length && + repoManifest.every(isIToolRelease) + ) { + return repoManifest; + } + throw new Error( + 'The repository manifest is invalid or does not include any valid tool release (IToolRelease) entries.' + ); } catch (err) { - core.debug('Fetching the manifest via the API failed.'); + core.debug('Failed to fetch the manifest from the repository API.'); if (err instanceof Error) { - core.debug(err.message); + core.debug(`Error message: ${err.message}`); + core.debug(`Error stack: ${err.stack}`); + } else { + core.error('An unexpected error occurred while fetching the manifest.'); } } return await getManifestFromURL(); } export function getManifestFromRepo(): Promise { - core.debug( + core.info( `Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}` ); return tc.getManifestFromRepo( @@ -57,12 +86,14 @@ export function getManifestFromRepo(): Promise { } export async function getManifestFromURL(): Promise { - core.debug('Falling back to fetching the manifest using raw URL.'); + core.info('Falling back to fetching the manifest using raw URL.'); const http: httpm.HttpClient = new httpm.HttpClient('tool-cache'); const response = await http.getJson(MANIFEST_URL); if (!response.result) { - throw new Error(`Unable to get manifest from ${MANIFEST_URL}`); + throw new Error( + `Unable to get manifest from ${MANIFEST_URL}. HTTP status: ${response.statusCode}` + ); } return response.result; } @@ -93,6 +124,10 @@ async function installPython(workingDirectory: string) { } export async function installCpythonFromRelease(release: tc.IToolRelease) { + + if (!release.files || release.files.length === 0) { + throw new Error('No files found in the release to download.'); + } const downloadUrl = release.files[0].download_url; core.info(`Download from "${downloadUrl}"`);