mirror of
https://gitea.com/actions/setup-python.git
synced 2026-09-09 16:20:17 +00:00
* feat: Add `mirror` and `mirror-token` inputs for custom Python distribution sources Users who need custom CPython builds (internal mirrors, GHES-hosted forks, special build configurations, compliance builds, air-gapped runners) could not previously point setup-python at anything other than actions/python-versions. Adds two new inputs: - `mirror`: base URL hosting versions-manifest.json and the Python distributions it references. Defaults to the existing https://raw.githubusercontent.com/actions/python-versions/main. - `mirror-token`: optional token used to authenticate requests to the mirror. If `mirror` is a raw.githubusercontent.com/{owner}/{repo}/{branch} URL, the manifest is fetched via the GitHub REST API (authenticated rate limit applies); otherwise the action falls back to a direct GET of {mirror}/versions-manifest.json. Token interaction ----------------- `token` is never forwarded to arbitrary hosts. Auth resolution is per-URL: 1. if mirror-token is set, use mirror-token 2. else if token is set AND the target host is github.com, *.github.com, or *.githubusercontent.com, use token 3. else send no auth Cases: Default (no inputs set) mirror = default raw.githubusercontent.com URL, mirror-token empty, token = github.token. → manifest API call and tarball downloads use `token`. Identical to prior behavior. Custom raw.githubusercontent.com mirror (e.g. personal fork) mirror-token empty, token = github.token. → manifest API call and tarball downloads use `token` (target hosts are GitHub-owned). Custom non-GitHub mirror, no mirror-token mirror-token empty, token = github.token. → manifest fetched via direct URL (no auth attached), tarball downloads use no auth. `token` is NOT forwarded to the custom host — this is the leak-prevention case. Custom non-GitHub mirror with mirror-token mirror-token set, token may be set. → manifest fetch and tarball downloads use `mirror-token`. Custom GitHub mirror with both tokens set mirror-token wins. Used for both the manifest API call and tarball downloads. * fix: address mirror review feedback - scope mirror-token to the mirror host and send it verbatim - route non-repo mirrors straight to the URL fetch instead of throwing - authenticate the manifest fetch - warn on slash branches, and on mirror with PyPy/GraalPy - memoize mirror validation - exercise the direct-URL path in the E2E job Addresses https://github.com/actions/setup-python/pull/1302#issuecomment-5202618946 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: correct mirror warnings, auth scoping, and integration coverage - only warn about PyPy/GraalPy mirror when a custom mirror is set; the action.yml default made the warning fire on every run - accept the refs/heads/{branch} raw URL form so it routes via the REST API instead of tripping the slash-branch warning - scope mirror-token to the full mirror origin (scheme+host+port) so it can't leak to a same-host http download_url - make an invalid mirror fatal on the auth path, matching getManifestUrl - fix warning/docs that wrongly claimed the raw fallback is anonymous - force a manifest fetch in the mirror integration job (check-latest) so it actually contacts the mirror instead of using the preinstalled cache --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
184 lines
6.0 KiB
TypeScript
184 lines
6.0 KiB
TypeScript
import * as core from '@actions/core';
|
|
import * as finder from './find-python.js';
|
|
import * as finderPyPy from './find-pypy.js';
|
|
import * as finderGraalPy from './find-graalpy.js';
|
|
import {isMirrorCustomized} from './install-python.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.js';
|
|
import {
|
|
isCacheFeatureAvailable,
|
|
logWarning,
|
|
IS_MAC,
|
|
getVersionInputFromFile,
|
|
getVersionsInputFromPlainFile
|
|
} from './utils.js';
|
|
|
|
function isPyPyVersion(versionSpec: string) {
|
|
return versionSpec.startsWith('pypy');
|
|
}
|
|
|
|
function isGraalPyVersion(versionSpec: string) {
|
|
return versionSpec.startsWith('graalpy');
|
|
}
|
|
|
|
// `mirror` only redirects CPython distributions. PyPy and GraalPy resolve from
|
|
// downloads.python.org and the GitHub releases API respectively, so warn rather
|
|
// than let the input look like it applied. Only warns when the user actually
|
|
// set a custom mirror: action.yml gives `mirror` a default, so a plain
|
|
// getInput() check would fire on every pypy-*/graalpy-* run.
|
|
function warnIfMirrorUnsupported(versionSpec: string) {
|
|
if (!isMirrorCustomized()) {
|
|
return;
|
|
}
|
|
const implementation = isPyPyVersion(versionSpec) ? 'PyPy' : 'GraalPy';
|
|
core.warning(
|
|
`The 'mirror' input only applies to CPython distributions and is ignored for ${implementation} ('${versionSpec}'), which is downloaded from its own upstream source.`
|
|
);
|
|
}
|
|
|
|
async function cacheDependencies(cache: string, pythonVersion: string) {
|
|
const cacheDependencyPath =
|
|
core.getInput('cache-dependency-path') || undefined;
|
|
const cacheDistributor = getCacheDistributor(
|
|
cache,
|
|
pythonVersion,
|
|
cacheDependencyPath
|
|
);
|
|
await cacheDistributor.restoreCache();
|
|
}
|
|
|
|
function resolveVersionInputFromDefaultFile(): string[] {
|
|
const couples: [string, (versionFile: string) => string[]][] = [
|
|
['.python-version', getVersionsInputFromPlainFile]
|
|
];
|
|
for (const [versionFile, _fn] of couples) {
|
|
logWarning(
|
|
`Neither 'python-version' nor 'python-version-file' inputs were supplied. Attempting to find '${versionFile}' file.`
|
|
);
|
|
if (fs.existsSync(versionFile)) {
|
|
return _fn(versionFile);
|
|
} else {
|
|
logWarning(`${versionFile} doesn't exist.`);
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function resolveVersionInput() {
|
|
let versions = core.getMultilineInput('python-version');
|
|
const versionFile = core.getInput('python-version-file');
|
|
|
|
if (versions.length) {
|
|
if (versionFile) {
|
|
core.warning(
|
|
'Both python-version and python-version-file inputs are specified, only python-version will be used.'
|
|
);
|
|
}
|
|
} else {
|
|
if (versionFile) {
|
|
if (!fs.existsSync(versionFile)) {
|
|
throw new Error(
|
|
`The specified python version file at: ${versionFile} doesn't exist.`
|
|
);
|
|
}
|
|
versions = getVersionInputFromFile(versionFile);
|
|
} else {
|
|
versions = resolveVersionInputFromDefaultFile();
|
|
}
|
|
}
|
|
|
|
return versions;
|
|
}
|
|
|
|
async function run() {
|
|
if (IS_MAC) {
|
|
process.env['AGENT_TOOLSDIRECTORY'] = '/Users/runner/hostedtoolcache';
|
|
}
|
|
|
|
if (process.env.AGENT_TOOLSDIRECTORY?.trim()) {
|
|
process.env['RUNNER_TOOL_CACHE'] = process.env['AGENT_TOOLSDIRECTORY'];
|
|
}
|
|
|
|
core.debug(
|
|
`Python is expected to be installed into ${process.env['RUNNER_TOOL_CACHE']}`
|
|
);
|
|
try {
|
|
const versions = resolveVersionInput();
|
|
const checkLatest = core.getBooleanInput('check-latest');
|
|
const allowPreReleases = core.getBooleanInput('allow-prereleases');
|
|
const freethreaded = core.getBooleanInput('freethreaded');
|
|
|
|
if (versions.length) {
|
|
let pythonVersion = '';
|
|
const arch: string = core.getInput('architecture') || os.arch();
|
|
const updateEnvironment = core.getBooleanInput('update-environment');
|
|
core.startGroup('Installed versions');
|
|
for (const version of versions) {
|
|
if (isPyPyVersion(version)) {
|
|
warnIfMirrorUnsupported(version);
|
|
const installed = await finderPyPy.findPyPyVersion(
|
|
version,
|
|
arch,
|
|
updateEnvironment,
|
|
checkLatest,
|
|
allowPreReleases
|
|
);
|
|
pythonVersion = `${installed.resolvedPyPyVersion}-${installed.resolvedPythonVersion}`;
|
|
core.info(
|
|
`Successfully set up PyPy ${installed.resolvedPyPyVersion} with Python (${installed.resolvedPythonVersion})`
|
|
);
|
|
} else if (isGraalPyVersion(version)) {
|
|
warnIfMirrorUnsupported(version);
|
|
const installed = await finderGraalPy.findGraalPyVersion(
|
|
version,
|
|
arch,
|
|
updateEnvironment,
|
|
checkLatest,
|
|
allowPreReleases
|
|
);
|
|
pythonVersion = `${installed}`;
|
|
core.info(`Successfully set up GraalPy ${installed}`);
|
|
} else {
|
|
if (version.startsWith('2')) {
|
|
core.warning(
|
|
'The support for python 2.7 was removed on June 19, 2023. Related issue: https://github.com/actions/setup-python/issues/672'
|
|
);
|
|
}
|
|
const installed = await finder.useCpythonVersion(
|
|
version,
|
|
arch,
|
|
updateEnvironment,
|
|
checkLatest,
|
|
allowPreReleases,
|
|
freethreaded
|
|
);
|
|
pythonVersion = installed.version;
|
|
core.info(`Successfully set up ${installed.impl} (${pythonVersion})`);
|
|
}
|
|
}
|
|
core.endGroup();
|
|
const cache = core.getInput('cache');
|
|
if (cache && isCacheFeatureAvailable()) {
|
|
await cacheDependencies(cache, pythonVersion);
|
|
}
|
|
} else {
|
|
core.warning(
|
|
'The `python-version` input is not set. The version of Python currently in `PATH` will be used.'
|
|
);
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
run();
|