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.
This commit is contained in:
Ludovic Henry 2026-04-06 00:59:40 +02:00
parent 8549b9f8f5
commit f30f2fee26
No known key found for this signature in database
7 changed files with 559 additions and 35 deletions

91
dist/setup/index.js vendored
View file

@ -98693,12 +98693,56 @@ function _unique(values) {
const TOKEN = getInput('token');
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;
const MANIFEST_REPO_OWNER = 'actions';
const MANIFEST_REPO_NAME = 'python-versions';
const MANIFEST_REPO_BRANCH = 'main';
const MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`;
const DEFAULT_REPO_OWNER = 'actions';
const DEFAULT_REPO_NAME = 'python-versions';
const DEFAULT_REPO_BRANCH = 'main';
const DEFAULT_MIRROR = `https://raw.githubusercontent.com/${DEFAULT_REPO_OWNER}/${DEFAULT_REPO_NAME}/${DEFAULT_REPO_BRANCH}`;
// Matches https://raw.githubusercontent.com/{owner}/{repo}/{branch}
const REPO_COORDS_RE = /^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/?$/;
function getToken() {
return getInput('token');
}
function getMirrorToken() {
return getInput('mirror-token');
}
function getMirror() {
const raw = (getInput('mirror') || DEFAULT_MIRROR)
.trim()
.replace(/\/+$/, '');
try {
new URL(raw);
}
catch {
throw new Error(`Invalid 'mirror' URL: "${raw}"`);
}
return raw;
}
function getManifestUrl() {
return `${getMirror()}/versions-manifest.json`;
}
function resolveRepoCoords() {
const m = REPO_COORDS_RE.exec(getMirror());
return m ? { owner: m[1], repo: m[2], branch: m[3] } : null;
}
function authForUrl(url) {
const mirrorToken = getMirrorToken();
if (mirrorToken)
return `token ${mirrorToken}`;
let host;
try {
host = new URL(url).host;
}
catch {
return undefined;
}
const token = getToken();
if (token &&
(host === 'github.com' ||
host.endsWith('.github.com') ||
host.endsWith('.githubusercontent.com')))
return `token ${token}`;
return undefined;
}
function getLinuxOsRelease() {
try {
const content = external_fs_namespaceObject.readFileSync('/etc/os-release', 'utf8');
@ -98846,15 +98890,28 @@ async function getManifest() {
}
}
function install_python_getManifestFromRepo() {
core_debug(`Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`);
return getManifestFromRepo(MANIFEST_REPO_OWNER, MANIFEST_REPO_NAME, AUTH, MANIFEST_REPO_BRANCH);
const coords = resolveRepoCoords();
if (!coords) {
throw new Error(`Mirror "${getMirror()}" is not a GitHub repo URL; falling back to raw URL fetch.`);
}
core_debug(`Getting manifest from ${coords.owner}/${coords.repo}@${coords.branch}`);
// api.github.com is a GitHub-owned URL. Prefer MIRROR_TOKEN (the user provided token), fall back to TOKEN.
const token = getToken();
const mirrorToken = getMirrorToken();
const auth = !mirrorToken
? !token
? undefined
: `token ${token}`
: `token ${mirrorToken}`;
return getManifestFromRepo(coords.owner, coords.repo, auth, coords.branch);
}
async function getManifestFromURL() {
core_debug('Falling back to fetching the manifest using raw URL.');
const manifestUrl = getManifestUrl();
const http = new lib_HttpClient('tool-cache');
const response = await http.getJson(MANIFEST_URL);
const response = await http.getJson(manifestUrl);
if (!response.result) {
throw new Error(`Unable to get manifest from ${MANIFEST_URL}`);
throw new Error(`Unable to get manifest from ${manifestUrl}`);
}
return response.result;
}
@ -98897,7 +98954,7 @@ async function installCpythonFromRelease(release) {
let pythonPath = '';
try {
const fileName = getDownloadFileName(downloadUrl);
pythonPath = await downloadTool(downloadUrl, fileName, AUTH);
pythonPath = await downloadTool(downloadUrl, fileName, authForUrl(downloadUrl));
info('Extract downloaded archive');
let pythonExtractedFolder;
if (utils_IS_WINDOWS) {
@ -99015,7 +99072,7 @@ async function useCpythonVersion(version, architecture, updateEnvironment, check
if (freethreaded) {
msg.push(`Free threaded versions are only available for Python 3.13.0 and later.`);
}
msg.push(`The list of all available versions can be found here: ${MANIFEST_URL}`);
msg.push(`The list of all available versions can be found here: ${getManifestUrl()}`);
throw new Error(msg.join(external_os_.EOL));
}
const _binDir = binDir(installDir);
@ -99417,8 +99474,8 @@ function findPyPyInstallDirForWindows(pythonVersion) {
const install_graalpy_TOKEN = getInput('token');
const install_graalpy_AUTH = !install_graalpy_TOKEN ? undefined : `token ${install_graalpy_TOKEN}`;
const TOKEN = getInput('token');
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;
async function installGraalPy(graalpyVersion, architecture, allowPreReleases, releases) {
let downloadDir;
releases = releases ?? (await getAvailableGraalPyVersions());
@ -99441,7 +99498,7 @@ async function installGraalPy(graalpyVersion, architecture, allowPreReleases, re
const downloadUrl = `${foundAsset.browser_download_url}`;
info(`Downloading GraalPy from "${downloadUrl}" ...`);
try {
const graalpyPath = await downloadTool(downloadUrl, undefined, install_graalpy_AUTH);
const graalpyPath = await downloadTool(downloadUrl, undefined, AUTH);
info('Extracting downloaded archive...');
if (utils_IS_WINDOWS) {
downloadDir = await extractZip(graalpyPath);
@ -99482,8 +99539,8 @@ async function installGraalPy(graalpyVersion, architecture, allowPreReleases, re
async function getAvailableGraalPyVersions() {
const http = new lib_HttpClient('tool-cache');
const headers = {};
if (install_graalpy_AUTH) {
headers.authorization = install_graalpy_AUTH;
if (AUTH) {
headers.authorization = AUTH;
}
/*
Get releases first.