Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions __tests__/install-graalpy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,35 @@ describe('graalpyVersionToSemantic', () => {
});
});

describe('GraalPy release URL overrides', () => {
afterEach(() => {
jest.restoreAllMocks();
});

it('returns default URLs when no overrides are set', () => {
jest.spyOn(core, 'getInput').mockReturnValue('');
expect(installer.getGraalPyReleasesUrl()).toBe(
'https://api.github.com/repos/oracle/graalpython/releases'
);
expect(installer.getGraalPyPrereleaseReleasesUrl()).toBe(
'https://api.github.com/repos/graalvm/graal-languages-ea-builds/releases'
);
});

it('returns the graalpy-*-releases-url inputs when provided', () => {
const stable = 'https://mirror.example.com/graalpython/releases';
const prerelease =
'https://mirror.example.com/graal-languages-ea-builds/releases';
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'graalpy-releases-url') return stable;
if (name === 'graalpy-prerelease-releases-url') return prerelease;
return '';
});
expect(installer.getGraalPyReleasesUrl()).toBe(stable);
expect(installer.getGraalPyPrereleaseReleasesUrl()).toBe(prerelease);
});
});

describe('findRelease', () => {
const result = JSON.stringify(manifestData);
const releases = JSON.parse(result) as IGraalPyManifestRelease[];
Expand Down
22 changes: 22 additions & 0 deletions __tests__/install-pypy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ describe('pypyVersionToSemantic', () => {
});
});

describe('getPyPyVersionsUrl', () => {
afterEach(() => {
jest.restoreAllMocks();
});

it('returns the default URL when no override is set', () => {
jest.spyOn(core, 'getInput').mockReturnValue('');
expect(installer.getPyPyVersionsUrl()).toBe(
'https://downloads.python.org/pypy/versions.json'
);
});

it('returns the pypy-versions-url input when provided', () => {
const override = 'https://mirror.example.com/pypy/versions.json';
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'pypy-versions-url') return override;
return '';
});
expect(installer.getPyPyVersionsUrl()).toBe(override);
});
});

describe('findRelease', () => {
const result = JSON.stringify(manifestData);
const releases = JSON.parse(result) as IPyPyManifestRelease[];
Expand Down
88 changes: 87 additions & 1 deletion __tests__/install-python.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
getManifest,
getManifestFromRepo,
getManifestFromURL
getManifestFromURL,
getManifestUrl
} from '../src/install-python';
import * as httpm from '@actions/http-client';
import * as tc from '@actions/tool-cache';
import * as core from '@actions/core';

jest.mock('@actions/http-client');
jest.mock('@actions/tool-cache');
Expand Down Expand Up @@ -74,4 +76,88 @@ describe('getManifestFromURL', () => {
'Unable to get manifest from'
);
});

it('should fetch from the manifest-url override when provided', async () => {
const override = 'https://mirror.example.com/versions-manifest.json';
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'manifest-url') return override;
return '';
});
const getJson = jest
.spyOn(httpm.HttpClient.prototype, 'getJson')
.mockResolvedValue({result: mockManifest} as any);

await getManifestFromURL();
expect(getJson).toHaveBeenCalledWith(override);
});
});

describe('getManifestUrl', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('returns the default URL when no overrides are set', () => {
jest.spyOn(core, 'getInput').mockReturnValue('');
expect(getManifestUrl()).toBe(
'https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json'
);
});

it('returns the manifest-url input when provided', () => {
const override = 'https://mirror.example.com/versions-manifest.json';
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'manifest-url') return override;
return '';
});
expect(getManifestUrl()).toBe(override);
});

it('composes the URL from the manifest-repo-* inputs', () => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'manifest-repo-owner':
return 'my-org';
case 'manifest-repo-name':
return 'my-repo';
case 'manifest-repo-branch':
return 'release';
default:
return '';
}
});
expect(getManifestUrl()).toBe(
'https://raw.githubusercontent.com/my-org/my-repo/release/versions-manifest.json'
);
});
});

describe('getManifestFromRepo overrides', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('forwards the manifest-repo-* inputs to tc.getManifestFromRepo', async () => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'manifest-repo-owner':
return 'my-org';
case 'manifest-repo-name':
return 'my-repo';
case 'manifest-repo-branch':
return 'release';
default:
return '';
}
});
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue(mockManifest);

await getManifestFromRepo();
expect(tc.getManifestFromRepo).toHaveBeenCalledWith(
'my-org',
'my-repo',
undefined,
'release'
);
});
});
17 changes: 17 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,23 @@ inputs:
description: "Used to specify the version of pip to install with the Python. Supported format: major[.minor][.patch]."
pip-install:
description: "Used to specify the packages to install with pip after setting up Python. Can be a requirements file or package names."
manifest-url:
description: "URL of the CPython versions-manifest.json. Override when mirroring actions/python-versions to a self-hosted source."
manifest-repo-owner:
description: "Owner of the repository hosting the CPython versions-manifest.json. Used by the GitHub API fallback."
default: "actions"
manifest-repo-name:
description: "Name of the repository hosting the CPython versions-manifest.json."
default: "python-versions"
manifest-repo-branch:
description: "Branch of the repository hosting the CPython versions-manifest.json."
default: "main"
pypy-versions-url:
description: "URL of the PyPy versions.json. Override when mirroring downloads.python.org/pypy."
graalpy-releases-url:
description: "URL returning the GraalPy stable releases (mirror of api.github.com/repos/oracle/graalpython/releases)."
graalpy-prerelease-releases-url:
description: "URL returning the GraalPy pre-release builds (mirror of api.github.com/repos/graalvm/graal-languages-ea-builds/releases)."
outputs:
python-version:
description: "The installed Python or PyPy version. Useful when given a version range as input."
Expand Down
65 changes: 52 additions & 13 deletions dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54654,7 +54654,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: ${installer.MANIFEST_URL}`);
msg.push(`The list of all available versions can be found here: ${installer.getManifestUrl()}`);
throw new Error(msg.join(os.EOL));
}
const _binDir = binDir(installDir);
Expand Down Expand Up @@ -54821,6 +54821,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getGraalPyReleasesUrl = getGraalPyReleasesUrl;
exports.getGraalPyPrereleaseReleasesUrl = getGraalPyPrereleaseReleasesUrl;
exports.installGraalPy = installGraalPy;
exports.getAvailableGraalPyVersions = getAvailableGraalPyVersions;
exports.graalPyTagToVersion = graalPyTagToVersion;
Expand All @@ -54839,6 +54841,15 @@ const fs_1 = __importDefault(__nccwpck_require__(79896));
const utils_1 = __nccwpck_require__(71798);
const TOKEN = core.getInput('token');
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;
const DEFAULT_GRAALPY_RELEASES_URL = 'https://api.github.com/repos/oracle/graalpython/releases';
const DEFAULT_GRAALPY_PRERELEASE_RELEASES_URL = 'https://api.github.com/repos/graalvm/graal-languages-ea-builds/releases';
function getGraalPyReleasesUrl() {
return core.getInput('graalpy-releases-url') || DEFAULT_GRAALPY_RELEASES_URL;
}
function getGraalPyPrereleaseReleasesUrl() {
return (core.getInput('graalpy-prerelease-releases-url') ||
DEFAULT_GRAALPY_PRERELEASE_RELEASES_URL);
}
async function installGraalPy(graalpyVersion, architecture, allowPreReleases, releases) {
let downloadDir;
releases = releases ?? (await getAvailableGraalPyVersions());
Expand Down Expand Up @@ -54908,7 +54919,7 @@ async function getAvailableGraalPyVersions() {
/*
Get releases first.
*/
let url = 'https://api.github.com/repos/oracle/graalpython/releases';
let url = getGraalPyReleasesUrl();
const result = [];
do {
const response = await http.getJson(url, headers);
Expand All @@ -54921,8 +54932,7 @@ async function getAvailableGraalPyVersions() {
/*
Add pre-release builds.
*/
url =
'https://api.github.com/repos/graalvm/graal-languages-ea-builds/releases';
url = getGraalPyPrereleaseReleasesUrl();
do {
const response = await http.getJson(url, headers);
if (!response.result) {
Expand Down Expand Up @@ -55056,6 +55066,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.installPyPy = installPyPy;
exports.getPyPyVersionsUrl = getPyPyVersionsUrl;
exports.getAvailablePyPyVersions = getAvailablePyPyVersions;
exports.findRelease = findRelease;
exports.pypyVersionToSemantic = pypyVersionToSemantic;
Expand Down Expand Up @@ -55134,8 +55145,12 @@ async function installPyPy(pypyVersion, pythonVersion, architecture, allowPreRel
throw err;
}
}
const DEFAULT_PYPY_VERSIONS_URL = 'https://downloads.python.org/pypy/versions.json';
function getPyPyVersionsUrl() {
return core.getInput('pypy-versions-url') || DEFAULT_PYPY_VERSIONS_URL;
}
async function getAvailablePyPyVersions() {
const url = 'https://downloads.python.org/pypy/versions.json';
const url = getPyPyVersionsUrl();
const http = new httpm.HttpClient('tool-cache');
const response = await http.getJson(url);
if (!response.result) {
Expand Down Expand Up @@ -55265,6 +55280,7 @@ var __importStar = (this && this.__importStar) || (function () {
})();
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.MANIFEST_URL = void 0;
exports.getManifestUrl = getManifestUrl;
exports.findReleaseFromManifest = findReleaseFromManifest;
exports.getManifest = getManifest;
exports.getManifestFromRepo = getManifestFromRepo;
Expand All @@ -55278,10 +55294,29 @@ const httpm = __importStar(__nccwpck_require__(54844));
const utils_1 = __nccwpck_require__(71798);
const TOKEN = core.getInput('token');
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;
const MANIFEST_REPO_OWNER = 'actions';
const MANIFEST_REPO_NAME = 'python-versions';
const MANIFEST_REPO_BRANCH = 'main';
exports.MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`;
const DEFAULT_MANIFEST_REPO_OWNER = 'actions';
const DEFAULT_MANIFEST_REPO_NAME = 'python-versions';
const DEFAULT_MANIFEST_REPO_BRANCH = 'main';
function getManifestRepoOwner() {
return core.getInput('manifest-repo-owner') || DEFAULT_MANIFEST_REPO_OWNER;
}
function getManifestRepoName() {
return core.getInput('manifest-repo-name') || DEFAULT_MANIFEST_REPO_NAME;
}
function getManifestRepoBranch() {
return core.getInput('manifest-repo-branch') || DEFAULT_MANIFEST_REPO_BRANCH;
}
function getManifestUrl() {
const override = core.getInput('manifest-url');
if (override) {
return override;
}
return `https://raw.githubusercontent.com/${getManifestRepoOwner()}/${getManifestRepoName()}/${getManifestRepoBranch()}/versions-manifest.json`;
}
// Kept for backwards compatibility with consumers that import the constant.
// Note: this is evaluated at module load and does not reflect input overrides.
// Prefer getManifestUrl() in new code.
exports.MANIFEST_URL = getManifestUrl();
async function findReleaseFromManifest(semanticVersionSpec, architecture, manifest) {
if (!manifest) {
manifest = await getManifest();
Expand Down Expand Up @@ -55322,15 +55357,19 @@ async function getManifest() {
return await getManifestFromURL();
}
function getManifestFromRepo() {
core.debug(`Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`);
return tc.getManifestFromRepo(MANIFEST_REPO_OWNER, MANIFEST_REPO_NAME, AUTH, MANIFEST_REPO_BRANCH);
const owner = getManifestRepoOwner();
const name = getManifestRepoName();
const branch = getManifestRepoBranch();
core.debug(`Getting manifest from ${owner}/${name}@${branch}`);
return tc.getManifestFromRepo(owner, name, AUTH, branch);
}
async function getManifestFromURL() {
core.debug('Falling back to fetching the manifest using raw URL.');
const manifestUrl = getManifestUrl();
const http = new httpm.HttpClient('tool-cache');
const response = await http.getJson(exports.MANIFEST_URL);
const response = await http.getJson(manifestUrl);
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;
}
Expand Down
24 changes: 24 additions & 0 deletions docs/advanced-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,30 @@ Such a requirement on side-effect could be because you don't want your composite
`setup-python` is able to download GraalPy versions from the [official GraalPy repository](https://github.com/oracle/graalpython).
- All available versions that we can download are listed in [releases](https://github.com/oracle/graalpython/releases).

## Using a mirror for Python, PyPy or GraalPy

For runners that cannot reach `github.com` or `downloads.python.org` directly (for example, behind a firewall or on a self-hosted Gitea instance), the source URLs that `setup-python` queries can be redirected to mirrors via action inputs. The mirror is expected to serve the same JSON shapes as the upstream sources, and the URLs embedded in the mirrored manifests should point to mirrored download targets as well.

| Input | Default | Purpose |
| --- | --- | --- |
| `manifest-url` | `https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json` | Raw URL of the CPython `versions-manifest.json`. |
| `manifest-repo-owner` / `manifest-repo-name` / `manifest-repo-branch` | `actions` / `python-versions` / `main` | Used by the GitHub API fallback that fetches the manifest from a repository. |
| `pypy-versions-url` | `https://downloads.python.org/pypy/versions.json` | URL of the PyPy `versions.json`. |
| `graalpy-releases-url` | `https://api.github.com/repos/oracle/graalpython/releases` | Endpoint returning the GraalPy stable releases. |
| `graalpy-prerelease-releases-url` | `https://api.github.com/repos/graalvm/graal-languages-ea-builds/releases` | Endpoint returning the GraalPy pre-release builds. |

```yaml
- uses: actions/setup-python@v6
with:
python-version: '3.13'
manifest-url: 'https://gitea.example.com/mirror/python-versions/raw/branch/main/versions-manifest.json'
manifest-repo-owner: 'mirror'
manifest-repo-name: 'python-versions'
manifest-repo-branch: 'main'
```

> Only set the overrides you need. Each input falls back to the GitHub default when left empty, so a mirror that only handles CPython can leave the PyPy and GraalPy inputs unset.

## Hosted tool cache

GitHub hosted runners have a tool cache that comes with a few versions of Python + PyPy already installed. This tool cache helps speed up runs and tool setup by not requiring any new downloads. There is an environment variable called `RUNNER_TOOL_CACHE` on each runner that describes the location of the tool cache with Python and PyPy installed. `setup-python` works by taking a specific version of Python or PyPy from this tool cache and adding it to PATH.
Expand Down
2 changes: 1 addition & 1 deletion src/find-python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export async function useCpythonVersion(
);
}
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));
}
Expand Down
Loading
Loading