mirror of
https://github.com/actions/setup-python.git
synced 2026-04-06 09:56:54 +00:00
Merge 8b57351c0f into 28f2168f4d
This commit is contained in:
commit
af99cdaefe
7 changed files with 441 additions and 41 deletions
20
.github/workflows/test-python.yml
vendored
20
.github/workflows/test-python.yml
vendored
|
|
@ -58,6 +58,26 @@ jobs:
|
||||||
- name: Run simple code
|
- name: Run simple code
|
||||||
run: python -c 'import math; print(math.factorial(5))'
|
run: python -c 'import math; print(math.factorial(5))'
|
||||||
|
|
||||||
|
setup-versions-via-mirror-input:
|
||||||
|
name: 'Setup via explicit mirror input: ${{ matrix.os }}'
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: setup-python with explicit mirror
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
mirror: https://raw.githubusercontent.com/actions/python-versions/main
|
||||||
|
|
||||||
|
- name: Run simple code
|
||||||
|
run: python -c 'import sys; print(sys.version)'
|
||||||
|
|
||||||
setup-versions-from-file:
|
setup-versions-from-file:
|
||||||
name: Setup ${{ matrix.python }} ${{ matrix.os }} version file
|
name: Setup ${{ matrix.python }} ${{ matrix.os }} version file
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,29 @@
|
||||||
import {
|
import {
|
||||||
getManifest,
|
getManifest,
|
||||||
getManifestFromRepo,
|
getManifestFromRepo,
|
||||||
getManifestFromURL
|
getManifestFromURL,
|
||||||
|
installCpythonFromRelease
|
||||||
} from '../src/install-python';
|
} from '../src/install-python';
|
||||||
import * as httpm from '@actions/http-client';
|
import * as httpm from '@actions/http-client';
|
||||||
import * as tc from '@actions/tool-cache';
|
import * as tc from '@actions/tool-cache';
|
||||||
|
|
||||||
jest.mock('@actions/http-client');
|
jest.mock('@actions/http-client');
|
||||||
jest.mock('@actions/tool-cache');
|
|
||||||
jest.mock('@actions/tool-cache', () => ({
|
jest.mock('@actions/tool-cache', () => ({
|
||||||
getManifestFromRepo: jest.fn()
|
getManifestFromRepo: jest.fn(),
|
||||||
|
downloadTool: jest.fn(),
|
||||||
|
extractTar: jest.fn(),
|
||||||
|
extractZip: jest.fn(),
|
||||||
|
HTTPError: class HTTPError extends Error {}
|
||||||
}));
|
}));
|
||||||
|
jest.mock('@actions/exec', () => ({
|
||||||
|
exec: jest.fn().mockResolvedValue(0)
|
||||||
|
}));
|
||||||
|
jest.mock('../src/utils', () => ({
|
||||||
|
...jest.requireActual('../src/utils'),
|
||||||
|
IS_WINDOWS: false,
|
||||||
|
IS_LINUX: false
|
||||||
|
}));
|
||||||
|
|
||||||
const mockManifest = [
|
const mockManifest = [
|
||||||
{
|
{
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
|
|
@ -26,11 +39,27 @@ const mockManifest = [
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('getManifest', () => {
|
function setInputs(values: Record<string, string | undefined>) {
|
||||||
|
for (const key of ['TOKEN', 'MIRROR', 'MIRROR-TOKEN']) {
|
||||||
|
delete process.env[`INPUT_${key}`];
|
||||||
|
}
|
||||||
|
for (const [k, v] of Object.entries(values)) {
|
||||||
|
if (v !== undefined) {
|
||||||
|
process.env[`INPUT_${k.toUpperCase()}`] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
setInputs({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
setInputs({});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getManifest', () => {
|
||||||
it('should return manifest from repo', async () => {
|
it('should return manifest from repo', async () => {
|
||||||
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue(mockManifest);
|
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue(mockManifest);
|
||||||
const manifest = await getManifest();
|
const manifest = await getManifest();
|
||||||
|
|
@ -50,10 +79,82 @@ describe('getManifest', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getManifestFromRepo', () => {
|
describe('getManifestFromRepo', () => {
|
||||||
it('should return manifest from repo', async () => {
|
it('default mirror calls getManifestFromRepo with actions/python-versions@main and token', async () => {
|
||||||
|
setInputs({token: 'TKN'});
|
||||||
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue(mockManifest);
|
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue(mockManifest);
|
||||||
const manifest = await getManifestFromRepo();
|
await getManifestFromRepo();
|
||||||
expect(manifest).toEqual(mockManifest);
|
expect(tc.getManifestFromRepo).toHaveBeenCalledWith(
|
||||||
|
'actions',
|
||||||
|
'python-versions',
|
||||||
|
'token TKN',
|
||||||
|
'main'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('custom raw mirror extracts owner/repo/branch and passes token', async () => {
|
||||||
|
setInputs({
|
||||||
|
token: 'TKN',
|
||||||
|
mirror: 'https://raw.githubusercontent.com/foo/bar/dev'
|
||||||
|
});
|
||||||
|
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue(mockManifest);
|
||||||
|
await getManifestFromRepo();
|
||||||
|
expect(tc.getManifestFromRepo).toHaveBeenCalledWith(
|
||||||
|
'foo',
|
||||||
|
'bar',
|
||||||
|
'token TKN',
|
||||||
|
'dev'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('custom non-GitHub mirror throws (caller falls through to URL fetch)', () => {
|
||||||
|
setInputs({mirror: 'https://mirror.example/py'});
|
||||||
|
expect(() => getManifestFromRepo()).toThrow(/not a GitHub repo URL/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mirror-token wins over token for the api.github.com call (getManifestFromRepo)', async () => {
|
||||||
|
setInputs({
|
||||||
|
token: 'TKN',
|
||||||
|
'mirror-token': 'MTOK',
|
||||||
|
mirror: 'https://raw.githubusercontent.com/foo/bar/main'
|
||||||
|
});
|
||||||
|
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue(mockManifest);
|
||||||
|
await getManifestFromRepo();
|
||||||
|
expect(tc.getManifestFromRepo).toHaveBeenCalledWith(
|
||||||
|
'foo',
|
||||||
|
'bar',
|
||||||
|
'token MTOK',
|
||||||
|
'main'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('token is used when mirror-token is empty (getManifestFromRepo)', async () => {
|
||||||
|
setInputs({
|
||||||
|
token: 'TKN',
|
||||||
|
mirror: 'https://raw.githubusercontent.com/foo/bar/main'
|
||||||
|
});
|
||||||
|
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue(mockManifest);
|
||||||
|
await getManifestFromRepo();
|
||||||
|
expect(tc.getManifestFromRepo).toHaveBeenCalledWith(
|
||||||
|
'foo',
|
||||||
|
'bar',
|
||||||
|
'token TKN',
|
||||||
|
'main'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trailing slashes in mirror URL are stripped', async () => {
|
||||||
|
setInputs({
|
||||||
|
token: 'TKN',
|
||||||
|
mirror: 'https://raw.githubusercontent.com/foo/bar/main/'
|
||||||
|
});
|
||||||
|
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue(mockManifest);
|
||||||
|
await getManifestFromRepo();
|
||||||
|
expect(tc.getManifestFromRepo).toHaveBeenCalledWith(
|
||||||
|
'foo',
|
||||||
|
'bar',
|
||||||
|
'token TKN',
|
||||||
|
'main'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -74,4 +175,116 @@ describe('getManifestFromURL', () => {
|
||||||
'Unable to get manifest from'
|
'Unable to get manifest from'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('fetches from {mirror}/versions-manifest.json (no auth header attached)', async () => {
|
||||||
|
setInputs({token: 'TKN', mirror: 'https://mirror.example/py'});
|
||||||
|
(httpm.HttpClient.prototype.getJson as jest.Mock).mockResolvedValue({
|
||||||
|
result: mockManifest
|
||||||
|
});
|
||||||
|
await getManifestFromURL();
|
||||||
|
expect(httpm.HttpClient.prototype.getJson).toHaveBeenCalledWith(
|
||||||
|
'https://mirror.example/py/versions-manifest.json'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mirror URL validation', () => {
|
||||||
|
it('throws on invalid URL when used', () => {
|
||||||
|
setInputs({mirror: 'not a url'});
|
||||||
|
expect(() => getManifestFromRepo()).toThrow(/Invalid 'mirror' URL/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('installCpythonFromRelease auth gating', () => {
|
||||||
|
const makeRelease = (downloadUrl: string) =>
|
||||||
|
({
|
||||||
|
version: '3.12.0',
|
||||||
|
stable: true,
|
||||||
|
release_url: '',
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
filename: 'python-3.12.0-linux-x64.tar.gz',
|
||||||
|
platform: 'linux',
|
||||||
|
platform_version: '',
|
||||||
|
arch: 'x64',
|
||||||
|
download_url: downloadUrl
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
function stubInstallExtract() {
|
||||||
|
(tc.downloadTool as jest.Mock).mockResolvedValue('/tmp/py.tgz');
|
||||||
|
(tc.extractTar as jest.Mock).mockResolvedValue('/tmp/extracted');
|
||||||
|
}
|
||||||
|
|
||||||
|
it('forwards token to github.com download URLs', async () => {
|
||||||
|
setInputs({token: 'TKN'});
|
||||||
|
stubInstallExtract();
|
||||||
|
await installCpythonFromRelease(
|
||||||
|
makeRelease(
|
||||||
|
'https://github.com/actions/python-versions/releases/download/3.12.0-x/python-3.12.0-linux-x64.tar.gz'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
expect(tc.downloadTool).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
undefined,
|
||||||
|
'token TKN'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forwards token to api.github.com URLs', async () => {
|
||||||
|
setInputs({token: 'TKN'});
|
||||||
|
stubInstallExtract();
|
||||||
|
await installCpythonFromRelease(
|
||||||
|
makeRelease('https://api.github.com/repos/x/y/tarball/main')
|
||||||
|
);
|
||||||
|
expect(tc.downloadTool).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
undefined,
|
||||||
|
'token TKN'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forwards token to objects.githubusercontent.com download URLs', async () => {
|
||||||
|
setInputs({token: 'TKN'});
|
||||||
|
stubInstallExtract();
|
||||||
|
await installCpythonFromRelease(
|
||||||
|
makeRelease('https://objects.githubusercontent.com/x/python.tar.gz')
|
||||||
|
);
|
||||||
|
expect(tc.downloadTool).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
undefined,
|
||||||
|
'token TKN'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does NOT forward token to non-GitHub download URLs', async () => {
|
||||||
|
setInputs({token: 'TKN'});
|
||||||
|
stubInstallExtract();
|
||||||
|
await installCpythonFromRelease(
|
||||||
|
makeRelease('https://cdn.example/py.tar.gz')
|
||||||
|
);
|
||||||
|
expect(tc.downloadTool).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forwards mirror-token to non-GitHub download URLs', async () => {
|
||||||
|
setInputs({
|
||||||
|
token: 'TKN',
|
||||||
|
'mirror-token': 'MTOK',
|
||||||
|
mirror: 'https://cdn.example'
|
||||||
|
});
|
||||||
|
stubInstallExtract();
|
||||||
|
await installCpythonFromRelease(
|
||||||
|
makeRelease('https://cdn.example/py.tar.gz')
|
||||||
|
);
|
||||||
|
expect(tc.downloadTool).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
undefined,
|
||||||
|
'token MTOK'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,14 @@ inputs:
|
||||||
description: "Set this option if you want the action to check for the latest available version that satisfies the version spec."
|
description: "Set this option if you want the action to check for the latest available version that satisfies the version spec."
|
||||||
default: false
|
default: false
|
||||||
token:
|
token:
|
||||||
description: "The token used to authenticate when fetching Python distributions from https://github.com/actions/python-versions. When running this action on github.com, the default value is sufficient. When running on GHES, you can pass a personal access token for github.com if you are experiencing rate limiting."
|
description: "The token used to authenticate when fetching Python distributions from https://github.com/actions/python-versions. When running this action on github.com, the default value is sufficient. When running on GHES, you can pass a personal access token for github.com if you are experiencing rate limiting. When 'mirror-token' is set, it takes precedence over this input."
|
||||||
default: ${{ github.server_url == 'https://github.com' && github.token || '' }}
|
default: ${{ github.server_url == 'https://github.com' && github.token || '' }}
|
||||||
|
mirror:
|
||||||
|
description: "Base URL for downloading Python distributions. Defaults to https://raw.githubusercontent.com/actions/python-versions/main. See docs/advanced-usage.md for details."
|
||||||
|
default: "https://raw.githubusercontent.com/actions/python-versions/main"
|
||||||
|
mirror-token:
|
||||||
|
description: "Token used to authenticate requests to 'mirror'. Takes precedence over 'token'."
|
||||||
|
required: false
|
||||||
cache-dependency-path:
|
cache-dependency-path:
|
||||||
description: "Used to specify the path to dependency files. Supports wildcards or a list of file names for caching multiple dependencies."
|
description: "Used to specify the path to dependency files. Supports wildcards or a list of file names for caching multiple dependencies."
|
||||||
update-environment:
|
update-environment:
|
||||||
|
|
|
||||||
83
dist/setup/index.js
vendored
83
dist/setup/index.js
vendored
|
|
@ -83007,7 +83007,7 @@ async function useCpythonVersion(version, architecture, updateEnvironment, check
|
||||||
if (freethreaded) {
|
if (freethreaded) {
|
||||||
msg.push(`Free threaded versions are only available for Python 3.13.0 and later.`);
|
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: ${installer.MANIFEST_URL}`);
|
msg.push(`The list of all available versions can be found here: ${installer.getManifestUrl()}`);
|
||||||
throw new Error(msg.join(os.EOL));
|
throw new Error(msg.join(os.EOL));
|
||||||
}
|
}
|
||||||
const _binDir = binDir(installDir);
|
const _binDir = binDir(installDir);
|
||||||
|
|
@ -83617,7 +83617,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||||
exports.MANIFEST_URL = void 0;
|
exports.getManifestUrl = getManifestUrl;
|
||||||
exports.findReleaseFromManifest = findReleaseFromManifest;
|
exports.findReleaseFromManifest = findReleaseFromManifest;
|
||||||
exports.getManifest = getManifest;
|
exports.getManifest = getManifest;
|
||||||
exports.getManifestFromRepo = getManifestFromRepo;
|
exports.getManifestFromRepo = getManifestFromRepo;
|
||||||
|
|
@ -83629,12 +83629,56 @@ const tc = __importStar(__nccwpck_require__(33472));
|
||||||
const exec = __importStar(__nccwpck_require__(95236));
|
const exec = __importStar(__nccwpck_require__(95236));
|
||||||
const httpm = __importStar(__nccwpck_require__(54844));
|
const httpm = __importStar(__nccwpck_require__(54844));
|
||||||
const utils_1 = __nccwpck_require__(71798);
|
const utils_1 = __nccwpck_require__(71798);
|
||||||
const TOKEN = core.getInput('token');
|
const DEFAULT_REPO_OWNER = 'actions';
|
||||||
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;
|
const DEFAULT_REPO_NAME = 'python-versions';
|
||||||
const MANIFEST_REPO_OWNER = 'actions';
|
const DEFAULT_REPO_BRANCH = 'main';
|
||||||
const MANIFEST_REPO_NAME = 'python-versions';
|
const DEFAULT_MIRROR = `https://raw.githubusercontent.com/${DEFAULT_REPO_OWNER}/${DEFAULT_REPO_NAME}/${DEFAULT_REPO_BRANCH}`;
|
||||||
const MANIFEST_REPO_BRANCH = 'main';
|
// Matches https://raw.githubusercontent.com/{owner}/{repo}/{branch}
|
||||||
exports.MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`;
|
const REPO_COORDS_RE = /^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/?$/;
|
||||||
|
function getToken() {
|
||||||
|
return core.getInput('token');
|
||||||
|
}
|
||||||
|
function getMirrorToken() {
|
||||||
|
return core.getInput('mirror-token');
|
||||||
|
}
|
||||||
|
function getMirror() {
|
||||||
|
const raw = (core.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;
|
||||||
|
}
|
||||||
async function findReleaseFromManifest(semanticVersionSpec, architecture, manifest) {
|
async function findReleaseFromManifest(semanticVersionSpec, architecture, manifest) {
|
||||||
if (!manifest) {
|
if (!manifest) {
|
||||||
manifest = await getManifest();
|
manifest = await getManifest();
|
||||||
|
|
@ -83675,15 +83719,28 @@ async function getManifest() {
|
||||||
return await getManifestFromURL();
|
return await getManifestFromURL();
|
||||||
}
|
}
|
||||||
function getManifestFromRepo() {
|
function getManifestFromRepo() {
|
||||||
core.debug(`Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`);
|
const coords = resolveRepoCoords();
|
||||||
return tc.getManifestFromRepo(MANIFEST_REPO_OWNER, MANIFEST_REPO_NAME, AUTH, MANIFEST_REPO_BRANCH);
|
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 tc.getManifestFromRepo(coords.owner, coords.repo, auth, coords.branch);
|
||||||
}
|
}
|
||||||
async function getManifestFromURL() {
|
async function getManifestFromURL() {
|
||||||
core.debug('Falling back to fetching the manifest using raw URL.');
|
core.debug('Falling back to fetching the manifest using raw URL.');
|
||||||
|
const manifestUrl = getManifestUrl();
|
||||||
const http = new httpm.HttpClient('tool-cache');
|
const http = new httpm.HttpClient('tool-cache');
|
||||||
const response = await http.getJson(exports.MANIFEST_URL);
|
const response = await http.getJson(manifestUrl);
|
||||||
if (!response.result) {
|
if (!response.result) {
|
||||||
throw new Error(`Unable to get manifest from ${exports.MANIFEST_URL}`);
|
throw new Error(`Unable to get manifest from ${manifestUrl}`);
|
||||||
}
|
}
|
||||||
return response.result;
|
return response.result;
|
||||||
}
|
}
|
||||||
|
|
@ -83720,7 +83777,7 @@ async function installCpythonFromRelease(release) {
|
||||||
let pythonPath = '';
|
let pythonPath = '';
|
||||||
try {
|
try {
|
||||||
const fileName = (0, utils_1.getDownloadFileName)(downloadUrl);
|
const fileName = (0, utils_1.getDownloadFileName)(downloadUrl);
|
||||||
pythonPath = await tc.downloadTool(downloadUrl, fileName, AUTH);
|
pythonPath = await tc.downloadTool(downloadUrl, fileName, authForUrl(downloadUrl));
|
||||||
core.info('Extract downloaded archive');
|
core.info('Extract downloaded archive');
|
||||||
let pythonExtractedFolder;
|
let pythonExtractedFolder;
|
||||||
if (utils_1.IS_WINDOWS) {
|
if (utils_1.IS_WINDOWS) {
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,41 @@ Such a requirement on side-effect could be because you don't want your composite
|
||||||
|
|
||||||
>**Note:** Python versions used in this action are generated in the [python-versions](https://github.com/actions/python-versions) repository. For macOS and Ubuntu images, python versions are built from the source code. For Windows, the python-versions repository uses installation executable. For more information please refer to the [python-versions](https://github.com/actions/python-versions) repository.
|
>**Note:** Python versions used in this action are generated in the [python-versions](https://github.com/actions/python-versions) repository. For macOS and Ubuntu images, python versions are built from the source code. For Windows, the python-versions repository uses installation executable. For more information please refer to the [python-versions](https://github.com/actions/python-versions) repository.
|
||||||
|
|
||||||
|
#### Using a custom mirror
|
||||||
|
|
||||||
|
The `mirror` input lets you point `setup-python` at a different location for CPython distributions — a personal fork of `actions/python-versions`, an internal mirror, or any server that hosts a `versions-manifest.json` at its root plus the tarballs referenced by that manifest. Default: `https://raw.githubusercontent.com/actions/python-versions/main`.
|
||||||
|
|
||||||
|
The manifest is resolved as follows:
|
||||||
|
|
||||||
|
- If `mirror` matches `https://raw.githubusercontent.com/{owner}/{repo}/{branch}`, the manifest is fetched via the GitHub REST API (giving you the 5000/hr authenticated rate limit when a token is present).
|
||||||
|
- Otherwise, the action fetches `{mirror}/versions-manifest.json` via a direct HTTP GET.
|
||||||
|
|
||||||
|
Authentication:
|
||||||
|
|
||||||
|
- `token` is forwarded **only** to `github.com` and hosts under `*.github.com` or `*.githubusercontent.com`. It is never sent to a custom mirror.
|
||||||
|
- `mirror-token` takes precedence over `token`: if `mirror-token` is set it is used for every authenticated request (manifest fetch and tarball downloads).
|
||||||
|
- If `mirror-token` is empty, `token` is used when the target URL is GitHub-owned.
|
||||||
|
- If neither applies, requests are anonymous.
|
||||||
|
|
||||||
|
Point at a personal fork of `actions/python-versions` (uses the default `token`, fetched via the GitHub API):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
mirror: https://raw.githubusercontent.com/my-org/python-versions/main
|
||||||
|
```
|
||||||
|
|
||||||
|
Point at an internal mirror with its own credential:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
mirror: https://python-mirror.internal.example
|
||||||
|
mirror-token: ${{ secrets.PYTHON_MIRROR_TOKEN }}
|
||||||
|
```
|
||||||
|
|
||||||
### PyPy
|
### PyPy
|
||||||
|
|
||||||
`setup-python` is able to configure **PyPy** from two sources:
|
`setup-python` is able to configure **PyPy** from two sources:
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ export async function useCpythonVersion(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
msg.push(
|
msg.push(
|
||||||
`The list of all available versions can be found here: ${installer.MANIFEST_URL}`
|
`The list of all available versions can be found here: ${installer.getManifestUrl()}`
|
||||||
);
|
);
|
||||||
throw new Error(msg.join(os.EOL));
|
throw new Error(msg.join(os.EOL));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,67 @@ import {ExecOptions} from '@actions/exec/lib/interfaces';
|
||||||
import {IS_WINDOWS, IS_LINUX, getDownloadFileName} from './utils';
|
import {IS_WINDOWS, IS_LINUX, getDownloadFileName} from './utils';
|
||||||
import {IToolRelease} from '@actions/tool-cache';
|
import {IToolRelease} from '@actions/tool-cache';
|
||||||
|
|
||||||
const TOKEN = core.getInput('token');
|
const DEFAULT_REPO_OWNER = 'actions';
|
||||||
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;
|
const DEFAULT_REPO_NAME = 'python-versions';
|
||||||
const MANIFEST_REPO_OWNER = 'actions';
|
const DEFAULT_REPO_BRANCH = 'main';
|
||||||
const MANIFEST_REPO_NAME = 'python-versions';
|
const DEFAULT_MIRROR = `https://raw.githubusercontent.com/${DEFAULT_REPO_OWNER}/${DEFAULT_REPO_NAME}/${DEFAULT_REPO_BRANCH}`;
|
||||||
const MANIFEST_REPO_BRANCH = 'main';
|
|
||||||
export const MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`;
|
// Matches https://raw.githubusercontent.com/{owner}/{repo}/{branch}
|
||||||
|
const REPO_COORDS_RE =
|
||||||
|
/^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/?$/;
|
||||||
|
|
||||||
|
function getToken(): string {
|
||||||
|
return core.getInput('token');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMirrorToken(): string {
|
||||||
|
return core.getInput('mirror-token');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMirror(): string {
|
||||||
|
const raw = (core.getInput('mirror') || DEFAULT_MIRROR)
|
||||||
|
.trim()
|
||||||
|
.replace(/\/+$/, '');
|
||||||
|
try {
|
||||||
|
new URL(raw);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Invalid 'mirror' URL: "${raw}"`);
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getManifestUrl(): string {
|
||||||
|
return `${getMirror()}/versions-manifest.json`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRepoCoords(): {
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
branch: string;
|
||||||
|
} | null {
|
||||||
|
const m = REPO_COORDS_RE.exec(getMirror());
|
||||||
|
return m ? {owner: m[1], repo: m[2], branch: m[3]} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function authForUrl(url: string): string | undefined {
|
||||||
|
const mirrorToken = getMirrorToken();
|
||||||
|
if (mirrorToken) return `token ${mirrorToken}`;
|
||||||
|
let host: string;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
export async function findReleaseFromManifest(
|
export async function findReleaseFromManifest(
|
||||||
semanticVersionSpec: string,
|
semanticVersionSpec: string,
|
||||||
|
|
@ -73,24 +128,34 @@ export async function getManifest(): Promise<tc.IToolRelease[]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getManifestFromRepo(): Promise<tc.IToolRelease[]> {
|
export function getManifestFromRepo(): Promise<tc.IToolRelease[]> {
|
||||||
|
const coords = resolveRepoCoords();
|
||||||
|
if (!coords) {
|
||||||
|
throw new Error(
|
||||||
|
`Mirror "${getMirror()}" is not a GitHub repo URL; falling back to raw URL fetch.`
|
||||||
|
);
|
||||||
|
}
|
||||||
core.debug(
|
core.debug(
|
||||||
`Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`
|
`Getting manifest from ${coords.owner}/${coords.repo}@${coords.branch}`
|
||||||
);
|
|
||||||
return tc.getManifestFromRepo(
|
|
||||||
MANIFEST_REPO_OWNER,
|
|
||||||
MANIFEST_REPO_NAME,
|
|
||||||
AUTH,
|
|
||||||
MANIFEST_REPO_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 tc.getManifestFromRepo(coords.owner, coords.repo, auth, coords.branch);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getManifestFromURL(): Promise<tc.IToolRelease[]> {
|
export async function getManifestFromURL(): Promise<tc.IToolRelease[]> {
|
||||||
core.debug('Falling back to fetching the manifest using raw URL.');
|
core.debug('Falling back to fetching the manifest using raw URL.');
|
||||||
|
|
||||||
|
const manifestUrl = getManifestUrl();
|
||||||
const http: httpm.HttpClient = new httpm.HttpClient('tool-cache');
|
const http: httpm.HttpClient = new httpm.HttpClient('tool-cache');
|
||||||
const response = await http.getJson<tc.IToolRelease[]>(MANIFEST_URL);
|
const response = await http.getJson<tc.IToolRelease[]>(manifestUrl);
|
||||||
if (!response.result) {
|
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;
|
return response.result;
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +195,11 @@ export async function installCpythonFromRelease(release: tc.IToolRelease) {
|
||||||
let pythonPath = '';
|
let pythonPath = '';
|
||||||
try {
|
try {
|
||||||
const fileName = getDownloadFileName(downloadUrl);
|
const fileName = getDownloadFileName(downloadUrl);
|
||||||
pythonPath = await tc.downloadTool(downloadUrl, fileName, AUTH);
|
pythonPath = await tc.downloadTool(
|
||||||
|
downloadUrl,
|
||||||
|
fileName,
|
||||||
|
authForUrl(downloadUrl)
|
||||||
|
);
|
||||||
core.info('Extract downloaded archive');
|
core.info('Extract downloaded archive');
|
||||||
let pythonExtractedFolder;
|
let pythonExtractedFolder;
|
||||||
if (IS_WINDOWS) {
|
if (IS_WINDOWS) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue