mirror of
https://github.com/actions/setup-python.git
synced 2026-09-21 08:43:40 +00:00
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
This commit is contained in:
parent
0d1135ac81
commit
990142f1ec
9
.github/workflows/test-python.yml
vendored
9
.github/workflows/test-python.yml
vendored
@ -72,13 +72,16 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# The refs/heads/ form serves the same manifest as the default mirror but
|
# The refs/heads/ form serves the same manifest as the default mirror and
|
||||||
# deliberately does not match {owner}/{repo}/{branch}, so this exercises
|
# exercises the refs/heads/{branch} -> {branch} parsing on the API path.
|
||||||
# the direct-URL manifest fetch that the default coordinates skip.
|
# check-latest forces a manifest fetch even though 3.12 is preinstalled,
|
||||||
|
# so the job actually contacts the mirror instead of short-circuiting on
|
||||||
|
# the tool cache.
|
||||||
- name: setup-python with explicit mirror
|
- name: setup-python with explicit mirror
|
||||||
uses: ./
|
uses: ./
|
||||||
with:
|
with:
|
||||||
python-version: 3.12
|
python-version: 3.12
|
||||||
|
check-latest: true
|
||||||
mirror: https://raw.githubusercontent.com/actions/python-versions/refs/heads/main
|
mirror: https://raw.githubusercontent.com/actions/python-versions/refs/heads/main
|
||||||
|
|
||||||
- name: Run simple code
|
- name: Run simple code
|
||||||
|
|||||||
@ -86,6 +86,7 @@ const {
|
|||||||
getManifestFromRepo,
|
getManifestFromRepo,
|
||||||
getManifestFromURL,
|
getManifestFromURL,
|
||||||
resolveRepoCoords,
|
resolveRepoCoords,
|
||||||
|
isMirrorCustomized,
|
||||||
installCpythonFromRelease
|
installCpythonFromRelease
|
||||||
} = await import('../src/install-python.js');
|
} = await import('../src/install-python.js');
|
||||||
|
|
||||||
@ -152,6 +153,55 @@ describe('getManifestUrl', () => {
|
|||||||
// find-python.ts calls this while building the "version not found" message.
|
// find-python.ts calls this while building the "version not found" message.
|
||||||
expect(() => getManifestUrl()).toThrow(/Invalid 'mirror' URL/);
|
expect(() => getManifestUrl()).toThrow(/Invalid 'mirror' URL/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('treats an invalid mirror as fatal on the auth path too', async () => {
|
||||||
|
// getManifestUrl() throws on a bad mirror; the auth resolution must agree
|
||||||
|
// rather than swallow the error and quietly skip the mirror-token branch.
|
||||||
|
setInputs({'mirror-token': 'MTOK', mirror: 'not a url'});
|
||||||
|
(tc.downloadTool as jest.Mock<any>).mockResolvedValue('/tmp/py.tgz');
|
||||||
|
(tc.extractTar as jest.Mock<any>).mockResolvedValue('/tmp/extracted');
|
||||||
|
|
||||||
|
const release = {
|
||||||
|
version: '3.12.0',
|
||||||
|
stable: true,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
filename: 'python-3.12.0-linux-x64.tar.gz',
|
||||||
|
platform: 'linux',
|
||||||
|
arch: 'x64',
|
||||||
|
download_url: 'https://cdn.example/py.tar.gz'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
await expect(installCpythonFromRelease(release)).rejects.toThrow(
|
||||||
|
/Invalid 'mirror' URL/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isMirrorCustomized', () => {
|
||||||
|
it('is false when the mirror input is empty', () => {
|
||||||
|
expect(isMirrorCustomized()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is false when the mirror input equals the default', () => {
|
||||||
|
// action.yml gives `mirror` this exact default, so getInput() returns it on
|
||||||
|
// every run where the user did not set one. The PyPy/GraalPy warning must
|
||||||
|
// not fire in that case.
|
||||||
|
setInputs({mirror: DEFAULT_MIRROR});
|
||||||
|
expect(isMirrorCustomized()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is false when the mirror input is the default with trailing slashes', () => {
|
||||||
|
setInputs({mirror: `${DEFAULT_MIRROR}///`});
|
||||||
|
expect(isMirrorCustomized()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is true when the mirror input is a custom URL', () => {
|
||||||
|
setInputs({mirror: 'https://mirror.example/py'});
|
||||||
|
expect(isMirrorCustomized()).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('resolveRepoCoords', () => {
|
describe('resolveRepoCoords', () => {
|
||||||
@ -166,6 +216,22 @@ describe('resolveRepoCoords', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('parses the refs/heads/{branch} form to the bare branch without warning', () => {
|
||||||
|
setInputs({
|
||||||
|
mirror:
|
||||||
|
'https://raw.githubusercontent.com/actions/python-versions/refs/heads/main'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolveRepoCoords()).toEqual({
|
||||||
|
owner: 'actions',
|
||||||
|
repo: 'python-versions',
|
||||||
|
branch: 'main'
|
||||||
|
});
|
||||||
|
// refs/heads/main is a valid single branch, so it must route through the
|
||||||
|
// API path and never hit the slash-branch warning.
|
||||||
|
expect(core.warning).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('does not warn for a non-GitHub mirror', () => {
|
it('does not warn for a non-GitHub mirror', () => {
|
||||||
setInputs({mirror: 'https://mirror.example/py'});
|
setInputs({mirror: 'https://mirror.example/py'});
|
||||||
|
|
||||||
@ -223,6 +289,25 @@ describe('getManifestFromRepo mirror resolution', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolves the refs/heads/{branch} form to the bare branch for the API', async () => {
|
||||||
|
setInputs({
|
||||||
|
token: 'TKN',
|
||||||
|
mirror: 'https://raw.githubusercontent.com/foo/bar/refs/heads/main'
|
||||||
|
});
|
||||||
|
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue(mockManifest);
|
||||||
|
|
||||||
|
await getManifestFromRepo();
|
||||||
|
|
||||||
|
// The GitHub tree API takes a bare branch, so the refs/heads/ prefix must
|
||||||
|
// be stripped rather than passed through as part of the branch name.
|
||||||
|
expect(tc.getManifestFromRepo).toHaveBeenCalledWith(
|
||||||
|
'foo',
|
||||||
|
'bar',
|
||||||
|
'token TKN',
|
||||||
|
'main'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns null for a non-GitHub mirror so the caller uses the raw URL', () => {
|
it('returns null for a non-GitHub mirror so the caller uses the raw URL', () => {
|
||||||
setInputs({mirror: 'https://mirror.example/py'});
|
setInputs({mirror: 'https://mirror.example/py'});
|
||||||
expect(resolveRepoCoords()).toBeNull();
|
expect(resolveRepoCoords()).toBeNull();
|
||||||
@ -296,9 +381,13 @@ describe('getManifestFromURL mirror resolution', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sends token as a prefixed header for a GitHub-hosted raw manifest', async () => {
|
it('sends token as a prefixed header for a GitHub-hosted raw manifest', async () => {
|
||||||
|
// A slash-branch URL genuinely falls to the direct-URL path (it does not
|
||||||
|
// match the {owner}/{repo}/{branch} shape). raw.githubusercontent.com is a
|
||||||
|
// GitHub host, so the token is still attached here — contradicting any
|
||||||
|
// claim that the fallback fetch is anonymous.
|
||||||
setInputs({
|
setInputs({
|
||||||
token: 'TKN',
|
token: 'TKN',
|
||||||
mirror: 'https://raw.githubusercontent.com/foo/bar/refs/heads/main'
|
mirror: 'https://raw.githubusercontent.com/foo/bar/feature/riscv'
|
||||||
});
|
});
|
||||||
const getJson = jest.fn(async () => ({result: mockManifest}));
|
const getJson = jest.fn(async () => ({result: mockManifest}));
|
||||||
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({getJson}));
|
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({getJson}));
|
||||||
@ -306,7 +395,7 @@ describe('getManifestFromURL mirror resolution', () => {
|
|||||||
await getManifestFromURL();
|
await getManifestFromURL();
|
||||||
|
|
||||||
expect(getJson).toHaveBeenCalledWith(
|
expect(getJson).toHaveBeenCalledWith(
|
||||||
'https://raw.githubusercontent.com/foo/bar/refs/heads/main/versions-manifest.json',
|
'https://raw.githubusercontent.com/foo/bar/feature/riscv/versions-manifest.json',
|
||||||
{authorization: 'token TKN'}
|
{authorization: 'token TKN'}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -426,6 +515,41 @@ describe('installCpythonFromRelease auth gating', () => {
|
|||||||
).resolves.toBe('Basic dXNlcjpwYXNz');
|
).resolves.toBe('Basic dXNlcjpwYXNz');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('withholds mirror-token from a same-host download URL on a different scheme', async () => {
|
||||||
|
setInputs({
|
||||||
|
'mirror-token': 'Bearer MTOK',
|
||||||
|
mirror: 'https://cdn.example'
|
||||||
|
});
|
||||||
|
// The mirror is https, but the manifest points a download_url at http on
|
||||||
|
// the same host. Matching on origin (scheme + host + port) rather than host
|
||||||
|
// alone keeps the token from going out in cleartext.
|
||||||
|
await expect(
|
||||||
|
downloadAuthFor('http://cdn.example/py.tar.gz')
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('withholds mirror-token from a same-host download URL on a different port', async () => {
|
||||||
|
setInputs({
|
||||||
|
'mirror-token': 'Bearer MTOK',
|
||||||
|
mirror: 'https://cdn.example'
|
||||||
|
});
|
||||||
|
// Different port is a different origin, so the nominated credential must
|
||||||
|
// not follow.
|
||||||
|
await expect(
|
||||||
|
downloadAuthFor('https://cdn.example:8443/py.tar.gz')
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends mirror-token to a same-origin download URL on an explicit port', async () => {
|
||||||
|
setInputs({
|
||||||
|
'mirror-token': 'Bearer MTOK',
|
||||||
|
mirror: 'https://cdn.example:8443'
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
downloadAuthFor('https://cdn.example:8443/py.tar.gz')
|
||||||
|
).resolves.toBe('Bearer MTOK');
|
||||||
|
});
|
||||||
|
|
||||||
it('withholds mirror-token from an incidental GitHub host and uses token there', async () => {
|
it('withholds mirror-token from an incidental GitHub host and uses token there', async () => {
|
||||||
setInputs({
|
setInputs({
|
||||||
token: 'TKN',
|
token: 'TKN',
|
||||||
|
|||||||
67
dist/setup/index.js
vendored
67
dist/setup/index.js
vendored
@ -98697,8 +98697,11 @@ const DEFAULT_REPO_OWNER = 'actions';
|
|||||||
const DEFAULT_REPO_NAME = 'python-versions';
|
const DEFAULT_REPO_NAME = 'python-versions';
|
||||||
const DEFAULT_REPO_BRANCH = 'main';
|
const DEFAULT_REPO_BRANCH = 'main';
|
||||||
const DEFAULT_MIRROR = `https://raw.githubusercontent.com/${DEFAULT_REPO_OWNER}/${DEFAULT_REPO_NAME}/${DEFAULT_REPO_BRANCH}`;
|
const DEFAULT_MIRROR = `https://raw.githubusercontent.com/${DEFAULT_REPO_OWNER}/${DEFAULT_REPO_NAME}/${DEFAULT_REPO_BRANCH}`;
|
||||||
// Matches https://raw.githubusercontent.com/{owner}/{repo}/{branch}
|
// Matches https://raw.githubusercontent.com/{owner}/{repo}/{branch} and the
|
||||||
const REPO_COORDS_RE = /^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/?$/;
|
// equivalent https://raw.githubusercontent.com/{owner}/{repo}/refs/heads/{branch}
|
||||||
|
// form, capturing the bare branch in both. The GitHub tree API wants the bare
|
||||||
|
// branch, so the optional refs/heads/ prefix is consumed, not captured.
|
||||||
|
const REPO_COORDS_RE = /^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/(?:refs\/heads\/)?([^/]+)\/?$/;
|
||||||
function getToken() {
|
function getToken() {
|
||||||
return getInput('token');
|
return getInput('token');
|
||||||
}
|
}
|
||||||
@ -98731,13 +98734,26 @@ function getMirror() {
|
|||||||
function getManifestUrl() {
|
function getManifestUrl() {
|
||||||
return `${getMirror()}/versions-manifest.json`;
|
return `${getMirror()}/versions-manifest.json`;
|
||||||
}
|
}
|
||||||
function getMirrorHost() {
|
// Whether the user set `mirror` to something other than the built-in default.
|
||||||
try {
|
// action.yml gives `mirror` a default, so core.getInput('mirror') is never
|
||||||
return new URL(getMirror()).host;
|
// empty; callers that want "did the user opt into a custom mirror" must compare
|
||||||
}
|
// against DEFAULT_MIRROR rather than test for a falsy input. Normalizes the
|
||||||
catch {
|
// same way getMirror() does but never throws, so a warning path can call it
|
||||||
return undefined;
|
// even when the mirror is malformed.
|
||||||
}
|
function isMirrorCustomized() {
|
||||||
|
const input = getInput('mirror');
|
||||||
|
if (!input)
|
||||||
|
return false;
|
||||||
|
return input.trim().replace(/\/+$/, '') !== DEFAULT_MIRROR;
|
||||||
|
}
|
||||||
|
// Origin (scheme + host + port) of the mirror, so `mirror-token` is matched
|
||||||
|
// against the exact origin the user nominated. Comparing origin rather than
|
||||||
|
// host alone means a manifest served over https that points a download_url at
|
||||||
|
// http://same-host/... does NOT get the token — the scheme must match too.
|
||||||
|
// Deliberately not wrapped in try/catch: an invalid mirror throws here just as
|
||||||
|
// it does in getManifestUrl(), so both agree a bad mirror is fatal.
|
||||||
|
function getMirrorOrigin() {
|
||||||
|
return new URL(getMirror()).origin;
|
||||||
}
|
}
|
||||||
function isGitHubHost(host) {
|
function isGitHubHost(host) {
|
||||||
return (host === 'github.com' ||
|
return (host === 'github.com' ||
|
||||||
@ -98752,34 +98768,36 @@ function resolveRepoCoords() {
|
|||||||
const m = REPO_COORDS_RE.exec(mirror);
|
const m = REPO_COORDS_RE.exec(mirror);
|
||||||
if (m)
|
if (m)
|
||||||
return { owner: m[1], repo: m[2], branch: m[3] };
|
return { owner: m[1], repo: m[2], branch: m[3] };
|
||||||
// A raw.githubusercontent.com URL that doesn't parse is usually a branch
|
// A raw.githubusercontent.com URL that doesn't parse is a branch name
|
||||||
// name containing a slash, which is indistinguishable from a deeper path.
|
// containing a slash (e.g. .../{owner}/{repo}/feature/riscv), which is
|
||||||
// Fetching still works, just anonymously and without the API rate limit.
|
// indistinguishable from a deeper path. getMirror() succeeded above, so the
|
||||||
if (!warnedMirrors.has(mirror) &&
|
// URL is well-formed and this parse cannot throw.
|
||||||
getMirrorHost() === 'raw.githubusercontent.com') {
|
const isRawGitHub = new URL(mirror).host === 'raw.githubusercontent.com';
|
||||||
|
if (!warnedMirrors.has(mirror) && isRawGitHub) {
|
||||||
warnedMirrors.add(mirror);
|
warnedMirrors.add(mirror);
|
||||||
warning(`Could not parse owner/repo/branch out of mirror "${mirror}", so the manifest will be fetched by direct URL instead of the GitHub API. ` +
|
warning(`Could not parse owner/repo/branch out of mirror "${mirror}", so the manifest will be fetched directly from the raw URL instead of through the GitHub REST API. ` +
|
||||||
`Branch names containing '/' are not supported; use a branch without a slash to get the authenticated API rate limit.`);
|
`The request is still authenticated with your token, because raw.githubusercontent.com is a GitHub host. ` +
|
||||||
|
`Branch names containing '/' are not supported for the REST API path; use a branch without a slash if you want the manifest fetched through the API.`);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Mirror host with `mirror-token` set gets the token verbatim, so internal
|
// Mirror origin with `mirror-token` set gets the token verbatim, so internal
|
||||||
// mirrors can choose their own scheme (Bearer, Basic, ...). GitHub hosts get
|
// mirrors can choose their own scheme (Bearer, Basic, ...). GitHub hosts get
|
||||||
// `token ${token}`. Anything else is anonymous — neither credential is sent to
|
// `token ${token}`. Anything else is anonymous — neither credential is sent to
|
||||||
// a host the user didn't nominate.
|
// a host the user didn't nominate.
|
||||||
function authForUrl(url) {
|
function authForUrl(url) {
|
||||||
let host;
|
let parsed;
|
||||||
try {
|
try {
|
||||||
host = new URL(url).host;
|
parsed = new URL(url);
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const mirrorToken = getMirrorToken();
|
const mirrorToken = getMirrorToken();
|
||||||
if (mirrorToken && host === getMirrorHost())
|
if (mirrorToken && parsed.origin === getMirrorOrigin())
|
||||||
return mirrorToken;
|
return mirrorToken;
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (token && isGitHubHost(host))
|
if (token && isGitHubHost(parsed.host))
|
||||||
return `token ${token}`;
|
return `token ${token}`;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -103541,6 +103559,7 @@ function getCacheDistributor(packageManager, pythonVersion, cacheDependencyPath)
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function isPyPyVersion(versionSpec) {
|
function isPyPyVersion(versionSpec) {
|
||||||
return versionSpec.startsWith('pypy');
|
return versionSpec.startsWith('pypy');
|
||||||
}
|
}
|
||||||
@ -103549,9 +103568,11 @@ function isGraalPyVersion(versionSpec) {
|
|||||||
}
|
}
|
||||||
// `mirror` only redirects CPython distributions. PyPy and GraalPy resolve from
|
// `mirror` only redirects CPython distributions. PyPy and GraalPy resolve from
|
||||||
// downloads.python.org and the GitHub releases API respectively, so warn rather
|
// downloads.python.org and the GitHub releases API respectively, so warn rather
|
||||||
// than let the input look like it applied.
|
// 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) {
|
function warnIfMirrorUnsupported(versionSpec) {
|
||||||
if (!getInput('mirror')) {
|
if (!isMirrorCustomized()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const implementation = isPyPyVersion(versionSpec) ? 'PyPy' : 'GraalPy';
|
const implementation = isPyPyVersion(versionSpec) ? 'PyPy' : 'GraalPy';
|
||||||
|
|||||||
@ -562,7 +562,7 @@ Point at an internal mirror with its own credential:
|
|||||||
Caveats:
|
Caveats:
|
||||||
|
|
||||||
- `mirror` and `mirror-token` apply to **CPython only**. PyPy resolves from `downloads.python.org` and GraalPy from the GitHub releases API; both ignore these inputs, and the action warns if you set `mirror` alongside a `pypy-*` or `graalpy-*` version.
|
- `mirror` and `mirror-token` apply to **CPython only**. PyPy resolves from `downloads.python.org` and GraalPy from the GitHub releases API; both ignore these inputs, and the action warns if you set `mirror` alongside a `pypy-*` or `graalpy-*` version.
|
||||||
- Branch names containing `/` cannot be used with a `raw.githubusercontent.com` mirror, because `.../{owner}/{repo}/feature/riscv` is indistinguishable from a repo path. Such a mirror still works, but falls back to an anonymous direct GET with the 60/hr unauthenticated rate limit; the action warns when this happens. Use a branch without a slash to get the API path.
|
- Branch names containing `/` cannot be used with a `raw.githubusercontent.com` mirror, because `.../{owner}/{repo}/feature/riscv` is indistinguishable from a repo path. Such a mirror still works and is still authenticated with your `token` (raw.githubusercontent.com is a GitHub host), but the manifest is fetched directly from the raw URL rather than through the GitHub REST API; the action warns when this happens. Use a branch without a slash to get the REST API path. The `refs/heads/{branch}` form (for example `.../actions/python-versions/refs/heads/main`) is recognized and routes through the REST API.
|
||||||
|
|
||||||
### PyPy
|
### PyPy
|
||||||
|
|
||||||
|
|||||||
@ -14,9 +14,12 @@ const DEFAULT_REPO_NAME = 'python-versions';
|
|||||||
const DEFAULT_REPO_BRANCH = 'main';
|
const DEFAULT_REPO_BRANCH = 'main';
|
||||||
const DEFAULT_MIRROR = `https://raw.githubusercontent.com/${DEFAULT_REPO_OWNER}/${DEFAULT_REPO_NAME}/${DEFAULT_REPO_BRANCH}`;
|
const DEFAULT_MIRROR = `https://raw.githubusercontent.com/${DEFAULT_REPO_OWNER}/${DEFAULT_REPO_NAME}/${DEFAULT_REPO_BRANCH}`;
|
||||||
|
|
||||||
// Matches https://raw.githubusercontent.com/{owner}/{repo}/{branch}
|
// Matches https://raw.githubusercontent.com/{owner}/{repo}/{branch} and the
|
||||||
|
// equivalent https://raw.githubusercontent.com/{owner}/{repo}/refs/heads/{branch}
|
||||||
|
// form, capturing the bare branch in both. The GitHub tree API wants the bare
|
||||||
|
// branch, so the optional refs/heads/ prefix is consumed, not captured.
|
||||||
const REPO_COORDS_RE =
|
const REPO_COORDS_RE =
|
||||||
/^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/?$/;
|
/^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/(?:refs\/heads\/)?([^/]+)\/?$/;
|
||||||
|
|
||||||
function getToken(): string {
|
function getToken(): string {
|
||||||
return core.getInput('token');
|
return core.getInput('token');
|
||||||
@ -55,12 +58,26 @@ export function getManifestUrl(): string {
|
|||||||
return `${getMirror()}/versions-manifest.json`;
|
return `${getMirror()}/versions-manifest.json`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMirrorHost(): string | undefined {
|
// Whether the user set `mirror` to something other than the built-in default.
|
||||||
try {
|
// action.yml gives `mirror` a default, so core.getInput('mirror') is never
|
||||||
return new URL(getMirror()).host;
|
// empty; callers that want "did the user opt into a custom mirror" must compare
|
||||||
} catch {
|
// against DEFAULT_MIRROR rather than test for a falsy input. Normalizes the
|
||||||
return undefined;
|
// same way getMirror() does but never throws, so a warning path can call it
|
||||||
}
|
// even when the mirror is malformed.
|
||||||
|
export function isMirrorCustomized(): boolean {
|
||||||
|
const input = core.getInput('mirror');
|
||||||
|
if (!input) return false;
|
||||||
|
return input.trim().replace(/\/+$/, '') !== DEFAULT_MIRROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Origin (scheme + host + port) of the mirror, so `mirror-token` is matched
|
||||||
|
// against the exact origin the user nominated. Comparing origin rather than
|
||||||
|
// host alone means a manifest served over https that points a download_url at
|
||||||
|
// http://same-host/... does NOT get the token — the scheme must match too.
|
||||||
|
// Deliberately not wrapped in try/catch: an invalid mirror throws here just as
|
||||||
|
// it does in getManifestUrl(), so both agree a bad mirror is fatal.
|
||||||
|
function getMirrorOrigin(): string {
|
||||||
|
return new URL(getMirror()).origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGitHubHost(host: string): boolean {
|
function isGitHubHost(host: string): boolean {
|
||||||
@ -84,40 +101,40 @@ export function resolveRepoCoords(): {
|
|||||||
const m = REPO_COORDS_RE.exec(mirror);
|
const m = REPO_COORDS_RE.exec(mirror);
|
||||||
if (m) return {owner: m[1], repo: m[2], branch: m[3]};
|
if (m) return {owner: m[1], repo: m[2], branch: m[3]};
|
||||||
|
|
||||||
// A raw.githubusercontent.com URL that doesn't parse is usually a branch
|
// A raw.githubusercontent.com URL that doesn't parse is a branch name
|
||||||
// name containing a slash, which is indistinguishable from a deeper path.
|
// containing a slash (e.g. .../{owner}/{repo}/feature/riscv), which is
|
||||||
// Fetching still works, just anonymously and without the API rate limit.
|
// indistinguishable from a deeper path. getMirror() succeeded above, so the
|
||||||
if (
|
// URL is well-formed and this parse cannot throw.
|
||||||
!warnedMirrors.has(mirror) &&
|
const isRawGitHub = new URL(mirror).host === 'raw.githubusercontent.com';
|
||||||
getMirrorHost() === 'raw.githubusercontent.com'
|
if (!warnedMirrors.has(mirror) && isRawGitHub) {
|
||||||
) {
|
|
||||||
warnedMirrors.add(mirror);
|
warnedMirrors.add(mirror);
|
||||||
core.warning(
|
core.warning(
|
||||||
`Could not parse owner/repo/branch out of mirror "${mirror}", so the manifest will be fetched by direct URL instead of the GitHub API. ` +
|
`Could not parse owner/repo/branch out of mirror "${mirror}", so the manifest will be fetched directly from the raw URL instead of through the GitHub REST API. ` +
|
||||||
`Branch names containing '/' are not supported; use a branch without a slash to get the authenticated API rate limit.`
|
`The request is still authenticated with your token, because raw.githubusercontent.com is a GitHub host. ` +
|
||||||
|
`Branch names containing '/' are not supported for the REST API path; use a branch without a slash if you want the manifest fetched through the API.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirror host with `mirror-token` set gets the token verbatim, so internal
|
// Mirror origin with `mirror-token` set gets the token verbatim, so internal
|
||||||
// mirrors can choose their own scheme (Bearer, Basic, ...). GitHub hosts get
|
// mirrors can choose their own scheme (Bearer, Basic, ...). GitHub hosts get
|
||||||
// `token ${token}`. Anything else is anonymous — neither credential is sent to
|
// `token ${token}`. Anything else is anonymous — neither credential is sent to
|
||||||
// a host the user didn't nominate.
|
// a host the user didn't nominate.
|
||||||
function authForUrl(url: string): string | undefined {
|
function authForUrl(url: string): string | undefined {
|
||||||
let host: string;
|
let parsed: URL;
|
||||||
try {
|
try {
|
||||||
host = new URL(url).host;
|
parsed = new URL(url);
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mirrorToken = getMirrorToken();
|
const mirrorToken = getMirrorToken();
|
||||||
if (mirrorToken && host === getMirrorHost()) return mirrorToken;
|
if (mirrorToken && parsed.origin === getMirrorOrigin()) return mirrorToken;
|
||||||
|
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (token && isGitHubHost(host)) return `token ${token}`;
|
if (token && isGitHubHost(parsed.host)) return `token ${token}`;
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import * as core from '@actions/core';
|
|||||||
import * as finder from './find-python.js';
|
import * as finder from './find-python.js';
|
||||||
import * as finderPyPy from './find-pypy.js';
|
import * as finderPyPy from './find-pypy.js';
|
||||||
import * as finderGraalPy from './find-graalpy.js';
|
import * as finderGraalPy from './find-graalpy.js';
|
||||||
|
import {isMirrorCustomized} from './install-python.js';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import {fileURLToPath} from 'url';
|
import {fileURLToPath} from 'url';
|
||||||
@ -25,9 +26,11 @@ function isGraalPyVersion(versionSpec: string) {
|
|||||||
|
|
||||||
// `mirror` only redirects CPython distributions. PyPy and GraalPy resolve from
|
// `mirror` only redirects CPython distributions. PyPy and GraalPy resolve from
|
||||||
// downloads.python.org and the GitHub releases API respectively, so warn rather
|
// downloads.python.org and the GitHub releases API respectively, so warn rather
|
||||||
// than let the input look like it applied.
|
// 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) {
|
function warnIfMirrorUnsupported(versionSpec: string) {
|
||||||
if (!core.getInput('mirror')) {
|
if (!isMirrorCustomized()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const implementation = isPyPyVersion(versionSpec) ? 'PyPy' : 'GraalPy';
|
const implementation = isPyPyVersion(versionSpec) ? 'PyPy' : 'GraalPy';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user