Migrate to ESM and upgrade dependencies (#1330)

* Migrate to ESM and upgrade dependencies

* Add ESM migration note to README for V7

* Remove unnecessary devDependencies: ts-node, @types/jest

* npm audit fix

* Upgrade @types/node to version 26.0.0

* Clarify ESM migration details in README for V7

* Update README and dependencies

* Fix lint issue
This commit is contained in:
Priya Gupta
2026-07-15 11:40:44 -05:00
committed by GitHub
parent 54baeea5b3
commit f8cf4291c8
66 changed files with 114064 additions and 103976 deletions
-6
View File
@@ -1,6 +0,0 @@
# Ignore list
/*
# Do not ignore these folders:
!__tests__/
!src/
-51
View File
@@ -1,51 +0,0 @@
// This is a reusable configuration file copied from https://github.com/actions/reusable-workflows/tree/main/reusable-configurations. Please don't make changes to this file as it's the subject of an automatic update.
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:eslint-plugin-jest/recommended',
'eslint-config-prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'eslint-plugin-node', 'eslint-plugin-jest'],
rules: {
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description'
}
],
'no-console': 'error',
'yoda': 'error',
'prefer-const': [
'error',
{
destructuring: 'all'
}
],
'no-control-regex': 'off',
'no-constant-condition': ['error', {checkLoops: false}],
'node/no-extraneous-import': 'error'
},
overrides: [
{
files: ['**/*{test,spec}.ts'],
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'jest/no-standalone-expect': 'off',
'jest/no-conditional-expect': 'off',
'no-console': 'off',
}
}
],
env: {
node: true,
es6: true,
'jest/globals': true
}
};
+3 -1
View File
@@ -14,4 +14,6 @@ allowed:
reviewed:
npm:
- "@actions/http-client"
- "@actions/http-client"
- "balanced-match"
- "brace-expansion"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-11
View File
@@ -1,11 +0,0 @@
// This is a reusable configuration file copied from https://github.com/actions/reusable-workflows/tree/main/reusable-configurations. Please don't make changes to this file as it's the subject of an automatic update.
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
trailingComma: 'none',
bracketSpacing: false,
arrowParens: 'avoid'
};
+10
View File
@@ -0,0 +1,10 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": false,
"arrowParens": "avoid"
}
+4
View File
@@ -11,6 +11,10 @@ This action provides the following functionality for GitHub Actions users:
- Optionally caching dependencies for pip, pipenv and poetry
- Registering problem matchers for error output
## What's new in V7
- Migrated action internals to ESM for compatibility with latest `@actions/*` packages. No changes to action inputs, outputs, or behavior.
## Breaking changes in V6
- Upgraded action from node20 to node24
+106 -49
View File
@@ -1,10 +1,62 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {fileURLToPath} from 'url';
import * as path from 'path';
import * as core from '@actions/core';
import * as cache from '@actions/cache';
import * as exec from '@actions/exec';
import * as io from '@actions/io';
import {getCacheDistributor} from '../src/cache-distributions/cache-factory';
import {State} from '../src/cache-distributions/cache-distributor';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Mock @actions modules before importing anything that depends on them
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
jest.unstable_mockModule('@actions/cache', () => ({
saveCache: jest.fn(),
restoreCache: jest.fn(),
isFeatureAvailable: jest.fn()
}));
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
jest.unstable_mockModule('@actions/io', () => ({
which: jest.fn(),
mkdirP: jest.fn(),
rmRF: jest.fn(),
mv: jest.fn(),
cp: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const cache = await import('@actions/cache');
const exec = await import('@actions/exec');
const io = await import('@actions/io');
const {getCacheDistributor} =
await import('../src/cache-distributions/cache-factory.js');
const {State} = await import('../src/cache-distributions/cache-distributor.js');
describe('restore-cache', () => {
const pipFileLockHash =
@@ -24,43 +76,38 @@ virtualenvs.in-project = true
virtualenvs.path = "{cache-dir}/virtualenvs" # /Users/patrick/Library/Caches/pypoetry/virtualenvs
`;
// core spy
let infoSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let saveStateSpy: jest.SpyInstance;
let getStateSpy: jest.SpyInstance;
let setOutputSpy: jest.SpyInstance;
// cache spy
let restoreCacheSpy: jest.SpyInstance;
// exec spy
let getExecOutputSpy: jest.SpyInstance;
// io spy
let whichSpy: jest.SpyInstance;
let infoSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let saveStateSpy: jest.Mock;
let getStateSpy: jest.Mock;
let setOutputSpy: jest.Mock;
let restoreCacheSpy: jest.Mock;
let getExecOutputSpy: jest.Mock;
let whichSpy: jest.Mock;
beforeEach(() => {
process.env['RUNNER_OS'] = process.env['RUNNER_OS'] ?? 'linux';
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(input => undefined);
infoSpy = core.info as jest.Mock;
infoSpy.mockImplementation(() => undefined);
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(input => undefined);
warningSpy = core.warning as jest.Mock;
warningSpy.mockImplementation(() => undefined);
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(input => undefined);
debugSpy = core.debug as jest.Mock;
debugSpy.mockImplementation(() => undefined);
saveStateSpy = jest.spyOn(core, 'saveState');
saveStateSpy.mockImplementation(input => undefined);
saveStateSpy = core.saveState as jest.Mock;
saveStateSpy.mockImplementation(() => undefined);
getStateSpy = jest.spyOn(core, 'getState');
getStateSpy.mockImplementation(input => undefined);
getStateSpy = core.getState as jest.Mock;
getStateSpy.mockImplementation(() => undefined);
getExecOutputSpy = jest.spyOn(exec, 'getExecOutput');
getExecOutputSpy.mockImplementation((input: string) => {
getExecOutputSpy = exec.getExecOutput as jest.Mock;
(
getExecOutputSpy as jest.Mock<typeof exec.getExecOutput>
).mockImplementation(async (input: string) => {
if (input.includes('pip')) {
return {stdout: 'pip', stderr: '', exitCode: 0};
}
@@ -74,17 +121,19 @@ virtualenvs.path = "{cache-dir}/virtualenvs" # /Users/patrick/Library/Caches/py
return {stdout: '', stderr: 'Error occured', exitCode: 2};
});
setOutputSpy = jest.spyOn(core, 'setOutput');
setOutputSpy.mockImplementation(input => undefined);
setOutputSpy = core.setOutput as jest.Mock;
setOutputSpy.mockImplementation(() => undefined);
restoreCacheSpy = jest.spyOn(cache, 'restoreCache');
restoreCacheSpy.mockImplementation(
(cachePaths: string[], primaryKey: string, restoreKey?: string) => {
return primaryKey;
restoreCacheSpy = cache.restoreCache as jest.Mock;
(
restoreCacheSpy as jest.Mock<typeof cache.restoreCache>
).mockImplementation(
(cachePaths: string[], primaryKey: string, restoreKey?: string[]) => {
return Promise.resolve(primaryKey);
}
);
whichSpy = jest.spyOn(io, 'which');
whichSpy = io.which as jest.Mock;
whichSpy.mockImplementation(() => '/path/to/python');
});
@@ -163,9 +212,13 @@ virtualenvs.path = "{cache-dir}/virtualenvs" # /Users/patrick/Library/Caches/py
fileHash,
cachePaths
) => {
restoreCacheSpy.mockImplementation(
(cachePaths: string[], primaryKey: string, restoreKey?: string) => {
return primaryKey.includes(fileHash) ? primaryKey : '';
(
restoreCacheSpy as jest.Mock<typeof cache.restoreCache>
).mockImplementation(
(cachePaths: string[], primaryKey: string, restoreKey?: string[]) => {
return Promise.resolve(
primaryKey.includes(fileHash) ? primaryKey : ''
);
}
);
@@ -184,8 +237,8 @@ virtualenvs.path = "{cache-dir}/virtualenvs" # /Users/patrick/Library/Caches/py
);
}
const restoredKeys = restoreCacheSpy.mock.results.map(
result => result.value
const restoredKeys = await Promise.all(
restoreCacheSpy.mock.results.map(result => result.value)
);
restoredKeys.forEach(restoredKey => {
@@ -284,9 +337,13 @@ virtualenvs.path = "{cache-dir}/virtualenvs" # /Users/patrick/Library/Caches/py
])(
'restored dependencies for %s by primaryKey',
async (packageManager, pythonVersion, dependencyFile, fileHash) => {
restoreCacheSpy.mockImplementation(
(cachePaths: string[], primaryKey: string, restoreKey?: string) => {
return primaryKey !== fileHash && restoreKey ? pipFileLockHash : '';
(
restoreCacheSpy as jest.Mock<typeof cache.restoreCache>
).mockImplementation(
(cachePaths: string[], primaryKey: string, restoreKey?: string[]) => {
return Promise.resolve(
primaryKey !== fileHash && restoreKey ? pipFileLockHash : ''
);
}
);
const cacheDistributor = getCacheDistributor(
+143 -79
View File
@@ -1,8 +1,61 @@
import * as core from '@actions/core';
import * as cache from '@actions/cache';
import * as exec from '@actions/exec';
import {run} from '../src/cache-save';
import {State} from '../src/cache-distributions/cache-distributor';
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {fileURLToPath} from 'url';
import path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Mock @actions modules before importing anything that depends on them
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
class MockValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}
jest.unstable_mockModule('@actions/cache', () => ({
saveCache: jest.fn(),
restoreCache: jest.fn(),
isFeatureAvailable: jest.fn(),
ValidationError: MockValidationError,
ReserveCacheError: MockValidationError
}));
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const cache = await import('@actions/cache');
const exec = await import('@actions/exec');
const {run} = await import('../src/cache-save.js');
const {State} = await import('../src/cache-distributions/cache-distributor.js');
describe('run', () => {
const pipFileLockHash =
@@ -14,53 +67,54 @@ describe('run', () => {
const poetryLockHash =
'571bf984f8d210e6a97f854e479fdd4a2b5af67b5fdac109ec337a0ea16e7836';
// core spy
let infoSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let saveStateSpy: jest.SpyInstance;
let getStateSpy: jest.SpyInstance;
let getInputSpy: jest.SpyInstance;
let setFailedSpy: jest.SpyInstance;
// cache spy
let saveCacheSpy: jest.SpyInstance;
// exec spy
let getExecOutputSpy: jest.SpyInstance;
let infoSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let saveStateSpy: jest.Mock;
let getStateSpy: jest.Mock;
let getInputSpy: jest.Mock;
let setFailedSpy: jest.Mock;
let saveCacheSpy: jest.Mock;
let getExecOutputSpy: jest.Mock;
let inputs = {} as any;
beforeEach(() => {
process.env['RUNNER_OS'] = process.env['RUNNER_OS'] ?? 'linux';
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(input => undefined);
infoSpy = core.info as jest.Mock;
infoSpy.mockImplementation(() => undefined);
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(input => undefined);
warningSpy = core.warning as jest.Mock;
warningSpy.mockImplementation(() => undefined);
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(input => undefined);
debugSpy = core.debug as jest.Mock;
debugSpy.mockImplementation(() => undefined);
saveStateSpy = jest.spyOn(core, 'saveState');
saveStateSpy.mockImplementation(input => undefined);
saveStateSpy = core.saveState as jest.Mock;
saveStateSpy.mockImplementation(() => undefined);
getStateSpy = jest.spyOn(core, 'getState');
getStateSpy.mockImplementation(input => {
if (input === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
getStateSpy = core.getState as jest.Mock;
(getStateSpy as jest.Mock<typeof core.getState>).mockImplementation(
(input: string) => {
if (input === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
}
return requirementsHash;
}
return requirementsHash;
});
);
setFailedSpy = jest.spyOn(core, 'setFailed');
setFailedSpy = core.setFailed as jest.Mock;
getInputSpy = jest.spyOn(core, 'getInput');
getInputSpy.mockImplementation(input => inputs[input]);
getInputSpy = core.getInput as jest.Mock;
(getInputSpy as jest.Mock<typeof core.getInput>).mockImplementation(
(input: string) => inputs[input]
);
getExecOutputSpy = jest.spyOn(exec, 'getExecOutput');
getExecOutputSpy.mockImplementation((input: string) => {
getExecOutputSpy = exec.getExecOutput as jest.Mock;
(
getExecOutputSpy as jest.Mock<typeof exec.getExecOutput>
).mockImplementation(async (input: string) => {
if (input.includes('pip')) {
return {stdout: 'pip', stderr: '', exitCode: 0};
}
@@ -68,7 +122,7 @@ describe('run', () => {
return {stdout: '', stderr: 'Error occured', exitCode: 2};
});
saveCacheSpy = jest.spyOn(cache, 'saveCache');
saveCacheSpy = cache.saveCache as jest.Mock;
saveCacheSpy.mockImplementation(() => undefined);
});
@@ -124,15 +178,17 @@ describe('run', () => {
it('saves cache from pip', async () => {
inputs['cache'] = 'pip';
inputs['python-version'] = '3.10.0';
getStateSpy.mockImplementation((name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return requirementsHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return pipFileLockHash;
(getStateSpy as jest.Mock<typeof core.getState>).mockImplementation(
(name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return requirementsHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return pipFileLockHash;
}
}
});
);
await run();
@@ -151,15 +207,17 @@ describe('run', () => {
it('saves cache from pipenv', async () => {
inputs['cache'] = 'pipenv';
inputs['python-version'] = '3.10.0';
getStateSpy.mockImplementation((name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return pipFileLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
(getStateSpy as jest.Mock<typeof core.getState>).mockImplementation(
(name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return pipFileLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
}
}
});
);
await run();
@@ -178,15 +236,17 @@ describe('run', () => {
it('saves cache from poetry', async () => {
inputs['cache'] = 'poetry';
inputs['python-version'] = '3.10.0';
getStateSpy.mockImplementation((name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return poetryLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
(getStateSpy as jest.Mock<typeof core.getState>).mockImplementation(
(name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return poetryLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
}
}
});
);
await run();
@@ -205,15 +265,17 @@ describe('run', () => {
it('saves with -1 cacheId , should not fail workflow', async () => {
inputs['cache'] = 'poetry';
inputs['python-version'] = '3.10.0';
getStateSpy.mockImplementation((name: string) => {
if (name === State.STATE_CACHE_PRIMARY_KEY) {
return poetryLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
(getStateSpy as jest.Mock<typeof core.getState>).mockImplementation(
(name: string) => {
if (name === State.STATE_CACHE_PRIMARY_KEY) {
return poetryLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
}
}
});
);
saveCacheSpy.mockImplementation(() => {
return -1;
@@ -234,15 +296,17 @@ describe('run', () => {
it('saves with error from toolkit, should not fail the workflow', async () => {
inputs['cache'] = 'npm';
inputs['python-version'] = '3.10.0';
getStateSpy.mockImplementation((name: string) => {
if (name === State.STATE_CACHE_PRIMARY_KEY) {
return poetryLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
(getStateSpy as jest.Mock<typeof core.getState>).mockImplementation(
(name: string) => {
if (name === State.STATE_CACHE_PRIMARY_KEY) {
return poetryLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
}
}
});
);
saveCacheSpy.mockImplementation(() => {
throw new cache.ValidationError('Validation failed');
+141 -84
View File
@@ -1,18 +1,70 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {fileURLToPath} from 'url';
import fs from 'fs';
import {HttpClient} from '@actions/http-client';
import * as ifm from '@actions/http-client/lib/interfaces';
import * as tc from '@actions/tool-cache';
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import * as path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
import * as semver from 'semver';
import * as finder from '../src/find-graalpy';
import {IGraalPyManifestRelease} from '../src/utils';
// Mock @actions modules
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
import manifestData from './data/graalpy.json';
jest.unstable_mockModule('@actions/tool-cache', () => ({
find: jest.fn(),
findAllVersions: jest.fn(),
downloadTool: jest.fn(),
extractZip: jest.fn(),
extractTar: jest.fn(),
extract7z: jest.fn(),
extractXar: jest.fn(),
cacheDir: jest.fn(),
cacheFile: jest.fn(),
getManifestFromRepo: jest.fn(),
findFromManifest: jest.fn(),
evaluateVersions: jest.fn()
}));
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const exec = await import('@actions/exec');
const finder = await import('../src/find-graalpy.js');
const utils = await import('../src/utils.js');
// Non-mocked imports
import {HttpClient} from '@actions/http-client';
import type * as ifm from '@actions/http-client/lib/interfaces';
import type {IGraalPyManifestRelease} from '../src/utils.js';
import manifestData from './data/graalpy.json' with {type: 'json'};
const architecture = 'x64';
@@ -41,39 +93,41 @@ describe('parseGraalPyVersion', () => {
describe('findGraalPyToolCache', () => {
const actualGraalPyVersion = '23.0.0';
const graalpyPath = path.join('GraalPy', actualGraalPyVersion, architecture);
let tcFind: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let addPathSpy: jest.SpyInstance;
let exportVariableSpy: jest.SpyInstance;
let setOutputSpy: jest.SpyInstance;
let tcFind: jest.Mock;
let infoSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let addPathSpy: jest.Mock;
let exportVariableSpy: jest.Mock;
let setOutputSpy: jest.Mock;
beforeEach(() => {
tcFind = jest.spyOn(tc, 'find');
tcFind.mockImplementation((toolname: string, pythonVersion: string) => {
const semverVersion = new semver.Range(pythonVersion);
return semver.satisfies(actualGraalPyVersion, semverVersion)
? graalpyPath
: '';
});
tcFind = tc.find as jest.Mock;
(tcFind as jest.Mock<typeof tc.find>).mockImplementation(
(toolname: string, pythonVersion: string) => {
const semverVersion = new semver.Range(pythonVersion);
return semver.satisfies(actualGraalPyVersion, semverVersion)
? graalpyPath
: '';
}
);
infoSpy = jest.spyOn(core, 'info');
infoSpy = core.info as jest.Mock;
infoSpy.mockImplementation(() => null);
warningSpy = jest.spyOn(core, 'warning');
warningSpy = core.warning as jest.Mock;
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy = core.debug as jest.Mock;
debugSpy.mockImplementation(() => null);
addPathSpy = jest.spyOn(core, 'addPath');
addPathSpy = core.addPath as jest.Mock;
addPathSpy.mockImplementation(() => null);
exportVariableSpy = jest.spyOn(core, 'exportVariable');
exportVariableSpy = core.exportVariable as jest.Mock;
exportVariableSpy.mockImplementation(() => null);
setOutputSpy = jest.spyOn(core, 'setOutput');
setOutputSpy = core.setOutput as jest.Mock;
setOutputSpy.mockImplementation(() => null);
});
@@ -106,73 +160,74 @@ describe('findGraalPyToolCache', () => {
});
describe('findGraalPyVersion', () => {
let getBooleanInputSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let addPathSpy: jest.SpyInstance;
let exportVariableSpy: jest.SpyInstance;
let setOutputSpy: jest.SpyInstance;
let tcFind: jest.SpyInstance;
let spyExtractZip: jest.SpyInstance;
let spyExtractTar: jest.SpyInstance;
let spyHttpClient: jest.SpyInstance;
let spyExistsSync: jest.SpyInstance;
let spyExec: jest.SpyInstance;
let spySymlinkSync: jest.SpyInstance;
let spyDownloadTool: jest.SpyInstance;
let spyFsReadDir: jest.SpyInstance;
let spyCacheDir: jest.SpyInstance;
let spyChmodSync: jest.SpyInstance;
let spyCoreAddPath: jest.SpyInstance;
let spyCoreExportVariable: jest.SpyInstance;
let getBooleanInputSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let infoSpy: jest.Mock;
let addPathSpy: jest.Mock;
let exportVariableSpy: jest.Mock;
let setOutputSpy: jest.Mock;
let tcFind: jest.Mock;
let spyExtractZip: jest.Mock;
let spyExtractTar: jest.Mock;
let spyHttpClient: jest.SpiedFunction<typeof HttpClient.prototype.getJson>;
let spyExistsSync: jest.SpiedFunction<typeof fs.existsSync>;
let spyExec: jest.Mock;
let spySymlinkSync: jest.SpiedFunction<typeof fs.symlinkSync>;
let spyDownloadTool: jest.Mock;
let spyFsReadDir: jest.SpiedFunction<typeof fs.readdirSync>;
let spyCacheDir: jest.Mock;
let spyChmodSync: jest.SpiedFunction<typeof fs.chmodSync>;
let spyCoreAddPath: jest.Mock;
let spyCoreExportVariable: jest.Mock;
const env = process.env;
beforeEach(() => {
getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
getBooleanInputSpy = core.getBooleanInput as jest.Mock;
getBooleanInputSpy.mockImplementation(() => false);
infoSpy = jest.spyOn(core, 'info');
infoSpy = core.info as jest.Mock;
infoSpy.mockImplementation(() => {});
warningSpy = jest.spyOn(core, 'warning');
warningSpy = core.warning as jest.Mock;
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy = core.debug as jest.Mock;
debugSpy.mockImplementation(() => null);
addPathSpy = jest.spyOn(core, 'addPath');
addPathSpy = core.addPath as jest.Mock;
addPathSpy.mockImplementation(() => null);
exportVariableSpy = jest.spyOn(core, 'exportVariable');
exportVariableSpy = core.exportVariable as jest.Mock;
exportVariableSpy.mockImplementation(() => null);
setOutputSpy = jest.spyOn(core, 'setOutput');
setOutputSpy = core.setOutput as jest.Mock;
setOutputSpy.mockImplementation(() => null);
jest.resetModules();
process.env = {...env};
tcFind = jest.spyOn(tc, 'find');
tcFind.mockImplementation((tool: string, version: string) => {
const semverRange = new semver.Range(version);
let graalpyPath = '';
if (semver.satisfies('23.0.0', semverRange)) {
graalpyPath = path.join(toolDir, 'GraalPy', '23.0.0', architecture);
tcFind = tc.find as jest.Mock;
(tcFind as jest.Mock<typeof tc.find>).mockImplementation(
(tool: string, version: string) => {
const semverRange = new semver.Range(version);
let graalpyPath = '';
if (semver.satisfies('23.0.0', semverRange)) {
graalpyPath = path.join(toolDir, 'GraalPy', '23.0.0', architecture);
}
return graalpyPath;
}
return graalpyPath;
});
);
spyDownloadTool = jest.spyOn(tc, 'downloadTool');
spyDownloadTool = tc.downloadTool as jest.Mock;
spyDownloadTool.mockImplementation(() => path.join(tempDir, 'GraalPy'));
spyExtractZip = jest.spyOn(tc, 'extractZip');
spyExtractZip = tc.extractZip as jest.Mock;
spyExtractZip.mockImplementation(() => tempDir);
spyExtractTar = jest.spyOn(tc, 'extractTar');
spyExtractTar = tc.extractTar as jest.Mock;
spyExtractTar.mockImplementation(() => tempDir);
spyFsReadDir = jest.spyOn(fs, 'readdirSync');
spyFsReadDir.mockImplementation((directory: string) => ['GraalPyTest']);
spyFsReadDir.mockImplementation(() => ['GraalPyTest'] as any);
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient.mockImplementation(
@@ -186,7 +241,7 @@ describe('findGraalPyVersion', () => {
}
);
spyExec = jest.spyOn(exec, 'exec');
spyExec = exec.exec as jest.Mock;
spyExec.mockImplementation(() => undefined);
spySymlinkSync = jest.spyOn(fs, 'symlinkSync');
@@ -195,9 +250,9 @@ describe('findGraalPyVersion', () => {
spyExistsSync = jest.spyOn(fs, 'existsSync');
spyExistsSync.mockReturnValue(true);
spyCoreAddPath = jest.spyOn(core, 'addPath');
spyCoreAddPath = core.addPath as jest.Mock;
spyCoreExportVariable = jest.spyOn(core, 'exportVariable');
spyCoreExportVariable = core.exportVariable as jest.Mock;
});
afterEach(() => {
@@ -235,7 +290,7 @@ describe('findGraalPyVersion', () => {
});
it('found and install successfully', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.0.0', architecture)
);
@@ -262,7 +317,7 @@ describe('findGraalPyVersion', () => {
});
it('found and install successfully without environment update', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.0.0', architecture)
);
@@ -310,7 +365,7 @@ describe('findGraalPyVersion', () => {
});
it('check-latest enabled version found and install successfully', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.0.0', architecture)
);
@@ -329,14 +384,16 @@ describe('findGraalPyVersion', () => {
});
it('check-latest enabled version is not found and used from toolcache', async () => {
tcFind.mockImplementationOnce((tool: string, version: string) => {
const semverRange = new semver.Range(version);
let graalpyPath = '';
if (semver.satisfies('22.3.4', semverRange)) {
graalpyPath = path.join(toolDir, 'GraalPy', '22.3.4', architecture);
(tcFind as jest.Mock<typeof tc.find>).mockImplementationOnce(
(tool: string, version: string) => {
const semverRange = new semver.Range(version);
let graalpyPath = '';
if (semver.satisfies('22.3.4', semverRange)) {
graalpyPath = path.join(toolDir, 'GraalPy', '22.3.4', architecture);
}
return graalpyPath;
}
return graalpyPath;
});
);
await expect(
finder.findGraalPyVersion(
'graalpy-22.3.4',
@@ -353,7 +410,7 @@ describe('findGraalPyVersion', () => {
});
it('found and install successfully, pre-release fallback', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '24.1', architecture)
);
+160 -98
View File
@@ -1,23 +1,83 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {fileURLToPath} from 'url';
import fs from 'fs';
import * as utils from '../src/utils';
import {HttpClient} from '@actions/http-client';
import * as ifm from '@actions/http-client/lib/interfaces';
import * as tc from '@actions/tool-cache';
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import * as path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
import * as semver from 'semver';
import * as finder from '../src/find-pypy';
import {
IPyPyManifestRelease,
IS_WINDOWS,
getPyPyVersionFromPath
} from '../src/utils';
// Mock @actions modules
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
import manifestData from './data/pypy.json';
jest.unstable_mockModule('@actions/tool-cache', () => ({
find: jest.fn(),
findAllVersions: jest.fn(),
downloadTool: jest.fn(),
extractZip: jest.fn(),
extractTar: jest.fn(),
extract7z: jest.fn(),
extractXar: jest.fn(),
cacheDir: jest.fn(),
cacheFile: jest.fn(),
getManifestFromRepo: jest.fn(),
findFromManifest: jest.fn(),
evaluateVersions: jest.fn()
}));
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
// Import real utils BEFORE mock registration to get real function references
const realUtils = await import('../src/utils.js');
// Mock local utils module for readExactPyPyVersionFile/writeExactPyPyVersionFile
jest.unstable_mockModule('../src/utils.js', () => ({
...realUtils,
readExactPyPyVersionFile: jest.fn(),
writeExactPyPyVersionFile: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const exec = await import('@actions/exec');
const utils = await import('../src/utils.js');
const finder = await import('../src/find-pypy.js');
// Non-mocked imports
import {HttpClient} from '@actions/http-client';
import type * as ifm from '@actions/http-client/lib/interfaces';
import type {IPyPyManifestRelease} from '../src/utils.js';
import manifestData from './data/pypy.json' with {type: 'json'};
const IS_WINDOWS = utils.IS_WINDOWS;
const getPyPyVersionFromPath = utils.getPyPyVersionFromPath;
let architecture: string;
@@ -79,43 +139,45 @@ describe('findPyPyToolCache', () => {
const actualPythonVersion = '3.6.17';
const actualPyPyVersion = '7.5.4';
const pypyPath = path.join('PyPy', actualPythonVersion, architecture);
let tcFind: jest.SpyInstance;
let spyReadExactPyPyVersion: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let addPathSpy: jest.SpyInstance;
let exportVariableSpy: jest.SpyInstance;
let setOutputSpy: jest.SpyInstance;
let tcFind: jest.Mock;
let spyReadExactPyPyVersion: jest.Mock;
let infoSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let addPathSpy: jest.Mock;
let exportVariableSpy: jest.Mock;
let setOutputSpy: jest.Mock;
beforeEach(() => {
tcFind = jest.spyOn(tc, 'find');
tcFind.mockImplementation((toolname: string, pythonVersion: string) => {
const semverVersion = new semver.Range(pythonVersion);
return semver.satisfies(actualPythonVersion, semverVersion)
? pypyPath
: '';
});
tcFind = tc.find as jest.Mock;
(tcFind as jest.Mock<typeof tc.find>).mockImplementation(
(toolname: string, pythonVersion: string) => {
const semverVersion = new semver.Range(pythonVersion);
return semver.satisfies(actualPythonVersion, semverVersion)
? pypyPath
: '';
}
);
spyReadExactPyPyVersion = jest.spyOn(utils, 'readExactPyPyVersionFile');
spyReadExactPyPyVersion = utils.readExactPyPyVersionFile as jest.Mock;
spyReadExactPyPyVersion.mockImplementation(() => actualPyPyVersion);
infoSpy = jest.spyOn(core, 'info');
infoSpy = core.info as jest.Mock;
infoSpy.mockImplementation(() => null);
warningSpy = jest.spyOn(core, 'warning');
warningSpy = core.warning as jest.Mock;
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy = core.debug as jest.Mock;
debugSpy.mockImplementation(() => null);
addPathSpy = jest.spyOn(core, 'addPath');
addPathSpy = core.addPath as jest.Mock;
addPathSpy.mockImplementation(() => null);
exportVariableSpy = jest.spyOn(core, 'exportVariable');
exportVariableSpy = core.exportVariable as jest.Mock;
exportVariableSpy.mockImplementation(() => null);
setOutputSpy = jest.spyOn(core, 'setOutput');
setOutputSpy = core.setOutput as jest.Mock;
setOutputSpy.mockImplementation(() => null);
});
@@ -159,84 +221,82 @@ describe('findPyPyToolCache', () => {
});
describe('findPyPyVersion', () => {
let getBooleanInputSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let addPathSpy: jest.SpyInstance;
let exportVariableSpy: jest.SpyInstance;
let setOutputSpy: jest.SpyInstance;
let tcFind: jest.SpyInstance;
let spyExtractZip: jest.SpyInstance;
let spyExtractTar: jest.SpyInstance;
let spyHttpClient: jest.SpyInstance;
let spyExistsSync: jest.SpyInstance;
let spyExec: jest.SpyInstance;
let spySymlinkSync: jest.SpyInstance;
let spyDownloadTool: jest.SpyInstance;
let spyReadExactPyPyVersion: jest.SpyInstance;
let spyFsReadDir: jest.SpyInstance;
let spyWriteExactPyPyVersionFile: jest.SpyInstance;
let spyCacheDir: jest.SpyInstance;
let spyChmodSync: jest.SpyInstance;
let spyCoreAddPath: jest.SpyInstance;
let spyCoreExportVariable: jest.SpyInstance;
let getBooleanInputSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let infoSpy: jest.Mock;
let addPathSpy: jest.Mock;
let exportVariableSpy: jest.Mock;
let setOutputSpy: jest.Mock;
let tcFind: jest.Mock;
let spyExtractZip: jest.Mock;
let spyExtractTar: jest.Mock;
let spyHttpClient: jest.SpiedFunction<typeof HttpClient.prototype.getJson>;
let spyExistsSync: jest.SpiedFunction<typeof fs.existsSync>;
let spyExec: jest.Mock;
let spySymlinkSync: jest.SpiedFunction<typeof fs.symlinkSync>;
let spyDownloadTool: jest.Mock;
let spyReadExactPyPyVersion: jest.Mock;
let spyFsReadDir: jest.SpiedFunction<typeof fs.readdirSync>;
let spyWriteExactPyPyVersionFile: jest.Mock;
let spyCacheDir: jest.Mock;
let spyChmodSync: jest.SpiedFunction<typeof fs.chmodSync>;
let spyCoreAddPath: jest.Mock;
let spyCoreExportVariable: jest.Mock;
const env = process.env;
beforeEach(() => {
getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
getBooleanInputSpy = core.getBooleanInput as jest.Mock;
getBooleanInputSpy.mockImplementation(() => false);
infoSpy = jest.spyOn(core, 'info');
infoSpy = core.info as jest.Mock;
infoSpy.mockImplementation(() => {});
warningSpy = jest.spyOn(core, 'warning');
warningSpy = core.warning as jest.Mock;
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy = core.debug as jest.Mock;
debugSpy.mockImplementation(() => null);
addPathSpy = jest.spyOn(core, 'addPath');
addPathSpy = core.addPath as jest.Mock;
addPathSpy.mockImplementation(() => null);
exportVariableSpy = jest.spyOn(core, 'exportVariable');
exportVariableSpy = core.exportVariable as jest.Mock;
exportVariableSpy.mockImplementation(() => null);
setOutputSpy = jest.spyOn(core, 'setOutput');
setOutputSpy = core.setOutput as jest.Mock;
setOutputSpy.mockImplementation(() => null);
jest.resetModules();
process.env = {...env};
tcFind = jest.spyOn(tc, 'find');
tcFind.mockImplementation((tool: string, version: string) => {
const semverRange = new semver.Range(version);
let pypyPath = '';
if (semver.satisfies('3.6.12', semverRange)) {
pypyPath = path.join(toolDir, 'PyPy', '3.6.12', architecture);
tcFind = tc.find as jest.Mock;
(tcFind as jest.Mock<typeof tc.find>).mockImplementation(
(tool: string, version: string) => {
const semverRange = new semver.Range(version);
let pypyPath = '';
if (semver.satisfies('3.6.12', semverRange)) {
pypyPath = path.join(toolDir, 'PyPy', '3.6.12', architecture);
}
return pypyPath;
}
return pypyPath;
});
spyWriteExactPyPyVersionFile = jest.spyOn(
utils,
'writeExactPyPyVersionFile'
);
spyWriteExactPyPyVersionFile = utils.writeExactPyPyVersionFile as jest.Mock;
spyWriteExactPyPyVersionFile.mockImplementation(() => null);
spyReadExactPyPyVersion = jest.spyOn(utils, 'readExactPyPyVersionFile');
spyReadExactPyPyVersion = utils.readExactPyPyVersionFile as jest.Mock;
spyReadExactPyPyVersion.mockImplementation(() => '7.3.3');
spyDownloadTool = jest.spyOn(tc, 'downloadTool');
spyDownloadTool = tc.downloadTool as jest.Mock;
spyDownloadTool.mockImplementation(() => path.join(tempDir, 'PyPy'));
spyExtractZip = jest.spyOn(tc, 'extractZip');
spyExtractZip = tc.extractZip as jest.Mock;
spyExtractZip.mockImplementation(() => tempDir);
spyExtractTar = jest.spyOn(tc, 'extractTar');
spyExtractTar = tc.extractTar as jest.Mock;
spyExtractTar.mockImplementation(() => tempDir);
spyFsReadDir = jest.spyOn(fs, 'readdirSync');
spyFsReadDir.mockImplementation((directory: string) => ['PyPyTest']);
spyFsReadDir.mockImplementation(() => ['PyPyTest'] as any);
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient.mockImplementation(
@@ -250,7 +310,7 @@ describe('findPyPyVersion', () => {
}
);
spyExec = jest.spyOn(exec, 'exec');
spyExec = exec.exec as jest.Mock;
spyExec.mockImplementation(() => undefined);
spySymlinkSync = jest.spyOn(fs, 'symlinkSync');
@@ -259,9 +319,9 @@ describe('findPyPyVersion', () => {
spyExistsSync = jest.spyOn(fs, 'existsSync');
spyExistsSync.mockReturnValue(true);
spyCoreAddPath = jest.spyOn(core, 'addPath');
spyCoreAddPath = core.addPath as jest.Mock;
spyCoreExportVariable = jest.spyOn(core, 'exportVariable');
spyCoreExportVariable = core.exportVariable as jest.Mock;
});
afterEach(() => {
@@ -308,7 +368,7 @@ describe('findPyPyVersion', () => {
});
it('found and install successfully', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'PyPy', '3.7.7', architecture)
);
@@ -338,7 +398,7 @@ describe('findPyPyVersion', () => {
});
it('found and install successfully without environment update', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'PyPy', '3.7.7', architecture)
);
@@ -394,7 +454,7 @@ describe('findPyPyVersion', () => {
});
it('check-latest enabled version found and install successfully', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'PyPy', '3.7.7', architecture)
);
@@ -418,14 +478,16 @@ describe('findPyPyVersion', () => {
});
it('check-latest enabled version is not found and used from toolcache', async () => {
tcFind.mockImplementationOnce((tool: string, version: string) => {
const semverRange = new semver.Range(version);
let pypyPath = '';
if (semver.satisfies('3.8.8', semverRange)) {
pypyPath = path.join(toolDir, 'PyPy', '3.8.8', architecture);
(tcFind as jest.Mock<typeof tc.find>).mockImplementationOnce(
(tool: string, version: string) => {
const semverRange = new semver.Range(version);
let pypyPath = '';
if (semver.satisfies('3.8.8', semverRange)) {
pypyPath = path.join(toolDir, 'PyPy', '3.8.8', architecture);
}
return pypyPath;
}
return pypyPath;
});
);
await expect(
finder.findPyPyVersion(
'pypy-3.8-v7.3.x',
@@ -445,7 +507,7 @@ describe('findPyPyVersion', () => {
});
it('found and install successfully, pre-release fallback', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'PyPy', '3.8.12', architecture)
);
+2 -1
View File
@@ -1,4 +1,5 @@
import {desugarVersion, pythonVersionToSemantic} from '../src/find-python';
import {describe, it, expect} from '@jest/globals';
import {desugarVersion, pythonVersionToSemantic} from '../src/find-python.js';
describe('desugarVersion', () => {
it.each([
+97 -59
View File
@@ -1,8 +1,12 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {fileURLToPath} from 'url';
import * as io from '@actions/io';
import os from 'os';
import fs from 'fs';
import path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const toolDir = path.join(
__dirname,
'runner',
@@ -19,26 +23,78 @@ const tempDir = path.join(
process.env['RUNNER_TOOL_CACHE'] = toolDir;
process.env['RUNNER_TEMP'] = tempDir;
import * as tc from '@actions/tool-cache';
import * as core from '@actions/core';
import * as finder from '../src/find-python';
import * as installer from '../src/install-python';
// Mock @actions modules
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
import manifestData from './data/versions-manifest.json';
// Pre-import real @actions/tool-cache before any mocks
const realTc = await import('@actions/tool-cache');
jest.unstable_mockModule('@actions/tool-cache', () => ({
...realTc,
find: jest.fn(realTc.find),
getManifestFromRepo: jest.fn()
}));
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
// Pre-import real install-python AFTER all its dependency mocks are registered
// so it captures the mocked @actions/tool-cache, @actions/core, @actions/exec
const realInstaller = await import('../src/install-python.js');
// Mock local install-python module - keep real getManifest/findReleaseFromManifest
jest.unstable_mockModule('../src/install-python.js', () => ({
...realInstaller,
installCpythonFromRelease: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const finder = await import('../src/find-python.js');
const installer = await import('../src/install-python.js');
import manifestData from './data/versions-manifest.json' with {type: 'json'};
describe('Finder tests', () => {
let writeSpy: jest.SpyInstance;
let spyCoreAddPath: jest.SpyInstance;
let spyCoreExportVariable: jest.SpyInstance;
let writeSpy: jest.SpiedFunction<typeof process.stdout.write>;
let spyCoreAddPath: jest.Mock;
let spyCoreExportVariable: jest.Mock;
const env = process.env;
beforeEach(() => {
writeSpy = jest.spyOn(process.stdout, 'write');
writeSpy.mockImplementation(() => {});
jest.resetModules();
writeSpy.mockImplementation(() => true);
process.env = {...env};
spyCoreAddPath = jest.spyOn(core, 'addPath');
spyCoreExportVariable = jest.spyOn(core, 'exportVariable');
spyCoreAddPath = core.addPath as jest.Mock;
spyCoreExportVariable = core.exportVariable as jest.Mock;
// Restore real tc.find default (cleared by jest.resetAllMocks)
(tc.find as jest.Mock).mockImplementation(realTc.find as any);
});
afterEach(() => {
@@ -49,8 +105,8 @@ describe('Finder tests', () => {
});
it('Finds Python if it is installed', async () => {
const getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
getBooleanInputSpy.mockImplementation(input => false);
const getBooleanInputSpy = core.getBooleanInput as jest.Mock;
getBooleanInputSpy.mockImplementation(() => false);
const pythonDir: string = path.join(toolDir, 'Python', '3.0.0', 'x64');
await io.mkdirP(pythonDir);
@@ -79,16 +135,13 @@ describe('Finder tests', () => {
});
it('Finds stable Python version if it is not installed, but exists in the manifest', async () => {
const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo');
findSpy.mockImplementation(() => <tc.IToolRelease[]>manifestData);
const findSpy = tc.getManifestFromRepo as jest.Mock;
findSpy.mockImplementation(() => manifestData);
const getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
getBooleanInputSpy.mockImplementation(input => false);
const getBooleanInputSpy = core.getBooleanInput as jest.Mock;
getBooleanInputSpy.mockImplementation(() => false);
const installSpy: jest.SpyInstance = jest.spyOn(
installer,
'installCpythonFromRelease'
);
const installSpy = installer.installCpythonFromRelease as jest.Mock;
installSpy.mockImplementation(async () => {
const pythonDir: string = path.join(toolDir, 'Python', '1.2.3', 'x64');
await io.mkdirP(pythonDir);
@@ -113,16 +166,13 @@ describe('Finder tests', () => {
});
it('Finds pre-release Python version in the manifest', async () => {
const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo');
findSpy.mockImplementation(() => <tc.IToolRelease[]>manifestData);
const findSpy = tc.getManifestFromRepo as jest.Mock;
findSpy.mockImplementation(() => manifestData);
const getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
getBooleanInputSpy.mockImplementation(input => false);
const getBooleanInputSpy = core.getBooleanInput as jest.Mock;
getBooleanInputSpy.mockImplementation(() => false);
const installSpy: jest.SpyInstance = jest.spyOn(
installer,
'installCpythonFromRelease'
);
const installSpy = installer.installCpythonFromRelease as jest.Mock;
installSpy.mockImplementation(async () => {
const pythonDir: string = path.join(
toolDir,
@@ -150,40 +200,34 @@ describe('Finder tests', () => {
});
it('Check-latest true, finds the latest version in the manifest', async () => {
const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo');
findSpy.mockImplementation(() => <tc.IToolRelease[]>manifestData);
const findSpy = tc.getManifestFromRepo as jest.Mock;
findSpy.mockImplementation(() => manifestData);
const getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
getBooleanInputSpy.mockImplementation(input => true);
const getBooleanInputSpy = core.getBooleanInput as jest.Mock;
getBooleanInputSpy.mockImplementation(() => true);
const cnSpy: jest.SpyInstance = jest.spyOn(process.stdout, 'write');
cnSpy.mockImplementation(line => {
// uncomment to debug
// process.stderr.write('write:' + line + '\n');
});
const cnSpy = jest.spyOn(process.stdout, 'write');
cnSpy.mockImplementation(() => true);
const addPathSpy: jest.SpyInstance = jest.spyOn(core, 'addPath');
const addPathSpy = core.addPath as jest.Mock;
addPathSpy.mockImplementation(() => null);
const infoSpy: jest.SpyInstance = jest.spyOn(core, 'info');
const infoSpy = core.info as jest.Mock;
infoSpy.mockImplementation(() => {});
const debugSpy: jest.SpyInstance = jest.spyOn(core, 'debug');
const debugSpy = core.debug as jest.Mock;
debugSpy.mockImplementation(() => {});
const pythonDir: string = path.join(toolDir, 'Python', '1.2.2', 'x64');
const expPath: string = path.join(toolDir, 'Python', '1.2.3', 'x64');
const installSpy: jest.SpyInstance = jest.spyOn(
installer,
'installCpythonFromRelease'
);
const installSpy = installer.installCpythonFromRelease as jest.Mock;
installSpy.mockImplementation(async () => {
await io.mkdirP(expPath);
fs.writeFileSync(`${expPath}.complete`, 'hello');
});
const tcFindSpy: jest.SpyInstance = jest.spyOn(tc, 'find');
const tcFindSpy = tc.find as jest.Mock;
tcFindSpy
.mockImplementationOnce(() => '')
.mockImplementationOnce(() => expPath);
@@ -224,13 +268,10 @@ describe('Finder tests', () => {
});
it('Finds stable Python version if it is not installed, but exists in the manifest, skipping newer pre-release', async () => {
const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo');
findSpy.mockImplementation(() => <tc.IToolRelease[]>manifestData);
const findSpy = tc.getManifestFromRepo as jest.Mock;
findSpy.mockImplementation(() => manifestData);
const installSpy: jest.SpyInstance = jest.spyOn(
installer,
'installCpythonFromRelease'
);
const installSpy = installer.installCpythonFromRelease as jest.Mock;
installSpy.mockImplementation(async () => {
const pythonDir: string = path.join(toolDir, 'Python', '1.2.3', 'x64');
await io.mkdirP(pythonDir);
@@ -246,13 +287,10 @@ describe('Finder tests', () => {
});
it('Finds Python version if it is not installed, but exists in the manifest, pre-release fallback', async () => {
const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo');
findSpy.mockImplementation(() => <tc.IToolRelease[]>manifestData);
const findSpy = tc.getManifestFromRepo as jest.Mock;
findSpy.mockImplementation(() => manifestData);
const installSpy: jest.SpyInstance = jest.spyOn(
installer,
'installCpythonFromRelease'
);
const installSpy = installer.installCpythonFromRelease as jest.Mock;
installSpy.mockImplementation(async () => {
const pythonDir: string = path.join(
toolDir,
+106 -45
View File
@@ -1,20 +1,75 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {fileURLToPath} from 'url';
import fs from 'fs';
import {HttpClient} from '@actions/http-client';
import * as ifm from '@actions/http-client/lib/interfaces';
import * as tc from '@actions/tool-cache';
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import * as path from 'path';
import * as installer from '../src/install-graalpy';
import {
IGraalPyManifestRelease,
IGraalPyManifestAsset,
IS_WINDOWS
} from '../src/utils';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
import manifestData from './data/graalpy.json';
// Mock @actions modules
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
jest.unstable_mockModule('@actions/tool-cache', () => ({
find: jest.fn(),
findAllVersions: jest.fn(),
downloadTool: jest.fn(),
extractZip: jest.fn(),
extractTar: jest.fn(),
extract7z: jest.fn(),
extractXar: jest.fn(),
cacheDir: jest.fn(),
cacheFile: jest.fn(),
getManifestFromRepo: jest.fn(),
findFromManifest: jest.fn(),
evaluateVersions: jest.fn()
}));
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const exec = await import('@actions/exec');
// Non-mocked imports
import {HttpClient} from '@actions/http-client';
import type * as ifm from '@actions/http-client/lib/interfaces';
const installer = await import('../src/install-graalpy.js');
const utils = await import('../src/utils.js');
import type {
IGraalPyManifestRelease,
IGraalPyManifestAsset
} from '../src/utils.js';
import manifestData from './data/graalpy.json' with {type: 'json'};
const IS_WINDOWS = utils.IS_WINDOWS;
const architecture = 'x64';
@@ -48,18 +103,18 @@ describe('findRelease', () => {
browser_download_url: `https://github.com/graalvm/graal-languages-ea-builds/releases/download/graalpy-24.1.0-ea.09/graalpy-24.1.0-ea.09-${extensionName}`
};
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let infoSpy: jest.Mock;
beforeEach(() => {
infoSpy = jest.spyOn(core, 'info');
infoSpy = core.info as jest.Mock;
infoSpy.mockImplementation(() => {});
warningSpy = jest.spyOn(core, 'warning');
warningSpy = core.warning as jest.Mock;
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy = core.debug as jest.Mock;
debugSpy.mockImplementation(() => null);
});
@@ -115,51 +170,57 @@ describe('findRelease', () => {
resolvedGraalPyVersion: '24.1.0-ea.9'
});
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
jest.restoreAllMocks();
});
});
describe('installGraalPy', () => {
let tcFind: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let spyExtractZip: jest.SpyInstance;
let spyExtractTar: jest.SpyInstance;
let spyFsReadDir: jest.SpyInstance;
let spyFsWriteFile: jest.SpyInstance;
let spyHttpClient: jest.SpyInstance;
let spyExistsSync: jest.SpyInstance;
let spyExec: jest.SpyInstance;
let spySymlinkSync: jest.SpyInstance;
let spyDownloadTool: jest.SpyInstance;
let spyCacheDir: jest.SpyInstance;
let spyChmodSync: jest.SpyInstance;
let tcFind: jest.Mock;
let infoSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let spyExtractZip: jest.Mock;
let spyExtractTar: jest.Mock;
let spyFsReadDir: jest.SpiedFunction<typeof fs.readdirSync>;
let spyFsWriteFile: jest.SpiedFunction<typeof fs.writeFileSync>;
let spyHttpClient: jest.SpiedFunction<typeof HttpClient.prototype.getJson>;
let spyExistsSync: jest.SpiedFunction<typeof fs.existsSync>;
let spyExec: jest.Mock;
let spySymlinkSync: jest.SpiedFunction<typeof fs.symlinkSync>;
let spyDownloadTool: jest.Mock;
let spyCacheDir: jest.Mock;
let spyChmodSync: jest.SpiedFunction<typeof fs.chmodSync>;
beforeEach(() => {
tcFind = jest.spyOn(tc, 'find');
tcFind = tc.find as jest.Mock;
tcFind.mockImplementation(() =>
path.join('GraalPy', '3.6.12', architecture)
);
spyDownloadTool = jest.spyOn(tc, 'downloadTool');
spyDownloadTool = tc.downloadTool as jest.Mock;
spyDownloadTool.mockImplementation(() => path.join(tempDir, 'GraalPy'));
spyExtractZip = jest.spyOn(tc, 'extractZip');
spyExtractZip = tc.extractZip as jest.Mock;
spyExtractZip.mockImplementation(() => tempDir);
spyExtractTar = jest.spyOn(tc, 'extractTar');
spyExtractTar = tc.extractTar as jest.Mock;
spyExtractTar.mockImplementation(() => tempDir);
infoSpy = jest.spyOn(core, 'info');
infoSpy = core.info as jest.Mock;
infoSpy.mockImplementation(() => {});
warningSpy = jest.spyOn(core, 'warning');
warningSpy = core.warning as jest.Mock;
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy = core.debug as jest.Mock;
debugSpy.mockImplementation(() => null);
spyFsReadDir = jest.spyOn(fs, 'readdirSync');
spyFsReadDir.mockImplementation(() => ['GraalPyTest']);
spyFsReadDir.mockImplementation(() => ['GraalPyTest'] as any);
spyFsWriteFile = jest.spyOn(fs, 'writeFileSync');
spyFsWriteFile.mockImplementation(() => undefined);
@@ -176,7 +237,7 @@ describe('installGraalPy', () => {
}
);
spyExec = jest.spyOn(exec, 'exec');
spyExec = exec.exec as jest.Mock;
spyExec.mockImplementation(() => undefined);
spySymlinkSync = jest.spyOn(fs, 'symlinkSync');
@@ -205,7 +266,7 @@ describe('installGraalPy', () => {
});
it('found and install GraalPy', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '21.3.0', architecture)
);
@@ -227,7 +288,7 @@ describe('installGraalPy', () => {
});
it('found and install GraalPy, pre-release fallback', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '24.1.0', architecture)
);
+103 -47
View File
@@ -1,20 +1,72 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {fileURLToPath} from 'url';
import fs from 'fs';
import {HttpClient} from '@actions/http-client';
import * as ifm from '@actions/http-client/lib/interfaces';
import * as tc from '@actions/tool-cache';
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import * as path from 'path';
import * as installer from '../src/install-pypy';
import {
IPyPyManifestRelease,
IPyPyManifestAsset,
IS_WINDOWS
} from '../src/utils';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
import manifestData from './data/pypy.json';
// Mock @actions modules
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
jest.unstable_mockModule('@actions/tool-cache', () => ({
find: jest.fn(),
findAllVersions: jest.fn(),
downloadTool: jest.fn(),
extractZip: jest.fn(),
extractTar: jest.fn(),
extract7z: jest.fn(),
extractXar: jest.fn(),
cacheDir: jest.fn(),
cacheFile: jest.fn(),
getManifestFromRepo: jest.fn(),
findFromManifest: jest.fn(),
evaluateVersions: jest.fn()
}));
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const exec = await import('@actions/exec');
// Non-mocked imports
import {HttpClient} from '@actions/http-client';
import type * as ifm from '@actions/http-client/lib/interfaces';
const installer = await import('../src/install-pypy.js');
const utils = await import('../src/utils.js');
import type {IPyPyManifestRelease, IPyPyManifestAsset} from '../src/utils.js';
import manifestData from './data/pypy.json' with {type: 'json'};
const IS_WINDOWS = utils.IS_WINDOWS;
let architecture: string;
if (IS_WINDOWS) {
@@ -58,19 +110,18 @@ describe('findRelease', () => {
download_url: `https://test.download.python.org/pypy/pypy3.6-v7.4.0rc1-${extensionName}`
};
let getBooleanInputSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let infoSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
beforeEach(() => {
infoSpy = jest.spyOn(core, 'info');
infoSpy = core.info as jest.Mock;
infoSpy.mockImplementation(() => {});
warningSpy = jest.spyOn(core, 'warning');
warningSpy = core.warning as jest.Mock;
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy = core.debug as jest.Mock;
debugSpy.mockImplementation(() => null);
});
@@ -215,50 +266,55 @@ describe('findRelease', () => {
resolvedPyPyVersion: pypyVersion
});
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
jest.restoreAllMocks();
});
});
describe('installPyPy', () => {
let tcFind: jest.SpyInstance;
let getBooleanInputSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let spyExtractZip: jest.SpyInstance;
let spyExtractTar: jest.SpyInstance;
let spyFsReadDir: jest.SpyInstance;
let spyFsWriteFile: jest.SpyInstance;
let spyHttpClient: jest.SpyInstance;
let spyExistsSync: jest.SpyInstance;
let spyExec: jest.SpyInstance;
let spySymlinkSync: jest.SpyInstance;
let spyDownloadTool: jest.SpyInstance;
let spyCacheDir: jest.SpyInstance;
let spyChmodSync: jest.SpyInstance;
let tcFind: jest.Mock;
let infoSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let spyExtractZip: jest.Mock;
let spyExtractTar: jest.Mock;
let spyFsReadDir: jest.SpiedFunction<typeof fs.readdirSync>;
let spyFsWriteFile: jest.SpiedFunction<typeof fs.writeFileSync>;
let spyHttpClient: jest.SpiedFunction<typeof HttpClient.prototype.getJson>;
let spyExistsSync: jest.SpiedFunction<typeof fs.existsSync>;
let spyExec: jest.Mock;
let spySymlinkSync: jest.SpiedFunction<typeof fs.symlinkSync>;
let spyDownloadTool: jest.Mock;
let spyCacheDir: jest.Mock;
let spyChmodSync: jest.SpiedFunction<typeof fs.chmodSync>;
beforeEach(() => {
tcFind = jest.spyOn(tc, 'find');
tcFind = tc.find as jest.Mock;
tcFind.mockImplementation(() => path.join('PyPy', '3.6.12', architecture));
spyDownloadTool = jest.spyOn(tc, 'downloadTool');
spyDownloadTool = tc.downloadTool as jest.Mock;
spyDownloadTool.mockImplementation(() => path.join(tempDir, 'PyPy'));
spyExtractZip = jest.spyOn(tc, 'extractZip');
spyExtractZip = tc.extractZip as jest.Mock;
spyExtractZip.mockImplementation(() => tempDir);
spyExtractTar = jest.spyOn(tc, 'extractTar');
spyExtractTar = tc.extractTar as jest.Mock;
spyExtractTar.mockImplementation(() => tempDir);
infoSpy = jest.spyOn(core, 'info');
infoSpy = core.info as jest.Mock;
infoSpy.mockImplementation(() => {});
warningSpy = jest.spyOn(core, 'warning');
warningSpy = core.warning as jest.Mock;
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy = core.debug as jest.Mock;
debugSpy.mockImplementation(() => null);
spyFsReadDir = jest.spyOn(fs, 'readdirSync');
spyFsReadDir.mockImplementation(() => ['PyPyTest']);
spyFsReadDir.mockImplementation(() => ['PyPyTest'] as any);
spyFsWriteFile = jest.spyOn(fs, 'writeFileSync');
spyFsWriteFile.mockImplementation(() => undefined);
@@ -275,7 +331,7 @@ describe('installPyPy', () => {
}
);
spyExec = jest.spyOn(exec, 'exec');
spyExec = exec.exec as jest.Mock;
spyExec.mockImplementation(() => undefined);
spySymlinkSync = jest.spyOn(fs, 'symlinkSync');
@@ -304,7 +360,7 @@ describe('installPyPy', () => {
});
it('found and install PyPy', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'PyPy', '3.6.12', architecture)
);
@@ -328,7 +384,7 @@ describe('installPyPy', () => {
});
it('found and install PyPy, pre-release fallback', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'PyPy', '3.6.12', architecture)
);
+107 -37
View File
@@ -1,16 +1,76 @@
import {
getManifest,
getManifestFromRepo,
getManifestFromURL
} from '../src/install-python';
import * as httpm from '@actions/http-client';
import * as tc from '@actions/tool-cache';
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
jest.mock('@actions/http-client');
jest.mock('@actions/tool-cache');
jest.mock('@actions/tool-cache', () => ({
class MockHttpClientError extends Error {
statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.name = 'HttpClientError';
this.statusCode = statusCode;
}
}
// Mock @actions/http-client
jest.unstable_mockModule('@actions/http-client', () => ({
HttpClient: jest.fn().mockImplementation(() => ({
getJson: jest.fn()
})),
HttpClientError: MockHttpClientError,
HttpCodes: {
OK: 200,
NotFound: 404,
InternalServerError: 500
}
}));
// Mock @actions/cache (needed transitively by utils.ts)
jest.unstable_mockModule('@actions/cache', () => ({
saveCache: jest.fn(),
restoreCache: jest.fn(),
isFeatureAvailable: jest.fn()
}));
// Mock @actions/tool-cache
jest.unstable_mockModule('@actions/tool-cache', () => ({
getManifestFromRepo: jest.fn()
}));
// Mock @actions/core (needed by install-python.ts)
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
// Mock @actions/exec (needed by install-python.ts)
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
// Dynamic imports after mocking
const httpm = await import('@actions/http-client');
const tc = await import('@actions/tool-cache');
const {getManifest, getManifestFromRepo, getManifestFromURL} =
await import('../src/install-python.js');
const mockManifest = [
{
version: '1.0.0',
@@ -36,19 +96,19 @@ describe('getManifest', () => {
});
it('should return manifest from repo', async () => {
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue(mockManifest);
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue(mockManifest);
const manifest = await getManifest();
expect(manifest).toEqual(mockManifest);
});
it('should return manifest from URL if repo fetch fails', async () => {
jest.useFakeTimers();
(tc.getManifestFromRepo as jest.Mock).mockRejectedValue(
(tc.getManifestFromRepo as jest.Mock<any>).mockRejectedValue(
new Error('Fetch failed')
);
(httpm.HttpClient.prototype.getJson as jest.Mock).mockResolvedValue({
result: mockManifest
});
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({
getJson: jest.fn(async () => ({result: mockManifest}))
}));
const promise = getManifest();
await jest.runAllTimersAsync();
const manifest = await promise;
@@ -57,10 +117,10 @@ describe('getManifest', () => {
it('should fall back to URL if repo returns a truncated/empty manifest', async () => {
jest.useFakeTimers();
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue([]);
(httpm.HttpClient.prototype.getJson as jest.Mock).mockResolvedValue({
result: mockManifest
});
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue([]);
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({
getJson: jest.fn(async () => ({result: mockManifest}))
}));
const promise = getManifest();
await jest.runAllTimersAsync();
const manifest = await promise;
@@ -69,22 +129,22 @@ describe('getManifest', () => {
it('should retry on a transient invalid manifest and then succeed', async () => {
jest.useFakeTimers();
(tc.getManifestFromRepo as jest.Mock)
(tc.getManifestFromRepo as jest.Mock<any>)
.mockResolvedValueOnce([])
.mockResolvedValueOnce(mockManifest);
const promise = getManifest();
await jest.runAllTimersAsync();
const manifest = await promise;
expect(manifest).toEqual(mockManifest);
expect(tc.getManifestFromRepo as jest.Mock).toHaveBeenCalledTimes(2);
expect(tc.getManifestFromRepo as jest.Mock<any>).toHaveBeenCalledTimes(2);
});
it('should fail loudly when the manifest is truncated/empty on every source', async () => {
jest.useFakeTimers();
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue([]);
(httpm.HttpClient.prototype.getJson as jest.Mock).mockResolvedValue({
result: []
});
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue([]);
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({
getJson: jest.fn(async () => ({result: []}))
}));
const promise = getManifest();
// Prevent unhandled rejection before timers advance.
const catchPromise = promise.catch(() => {});
@@ -99,37 +159,47 @@ describe('getManifest', () => {
const rateLimitError = Object.assign(new Error('API rate limit exceeded'), {
httpStatusCode: 403
});
(tc.getManifestFromRepo as jest.Mock).mockRejectedValue(rateLimitError);
(httpm.HttpClient.prototype.getJson as jest.Mock).mockResolvedValue({
result: mockManifest
});
(tc.getManifestFromRepo as jest.Mock<any>).mockRejectedValue(
rateLimitError
);
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({
getJson: jest.fn(async () => ({result: mockManifest}))
}));
const manifest = await getManifest();
expect(manifest).toEqual(mockManifest);
expect(tc.getManifestFromRepo as jest.Mock).toHaveBeenCalledTimes(1);
expect(tc.getManifestFromRepo as jest.Mock<any>).toHaveBeenCalledTimes(1);
});
});
describe('getManifestFromRepo', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should return manifest from repo', async () => {
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue(mockManifest);
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue(mockManifest);
const manifest = await getManifestFromRepo();
expect(manifest).toEqual(mockManifest);
});
});
describe('getManifestFromURL', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should return manifest from URL', async () => {
(httpm.HttpClient.prototype.getJson as jest.Mock).mockResolvedValue({
result: mockManifest
});
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({
getJson: jest.fn(async () => ({result: mockManifest}))
}));
const manifest = await getManifestFromURL();
expect(manifest).toEqual(mockManifest);
});
it('should throw error if unable to get manifest from URL', async () => {
(httpm.HttpClient.prototype.getJson as jest.Mock).mockResolvedValue({
result: null
});
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({
getJson: jest.fn(async () => ({result: null}))
}));
await expect(getManifestFromURL()).rejects.toThrow(
'Unable to get manifest from'
);
+61 -14
View File
@@ -1,11 +1,57 @@
import * as cache from '@actions/cache';
import * as core from '@actions/core';
import {
jest,
describe,
it,
expect,
afterAll,
afterEach,
beforeEach
} from '@jest/globals';
import {fileURLToPath} from 'url';
import path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Mock @actions/cache
jest.unstable_mockModule('@actions/cache', () => ({
isFeatureAvailable: jest.fn(),
saveCache: jest.fn(),
restoreCache: jest.fn()
}));
// Mock @actions/core
jest.unstable_mockModule('@actions/core', () => ({
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
setOutput: jest.fn(),
setFailed: jest.fn(),
warning: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
startGroup: jest.fn(),
endGroup: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
const cache = await import('@actions/cache');
const core = await import('@actions/core');
import * as io from '@actions/io';
import fs from 'fs';
import path from 'path';
import {
const {
validateVersion,
validatePythonVersionFormatForPyPy,
isCacheFeatureAvailable,
@@ -18,10 +64,7 @@ import {
IS_WINDOWS,
getDownloadFileName,
getVersionInputFromToolVersions
} from '../src/utils';
jest.mock('@actions/cache');
jest.mock('@actions/core');
} = await import('../src/utils.js');
describe('validatePythonVersionFormatForPyPy', () => {
it.each([
@@ -55,8 +98,8 @@ describe('validateVersion', () => {
describe('isCacheFeatureAvailable', () => {
it('isCacheFeatureAvailable disabled on GHES', () => {
jest.spyOn(cache, 'isFeatureAvailable').mockImplementation(() => false);
const infoMock = jest.spyOn(core, 'warning');
(cache.isFeatureAvailable as jest.Mock).mockImplementation(() => false);
const infoMock = core.warning as jest.Mock;
const message =
'Caching is only supported on GHES version >= 3.5. If you are on a version >= 3.5, please check with your GHES admin if the Actions cache service is enabled or not.';
try {
@@ -69,8 +112,8 @@ describe('isCacheFeatureAvailable', () => {
});
it('isCacheFeatureAvailable disabled on dotcom', () => {
jest.spyOn(cache, 'isFeatureAvailable').mockImplementation(() => false);
const infoMock = jest.spyOn(core, 'warning');
(cache.isFeatureAvailable as jest.Mock).mockImplementation(() => false);
const infoMock = core.warning as jest.Mock;
const message =
'The runner was not able to contact the cache service. Caching will be skipped';
try {
@@ -83,9 +126,14 @@ describe('isCacheFeatureAvailable', () => {
});
it('isCacheFeatureAvailable is enabled', () => {
jest.spyOn(cache, 'isFeatureAvailable').mockImplementation(() => true);
(cache.isFeatureAvailable as jest.Mock).mockImplementation(() => true);
expect(isCacheFeatureAvailable()).toBe(true);
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
});
const tempDir = path.join(
@@ -345,7 +393,6 @@ describe('isGhes', () => {
const pristineEnv = process.env;
beforeEach(() => {
jest.resetModules();
process.env = {...pristineEnv};
});
+52579 -48696
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+57896 -52848
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+74
View File
@@ -0,0 +1,74 @@
// This is a reusable configuration file copied from https://github.com/actions/reusable-workflows/tree/main/reusable-configurations. Please don't make changes to this file as it's the subject of an automatic update.
import js from '@eslint/js';
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import jest from 'eslint-plugin-jest';
import n from 'eslint-plugin-n';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
export default [
{
ignores: ['**/*', '!src/**', '!__tests__/**']
},
js.configs.recommended,
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
ecmaVersion: 2022,
sourceType: 'module',
globals: {
...globals.node,
...globals.es2015
}
},
plugins: {
'@typescript-eslint': tsPlugin,
n
},
rules: {
...tsPlugin.configs.recommended.rules,
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description'
}
],
'no-console': 'error',
yoda: 'error',
'prefer-const': [
'error',
{
destructuring: 'all'
}
],
'no-control-regex': 'off',
'no-constant-condition': ['error', {checkLoops: false}],
'no-undef': 'off',
'no-useless-assignment': 'off',
'n/no-extraneous-import': 'error'
}
},
{
files: ['**/*{test,spec}.ts'],
plugins: {jest},
languageOptions: {
globals: {
...globals.jest
}
},
rules: {
...jest.configs['flat/recommended'].rules,
'@typescript-eslint/no-unused-vars': 'off',
'jest/no-standalone-expect': 'off',
'jest/no-conditional-expect': 'off',
'no-console': 'off'
}
},
prettier
];
-11
View File
@@ -1,11 +0,0 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}
+24
View File
@@ -0,0 +1,24 @@
export default {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
roots: ['<rootDir>'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': [
'ts-jest',
{
useESM: true,
diagnostics: {
ignoreCodes: [151002]
}
}
]
},
extensionsToTreatAsEsm: ['.ts'],
transformIgnorePatterns: ['node_modules/(?!(@actions)/)'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1'
},
verbose: true
}
+2372 -1774
View File
File diff suppressed because it is too large Load Diff
+29 -30
View File
@@ -1,6 +1,7 @@
{
"name": "setup-python",
"version": "6.3.0",
"version": "7.0.0",
"type": "module",
"private": true,
"description": "Setup python action",
"main": "dist/index.js",
@@ -9,12 +10,12 @@
},
"scripts": {
"build": "ncc build -o dist/setup src/setup-python.ts && ncc build -o dist/cache-save src/cache-save.ts",
"format": "prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --write \"**/*.{ts,yml,yaml}\"",
"format-check": "prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --check \"**/*.{ts,yml,yaml}\"",
"lint": "eslint --config ./.eslintrc.js \"**/*.ts\"",
"lint:fix": "eslint --config ./.eslintrc.js \"**/*.ts\" --fix",
"format": "prettier --no-error-on-unmatched-pattern --write \"**/*.{ts,yml,yaml}\"",
"format-check": "prettier --no-error-on-unmatched-pattern --check \"**/*.{ts,yml,yaml}\"",
"lint": "eslint \"**/*.ts\"",
"lint:fix": "eslint \"**/*.ts\" --fix",
"release": "ncc build -o dist/setup src/setup-python.ts && ncc build -o dist/cache-save src/cache-save.ts && git add -f dist/",
"test": "jest --runInBand --coverage"
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --runInBand --coverage"
},
"repository": {
"type": "git",
@@ -28,34 +29,32 @@
"author": "GitHub",
"license": "MIT",
"dependencies": {
"@actions/cache": "^5.1.0",
"@actions/core": "^2.0.3",
"@actions/exec": "^2.0.0",
"@actions/glob": "^0.5.1",
"@actions/http-client": "^3.0.2",
"@actions/io": "^2.0.0",
"@actions/tool-cache": "^3.0.1",
"@actions/cache": "^6.1.0",
"@actions/core": "^3.0.1",
"@actions/exec": "^3.0.0",
"@actions/glob": "^0.7.0",
"@actions/http-client": "^4.0.1",
"@actions/io": "^3.0.2",
"@actions/tool-cache": "^4.0.0",
"@iarna/toml": "^3.0.0",
"semver": "^7.7.1"
"semver": "^7.8.5"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@eslint/js": "^10.0.1",
"@jest/globals": "^30.4.1",
"@types/node": "^24.10.1",
"@types/semver": "^7.7.0",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.54.0",
"@vercel/ncc": "^0.38.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-node": "^11.1.0",
"jest": "^29.7.0",
"jest-circus": "^29.7.0",
"prettier": "^3.6.2",
"ts-jest": "^29.3.2",
"typescript": "^5.9.3"
},
"overrides": {
"fast-xml-parser": "^5.9.2"
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.62.0",
"@vercel/ncc": "^0.44.0",
"eslint": "^10.5.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-jest": "^29.15.2",
"eslint-plugin-n": "^18.1.0",
"globals": "^17.7.0",
"jest": "^30.4.2",
"prettier": "^3.8.4",
"ts-jest": "^29.4.11",
"typescript": "^6.0.3"
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
import * as cache from '@actions/cache';
import * as core from '@actions/core';
import {getOSInfo, IS_LINUX} from '../utils';
import {CACHE_DEPENDENCY_BACKUP_PATH} from './constants';
import {getOSInfo, IS_LINUX} from '../utils.js';
import {CACHE_DEPENDENCY_BACKUP_PATH} from './constants.js';
export enum State {
STATE_CACHE_PRIMARY_KEY = 'cache-primary-key',
+3 -3
View File
@@ -1,6 +1,6 @@
import PipCache from './pip-cache';
import PipenvCache from './pipenv-cache';
import PoetryCache from './poetry-cache';
import PipCache from './pip-cache.js';
import PipenvCache from './pipenv-cache.js';
import PoetryCache from './poetry-cache.js';
export enum PackageManagers {
Pip = 'pip',
+3 -3
View File
@@ -6,9 +6,9 @@ import utils from 'util';
import * as path from 'path';
import os from 'os';
import CacheDistributor from './cache-distributor';
import {IS_WINDOWS} from '../utils';
import {CACHE_DEPENDENCY_BACKUP_PATH} from './constants';
import CacheDistributor from './cache-distributor.js';
import {IS_WINDOWS} from '../utils.js';
import {CACHE_DEPENDENCY_BACKUP_PATH} from './constants.js';
class PipCache extends CacheDistributor {
private cacheDependencyBackupPath: string = CACHE_DEPENDENCY_BACKUP_PATH;
+1 -1
View File
@@ -3,7 +3,7 @@ import * as os from 'os';
import * as path from 'path';
import * as core from '@actions/core';
import CacheDistributor from './cache-distributor';
import CacheDistributor from './cache-distributor.js';
class PipenvCache extends CacheDistributor {
constructor(
+2 -2
View File
@@ -4,8 +4,8 @@ import * as path from 'path';
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import CacheDistributor from './cache-distributor';
import {logWarning} from '../utils';
import CacheDistributor from './cache-distributor.js';
import {logWarning} from '../utils.js';
class PoetryCache extends CacheDistributor {
constructor(
+1 -1
View File
@@ -2,7 +2,7 @@ import * as core from '@actions/core';
import * as cache from '@actions/cache';
import fs from 'fs';
import {State} from './cache-distributions/cache-distributor';
import {State} from './cache-distributions/cache-distributor.js';
// Added early exit to resolve issue with slow post action step:
// - https://github.com/actions/setup-node/issues/878
+2 -2
View File
@@ -1,6 +1,6 @@
import * as path from 'path';
import * as graalpyInstall from './install-graalpy';
import {IS_WINDOWS, validateVersion, IGraalPyManifestRelease} from './utils';
import * as graalpyInstall from './install-graalpy.js';
import {IS_WINDOWS, validateVersion, IGraalPyManifestRelease} from './utils.js';
import * as semver from 'semver';
import * as core from '@actions/core';
+2 -2
View File
@@ -1,5 +1,5 @@
import * as path from 'path';
import * as pypyInstall from './install-pypy';
import * as pypyInstall from './install-pypy.js';
import {
IS_WINDOWS,
WINDOWS_ARCHS,
@@ -9,7 +9,7 @@ import {
validatePythonVersionFormatForPyPy,
IPyPyManifestRelease,
getBinaryDirectory
} from './utils';
} from './utils.js';
import * as semver from 'semver';
import * as core from '@actions/core';
+2 -2
View File
@@ -1,10 +1,10 @@
import * as os from 'os';
import * as path from 'path';
import {IS_WINDOWS, IS_LINUX, getOSInfo} from './utils';
import {IS_WINDOWS, IS_LINUX, getOSInfo} from './utils.js';
import * as semver from 'semver';
import * as installer from './install-python';
import * as installer from './install-python.js';
import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
+1 -1
View File
@@ -16,7 +16,7 @@ import {
createSymlinkInFolder,
isNightlyKeyword,
getNextPageUrl
} from './utils';
} from './utils.js';
const TOKEN = core.getInput('token');
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;
+1 -1
View File
@@ -17,7 +17,7 @@ import {
writeExactPyPyVersionFile,
getBinaryDirectory,
getDownloadFileName
} from './utils';
} from './utils.js';
export async function installPyPy(
pypyVersion: string,
+4 -3
View File
@@ -2,11 +2,11 @@ import * as path from 'path';
import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
import * as exec from '@actions/exec';
import {ExecOptions} from '@actions/exec';
import * as httpm from '@actions/http-client';
import * as fs from 'fs';
import * as semver from 'semver';
import {ExecOptions} from '@actions/exec/lib/interfaces';
import {IS_WINDOWS, IS_LINUX, getDownloadFileName} from './utils';
import {IS_WINDOWS, IS_LINUX, getDownloadFileName} from './utils.js';
import {IToolRelease} from '@actions/tool-cache';
const TOKEN = core.getInput('token');
@@ -222,7 +222,8 @@ export async function getManifest(): Promise<tc.IToolRelease[]> {
const message = err instanceof Error ? err.message : String(err);
// Fail loudly so the action doesn't exit 0 without installing Python.
throw new Error(
`Failed to fetch the Python versions manifest. The response was empty, truncated, or invalid, and all retries were exhausted. ${message}`
`Failed to fetch the Python versions manifest. The response was empty, truncated, or invalid, and all retries were exhausted. ${message}`,
{cause: err}
);
}
}
+12 -7
View File
@@ -1,18 +1,19 @@
import * as core from '@actions/core';
import * as finder from './find-python';
import * as finderPyPy from './find-pypy';
import * as finderGraalPy from './find-graalpy';
import * as finder from './find-python.js';
import * as finderPyPy from './find-pypy.js';
import * as finderGraalPy from './find-graalpy.js';
import * as path from 'path';
import * as os from 'os';
import {fileURLToPath} from 'url';
import fs from 'fs';
import {getCacheDistributor} from './cache-distributions/cache-factory';
import {getCacheDistributor} from './cache-distributions/cache-factory.js';
import {
isCacheFeatureAvailable,
logWarning,
IS_MAC,
getVersionInputFromFile,
getVersionsInputFromPlainFile
} from './utils';
} from './utils.js';
import {exec} from '@actions/exec';
function isPyPyVersion(versionSpec: string) {
@@ -29,7 +30,7 @@ async function installPipPackages(pipInstall: string) {
const installArgs = pipInstall.trim().split(/\s+/);
await exec('python', ['-m', 'pip', 'install', ...installArgs]);
core.info('Successfully installed pip packages');
} catch (error) {
} catch {
core.setFailed(
`Failed to install pip packages from "${pipInstall}". Please verify that the package names, versions, or requirements files provided are correct and installable, that the specified packages and versions can be resolved from PyPI or the configured package index, and that your network connection is stable and allows access to the package index.`
);
@@ -168,7 +169,11 @@ async function run() {
'The `python-version` input is not set. The version of Python currently in `PATH` will be used.'
);
}
const matchersPath = path.join(__dirname, '../..', '.github');
const matchersPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'../..',
'.github'
);
core.info(`##[add-matcher]${path.join(matchersPath, 'python.json')}`);
} catch (err) {
core.setFailed((err as Error).message);
+3 -3
View File
@@ -258,7 +258,7 @@ export function getVersionInputFromTomlFile(versionFile: string): string[] {
pyprojectFile = pyprojectFile.replace(/\r\n/g, '\n');
const pyprojectConfig = toml.parse(pyprojectFile);
let keys = [];
let keys: string[] = [];
if ('project' in pyprojectConfig) {
// standard project metadata (PEP 621)
@@ -267,7 +267,7 @@ export function getVersionInputFromTomlFile(versionFile: string): string[] {
// python poetry
keys = ['tool', 'poetry', 'dependencies', 'python'];
}
const versions = [];
const versions: string[] = [];
const version = extractValue(pyprojectConfig, keys);
if (version !== undefined) {
versions.push(version);
@@ -376,7 +376,7 @@ export function getVersionInputFromPipfileFile(versionFile: string): string[] {
} else {
keys.push('python_version');
}
const versions = [];
const versions: string[] = [];
const version = extractValue(pipfileConfig, keys);
if (version !== undefined) {
versions.push(version);
+2 -2
View File
@@ -3,7 +3,7 @@
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ES2022", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"module": "NodeNext", /* Specify module code generation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
@@ -60,5 +60,5 @@
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"exclude": ["node_modules", "**/*.test.ts"]
"exclude": ["node_modules", "**/*.test.ts", "jest.config.ts"]
}