Compare commits

..

6 Commits

Author SHA1 Message Date
dependabot[bot] c92a3353de Bump js-yaml from 3.15.1 to 3.15.2
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.15.1 to 3.15.2.
- [Changelog](https://github.com/nodeca/js-yaml/blob/3.15.2/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.15.1...3.15.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.15.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-08 22:32:59 +00:00
Ludovic Henry 337b0725b8 feat: Add mirror and mirror-token inputs for custom Python distribution sources (#1302)
* 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>
2026-09-08 17:31:48 -05:00
lmvysakh 751276eafc Updated error message (#1351)
Co-authored-by: lmvysakh <lmvysakh@github.com>
2026-08-20 11:47:16 -05:00
dependabot[bot] 66fcabf4d0 Bump actions/checkout from 7.0.0 to 7.0.1 (#1345)
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-19 09:58:13 -05:00
Haritha 9191ea1a55 fix: resolve npm audit high severity vulnerabilities (#1350)
Bumps transitive dependencies to patched versions:
- brace-expansion 5.0.8 -> 5.0.9 (GHSA-rgw5-rvv9-x895)
- js-yaml 3.15.0 -> 3.15.1 (GHSA-5p4m-2wfm-xmqj)
- undici 6.27.0 -> 6.28.0 (GHSA-8xcm-r25x-g524, GHSA-m8rv-5g2x-5cg5, GHSA-v3r7-h72x-cjcm)

Refreshes .licenses/ cache for the updated packages and rebuilds dist/.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-18 08:57:23 -05:00
Haritha 8549b9f8f5 fix: resolve npm audit high severity vulnerabilities (#1347)
- Upgrade fast-xml-parser to 5.10.1 (fixes GHSA-8r6m-32jq-jx6q)
- Add package.json override to force brace-expansion >=5.0.8 across
  all transitive dependencies (fixes GHSA-mh99-v99m-4gvg) without
  downgrading jest/ts-jest
- Refresh .licenses/npm cache to match updated dependency tree
- Rebuild dist/setup and dist/cache-save

npm audit now reports 0 vulnerabilities. Pre-existing test suite
failures (7 suites, ESM/jest teardown issue) verified unrelated to
this change - identical on unmodified main with node 24.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-03 11:18:19 -05:00
32 changed files with 3699 additions and 1629 deletions
+7 -7
View File
@@ -36,7 +36,7 @@ jobs:
- os: windows-11-arm
python-version: 3.13.14t
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
uses: ./
with:
@@ -66,7 +66,7 @@ jobs:
- os: windows-11-arm
python-version: 3.13.14t
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
id: cache-pipenv
uses: ./
@@ -106,7 +106,7 @@ jobs:
]
python-version: [3.13.14, 3.14.6]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install poetry
run: pipx install poetry
- name: Init pyproject.toml
@@ -141,7 +141,7 @@ jobs:
- os: windows-11-arm
python-version: 3.13.14t
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
uses: ./
with:
@@ -172,7 +172,7 @@ jobs:
- os: windows-11-arm
python-version: 3.13.14t
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
id: cache-pipenv
uses: ./
@@ -216,7 +216,7 @@ jobs:
- os: windows-11-arm
python-version: 3.13.14t
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
uses: ./
with:
@@ -247,7 +247,7 @@ jobs:
- os: windows-11-arm
python-version: 3.13.14t
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
uses: ./
with:
+7 -7
View File
@@ -42,7 +42,7 @@ jobs:
python-version: '3.10'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
uses: ./
with:
@@ -76,7 +76,7 @@ jobs:
- os: windows-11-arm
python-version: pypy-3.11-v7.x
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
id: cache-pipenv
uses: ./
@@ -131,7 +131,7 @@ jobs:
- os: windows-11-arm
python-version: '3.10'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install poetry
run: |
pipx install poetry
@@ -175,7 +175,7 @@ jobs:
- os: windows-11-arm
python-version: '3.10'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
uses: ./
with:
@@ -210,7 +210,7 @@ jobs:
- os: windows-11-arm
python-version: pypy-3.11-v7.x
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
id: cache-pipenv
uses: ./
@@ -259,7 +259,7 @@ jobs:
- os: windows-11-arm
python-version: '3.10'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
uses: ./
with:
@@ -290,7 +290,7 @@ jobs:
- os: windows-11-arm
python-version: '3.10'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
uses: ./
with:
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Run with setup-python 3.10.11
if: matrix.operating-system != 'windows-11-arm'
@@ -14,7 +14,7 @@ jobs:
steps:
- name: Checking out
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Publish
id: publish
uses: actions/publish-immutable-action@4bc8754ffc40f27910afb20287dbbbb675a4e978 # v0.0.4
+3 -3
View File
@@ -34,7 +34,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python ${{ matrix.graalpy }}
id: setup-python
@@ -85,7 +85,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python ${{ matrix.graalpy }}
id: setup-python
@@ -107,7 +107,7 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest, macos-15-intel]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup GraalPy and check latest
uses: ./
id: graalpy
+5 -5
View File
@@ -39,7 +39,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python ${{ matrix.pypy }}
id: setup-python
@@ -90,7 +90,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python ${{ matrix.pypy }}
id: setup-python
@@ -141,7 +141,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python ${{ matrix.pypy }}
id: setup-python
@@ -171,7 +171,7 @@ jobs:
macos-latest
]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup PyPy and check latest
uses: ./
with:
@@ -212,7 +212,7 @@ jobs:
macos-latest
]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup PyPy and check latest
uses: ./
with:
+15 -15
View File
@@ -33,7 +33,7 @@ jobs:
python: [3.13.14t, 3.14.6t]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python ${{ matrix.python }}
id: setup-python
@@ -70,7 +70,7 @@ jobs:
python: [3.13.14t, 3.14.6t]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: build-version-file ${{ matrix.python }}
run: echo ${{ matrix.python }} > .python-version
@@ -110,7 +110,7 @@ jobs:
python: [3.13.14t, 3.14.6t]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: build-version-file ${{ matrix.python }}
run: echo ${{ matrix.python }} > .python-version
@@ -148,7 +148,7 @@ jobs:
python: [3.13.14, 3.14.6]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: build-version-file ${{ matrix.python }}
run: |
@@ -189,7 +189,7 @@ jobs:
python: [3.13.14, 3.14.6]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: build-version-file ${{ matrix.python }}
run: |
@@ -230,7 +230,7 @@ jobs:
python: [3.13.14t, 3.14.6t, 3.15t-dev]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: build-tool-versions-file ${{ matrix.python }}
run: |
@@ -261,7 +261,7 @@ jobs:
python: [3.13t, 3.15t-dev]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: build-version-file ${{ matrix.python }}
run: |
@@ -301,7 +301,7 @@ jobs:
python: [3.13.14t, 3.14.6t, 3.15t-dev]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: build-version-file ${{ matrix.python }}
run: |
@@ -340,7 +340,7 @@ jobs:
]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python 3.15.0-beta.3
id: setup-python
@@ -377,7 +377,7 @@ jobs:
]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python 3.15t-dev
id: setup-python
@@ -414,7 +414,7 @@ jobs:
]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python 3.15t
id: setup-python
@@ -453,7 +453,7 @@ jobs:
python: [3.13.14t, 3.14.6t]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python ${{ matrix.python }}
id: setup-python
@@ -485,7 +485,7 @@ jobs:
]
python-version: [3.13t, 3.15t-dev]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python and check latest
id: setup-python
uses: ./
@@ -511,7 +511,7 @@ jobs:
macos-latest
]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup multiple Python versions
id: setup-python
uses: ./
@@ -542,7 +542,7 @@ jobs:
python: [3.13.14, 3.14.6, 3.15-dev, 3.15.0-beta.3]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python ${{ matrix.python }}
id: setup-python
uses: ./
+40 -14
View File
@@ -36,7 +36,7 @@ jobs:
python: 3.10.11
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python ${{ matrix.python }}
id: setup-python
@@ -61,6 +61,32 @@ jobs:
- name: Run simple code
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
# The refs/heads/ form serves the same manifest as the default mirror and
# exercises the refs/heads/{branch} -> {branch} parsing on the API path.
# 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
uses: ./
with:
python-version: 3.12
check-latest: true
mirror: https://raw.githubusercontent.com/actions/python-versions/refs/heads/main
- name: Run simple code
run: python -c 'import sys; print(sys.version)'
setup-versions-from-file:
name: Setup ${{ matrix.python }} ${{ matrix.os }} version file
runs-on: ${{ matrix.os }}
@@ -83,7 +109,7 @@ jobs:
python: 3.10.11
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: build-version-file ${{ matrix.python }}
run: echo ${{ matrix.python }} > .python-version
@@ -133,7 +159,7 @@ jobs:
python: 3.10.11
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: build-version-file ${{ matrix.python }}
run: echo ${{ matrix.python }} > .python-version
@@ -181,7 +207,7 @@ jobs:
python: 3.10.11
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: build-version-file ${{ matrix.python }}
run: |
@@ -234,7 +260,7 @@ jobs:
python: 3.10.11
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: build-version-file ${{ matrix.python }}
run: |
@@ -291,7 +317,7 @@ jobs:
python: pypy3.11-7.3.18
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: build-tool-versions-file ${{ matrix.python }}
run: |
@@ -325,7 +351,7 @@ jobs:
python: 3.10.11
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: build-version-file ${{ matrix.python }}
run: |
@@ -378,7 +404,7 @@ jobs:
python: 3.10.11
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: build-version-file ${{ matrix.python }}
run: |
@@ -427,7 +453,7 @@ jobs:
]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python 3.15.0-beta.3
id: setup-python
@@ -470,7 +496,7 @@ jobs:
]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python 3.15-dev
id: setup-python
@@ -507,7 +533,7 @@ jobs:
]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python 3.15
id: setup-python
@@ -549,7 +575,7 @@ jobs:
python: '3.10'
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup-python ${{ matrix.python }}
id: setup-python
@@ -584,7 +610,7 @@ jobs:
- os: windows-11-arm
python-version: '3.10'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python and check latest
uses: ./
with:
@@ -616,7 +642,7 @@ jobs:
macos-latest
]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python and check latest
uses: ./
with:
+2 -2
View File
@@ -1,9 +1,9 @@
---
name: "@nodable/entities"
version: 2.2.0
version: 3.0.0
type: npm
summary: Entity parser for XML, HTML, External entites with security and NCR control
homepage:
homepage:
license: mit
licenses:
- sources: README.md
-55
View File
@@ -1,55 +0,0 @@
---
name: balanced-match
version: 1.0.2
type: npm
summary: Match balanced character pairs, like "{" and "}"
homepage: https://github.com/juliangruber/balanced-match
license: mit
licenses:
- sources: LICENSE.md
text: |
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- sources: README.md
text: |-
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
notices: []
-55
View File
@@ -1,55 +0,0 @@
---
name: brace-expansion
version: 1.1.15
type: npm
summary: Brace expansion as known from sh/bash
homepage: https://github.com/juliangruber/brace-expansion
license: mit
licenses:
- sources: LICENSE
text: |
MIT License
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- sources: README.md
text: |-
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
notices: []
@@ -1,6 +1,6 @@
---
name: brace-expansion
version: 5.0.6
version: 5.0.9
type: npm
summary: Brace expansion as known from sh/bash
homepage:
-31
View File
@@ -1,31 +0,0 @@
---
name: concat-map
version: 0.0.1
type: npm
summary: concatenative mapdashery
homepage: https://github.com/substack/node-concat-map#readme
license: other
licenses:
- sources: LICENSE
text: |
This software is released under the MIT license:
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- sources: README.markdown
text: MIT
notices: []
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: fast-xml-parser
version: 5.9.3
version: 5.10.1
type: npm
summary: Validate XML, Parse XML, Build XML without C/C++ based libraries
homepage:
+2 -2
View File
@@ -1,10 +1,10 @@
---
name: is-unsafe
version: 1.0.1
version: 2.0.0
type: npm
summary: Zero-dependency, DOM-free, pure predicate for detecting unsafe strings across
HTML, XML, SVG, SQL, SHELL, and REGEX contexts
homepage:
homepage:
license: mit
licenses:
- sources: LICENSE
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: path-expression-matcher
version: 1.6.0
version: 1.6.2
type: npm
summary: Efficient path tracking and pattern matching for XML/JSON parsers
homepage: https://github.com/NaturalIntelligence/path-expression-matcher#readme
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: undici
version: 6.27.0
version: 6.28.0
type: npm
summary: An HTTP/1.1 client, written from scratch for Node.js
homepage: https://undici.nodejs.org
@@ -4,7 +4,7 @@ version: 0.1.0
type: npm
summary: Validates XML name productions — Name, NCName, QName, NMToken, NMTokens —
for XML 1.0 and 1.1
homepage:
homepage:
license: mit
licenses:
- sources: README.md
+35
View File
@@ -0,0 +1,35 @@
---
name: xml-naming
version: 0.3.0
type: npm
summary: Validates XML name productions — Name, NCName, QName, NMToken, NMTokens —
for XML 1.0 and 1.1
homepage:
license: mit
licenses:
- sources: LICENSE
text: |
MIT License
Copyright (c) 2026 Natural Intelligence
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- sources: README.md
text: MIT
notices: []
+3 -1
View File
@@ -276,7 +276,9 @@ virtualenvs.path = "{cache-dir}/virtualenvs" # /Users/patrick/Library/Caches/py
await expect(cacheDistributor.restoreCache()).rejects.toThrow(
`No file in ${process.cwd()} matched to [${cacheDependencyPath
.split('\n')
.join(',')}], make sure you have checked out the target repository`
.join(
','
)}] for ${packageManager}. Make sure you have checked out the target repository, or consider removing the cache step if there are no dependencies to cache.`
);
}
);
+600
View File
@@ -0,0 +1,600 @@
import {jest, describe, it, expect, beforeEach} from '@jest/globals';
// Inputs are read lazily by install-python.ts, so each test can set them
// before invoking the function under test.
const inputs: Record<string, string> = {};
// Mock @actions/http-client
jest.unstable_mockModule('@actions/http-client', () => ({
HttpClient: jest.fn().mockImplementation(() => ({
getJson: jest.fn()
})),
HttpClientError: class HttpClientError extends Error {},
HttpCodes: {
OK: 200,
NotFound: 404,
InternalServerError: 500
}
}));
// Mock @actions/cache (needed transitively by utils.ts)
jest.unstable_mockModule('@actions/cache', () => ({
saveCache: jest.fn(),
restoreCache: jest.fn(),
isFeatureAvailable: jest.fn()
}));
// Mock @actions/tool-cache
jest.unstable_mockModule('@actions/tool-cache', () => ({
getManifestFromRepo: jest.fn(),
downloadTool: jest.fn(),
extractTar: jest.fn(),
extractZip: jest.fn(),
HTTPError: class HTTPError extends Error {}
}));
// Mock @actions/core (needed by install-python.ts)
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
// Mock @actions/exec (needed by install-python.ts)
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
// Import real utils BEFORE mock registration to get real function references
const realUtils = await import('../src/utils.js');
// Pin the platform so the download/extract assertions below behave the same
// on every runner OS.
jest.unstable_mockModule('../src/utils.js', () => ({
...realUtils,
IS_WINDOWS: false,
IS_LINUX: false
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const httpm = await import('@actions/http-client');
const tc = await import('@actions/tool-cache');
const {
getManifestUrl,
getManifest,
getManifestFromRepo,
getManifestFromURL,
resolveRepoCoords,
isMirrorCustomized,
installCpythonFromRelease
} = await import('../src/install-python.js');
const DEFAULT_MIRROR =
'https://raw.githubusercontent.com/actions/python-versions/main';
const mockManifest = [
{
version: '1.0.0',
stable: true,
files: [
{
filename: 'tool-v1.0.0-linux-x64.tar.gz',
platform: 'linux',
arch: 'x64',
download_url: 'https://example.com/tool-v1.0.0-linux-x64.tar.gz'
}
]
}
];
function setInputs(values: Record<string, string>) {
Object.assign(inputs, values);
}
beforeEach(() => {
jest.resetAllMocks();
for (const key of Object.keys(inputs)) {
delete inputs[key];
}
(core.getInput as jest.Mock<any>).mockImplementation(
(name: string) => inputs[name] ?? ''
);
});
describe('getManifestUrl', () => {
it('defaults to the actions/python-versions manifest', () => {
expect(getManifestUrl()).toBe(`${DEFAULT_MIRROR}/versions-manifest.json`);
});
it('appends versions-manifest.json to a custom mirror', () => {
setInputs({mirror: 'https://mirror.example/py'});
expect(getManifestUrl()).toBe(
'https://mirror.example/py/versions-manifest.json'
);
});
it('strips trailing slashes from the mirror', () => {
setInputs({mirror: 'https://mirror.example/py///'});
expect(getManifestUrl()).toBe(
'https://mirror.example/py/versions-manifest.json'
);
});
it('throws on a mirror that is not a valid URL', () => {
setInputs({mirror: 'not a url'});
expect(() => getManifestUrl()).toThrow(/Invalid 'mirror' URL/);
});
it('keeps throwing the same error when called repeatedly', () => {
setInputs({mirror: 'not a url'});
expect(() => getManifestUrl()).toThrow(/Invalid 'mirror' URL/);
// Memoized, so the second call must not silently succeed or change shape —
// find-python.ts calls this while building the "version not found" message.
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', () => {
it('warns and returns null for a raw.githubusercontent.com mirror with a slash in the branch', () => {
setInputs({
mirror: 'https://raw.githubusercontent.com/foo/bar/feature/riscv'
});
expect(resolveRepoCoords()).toBeNull();
expect(core.warning).toHaveBeenCalledWith(
expect.stringMatching(/Branch names containing '\/' are not supported/)
);
});
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', () => {
setInputs({mirror: 'https://mirror.example/py'});
expect(resolveRepoCoords()).toBeNull();
expect(core.warning).not.toHaveBeenCalled();
});
});
describe('getManifestFromRepo mirror resolution', () => {
it('resolves the default mirror to actions/python-versions@main with token', async () => {
setInputs({token: 'TKN'});
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue(mockManifest);
await getManifestFromRepo();
expect(tc.getManifestFromRepo).toHaveBeenCalledWith(
'actions',
'python-versions',
'token TKN',
'main'
);
});
it('extracts owner/repo/branch from a custom raw.githubusercontent.com mirror', async () => {
setInputs({
token: 'TKN',
mirror: 'https://raw.githubusercontent.com/foo/bar/dev'
});
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue(mockManifest);
await getManifestFromRepo();
expect(tc.getManifestFromRepo).toHaveBeenCalledWith(
'foo',
'bar',
'token TKN',
'dev'
);
});
it('strips a trailing slash before extracting the branch', async () => {
setInputs({
token: 'TKN',
mirror: 'https://raw.githubusercontent.com/foo/bar/main/'
});
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue(mockManifest);
await getManifestFromRepo();
expect(tc.getManifestFromRepo).toHaveBeenCalledWith(
'foo',
'bar',
'token TKN',
'main'
);
});
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', () => {
setInputs({mirror: 'https://mirror.example/py'});
expect(resolveRepoCoords()).toBeNull();
expect(tc.getManifestFromRepo).not.toHaveBeenCalled();
});
it('prefers mirror-token over token for the GitHub API call', async () => {
setInputs({
token: 'TKN',
'mirror-token': 'MTOK',
mirror: 'https://raw.githubusercontent.com/foo/bar/main'
});
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue(mockManifest);
await getManifestFromRepo();
// The API requires the `token ` prefix, and naming a repo mirror is explicit intent to
// read that repo, so mirror-token is prefixed here even though downloads send it verbatim.
expect(tc.getManifestFromRepo).toHaveBeenCalledWith(
'foo',
'bar',
'token MTOK',
'main'
);
});
it('sends no auth when neither token nor mirror-token is set', async () => {
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue(mockManifest);
await getManifestFromRepo();
expect(tc.getManifestFromRepo).toHaveBeenCalledWith(
'actions',
'python-versions',
undefined,
'main'
);
});
});
describe('getManifestFromURL mirror resolution', () => {
it('fetches {mirror}/versions-manifest.json without auth when no mirror-token is set', async () => {
setInputs({token: 'TKN', mirror: 'https://mirror.example/py'});
const getJson = jest.fn(async () => ({result: mockManifest}));
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({getJson}));
await getManifestFromURL();
// `token` must not reach a non-GitHub mirror.
expect(getJson).toHaveBeenCalledWith(
'https://mirror.example/py/versions-manifest.json',
undefined
);
});
it('sends mirror-token verbatim on the manifest fetch', async () => {
setInputs({
token: 'TKN',
'mirror-token': 'Bearer MTOK',
mirror: 'https://mirror.example/py'
});
const getJson = jest.fn(async () => ({result: mockManifest}));
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({getJson}));
await getManifestFromURL();
expect(getJson).toHaveBeenCalledWith(
'https://mirror.example/py/versions-manifest.json',
{authorization: 'Bearer MTOK'}
);
});
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({
token: 'TKN',
mirror: 'https://raw.githubusercontent.com/foo/bar/feature/riscv'
});
const getJson = jest.fn(async () => ({result: mockManifest}));
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({getJson}));
await getManifestFromURL();
expect(getJson).toHaveBeenCalledWith(
'https://raw.githubusercontent.com/foo/bar/feature/riscv/versions-manifest.json',
{authorization: 'token TKN'}
);
});
});
describe('getManifest source routing', () => {
it('skips the GitHub API entirely for a non-GitHub mirror', async () => {
setInputs({mirror: 'https://mirror.example/py'});
const getJson = jest.fn(async () => ({result: mockManifest}));
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({getJson}));
await expect(getManifest()).resolves.toEqual(mockManifest);
// Routing straight to the URL fetch avoids 3 retries with backoff on a
// call that could never succeed.
expect(tc.getManifestFromRepo).not.toHaveBeenCalled();
expect(getJson).toHaveBeenCalledTimes(1);
});
it('uses the GitHub API for a repo mirror without touching the raw URL', async () => {
setInputs({token: 'TKN'});
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue(mockManifest);
const getJson = jest.fn(async () => ({result: mockManifest}));
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({getJson}));
await expect(getManifest()).resolves.toEqual(mockManifest);
expect(tc.getManifestFromRepo).toHaveBeenCalledTimes(1);
expect(getJson).not.toHaveBeenCalled();
});
});
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;
// Returns the auth argument tc.downloadTool was called with.
async function downloadAuthFor(downloadUrl: string) {
(tc.downloadTool as jest.Mock<any>).mockResolvedValue('/tmp/py.tgz');
(tc.extractTar as jest.Mock<any>).mockResolvedValue('/tmp/extracted');
await installCpythonFromRelease(makeRelease(downloadUrl));
const call = (tc.downloadTool as jest.Mock<any>).mock.calls[0];
expect(call[0]).toBe(downloadUrl);
return call[2];
}
it('forwards token to github.com download URLs', async () => {
setInputs({token: 'TKN'});
await expect(
downloadAuthFor(
'https://github.com/actions/python-versions/releases/download/3.12.0-x/python-3.12.0-linux-x64.tar.gz'
)
).resolves.toBe('token TKN');
});
it('forwards token to api.github.com download URLs', async () => {
setInputs({token: 'TKN'});
await expect(
downloadAuthFor('https://api.github.com/repos/x/y/tarball/main')
).resolves.toBe('token TKN');
});
it('forwards token to *.githubusercontent.com download URLs', async () => {
setInputs({token: 'TKN'});
await expect(
downloadAuthFor('https://objects.githubusercontent.com/x/python.tar.gz')
).resolves.toBe('token TKN');
});
it('does NOT forward token to a non-GitHub download URL', async () => {
setInputs({token: 'TKN', mirror: 'https://cdn.example'});
await expect(
downloadAuthFor('https://cdn.example/py.tar.gz')
).resolves.toBeUndefined();
});
it('does NOT forward token to a lookalike host', async () => {
setInputs({token: 'TKN', mirror: 'https://evil-github.com'});
await expect(
downloadAuthFor('https://evil-github.com/py.tar.gz')
).resolves.toBeUndefined();
});
it('forwards mirror-token verbatim to the mirror host', async () => {
setInputs({
token: 'TKN',
'mirror-token': 'Bearer MTOK',
mirror: 'https://cdn.example'
});
await expect(
downloadAuthFor('https://cdn.example/py.tar.gz')
).resolves.toBe('Bearer MTOK');
});
it('does not prefix or rewrite a mirror-token', async () => {
setInputs({
'mirror-token': 'Basic dXNlcjpwYXNz',
mirror: 'https://cdn.example'
});
await expect(
downloadAuthFor('https://cdn.example/py.tar.gz')
).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 () => {
setInputs({
token: 'TKN',
'mirror-token': 'MTOK',
mirror: 'https://cdn.example'
});
// A manifest hosted on the private mirror may still point release assets at
// GitHub; the private credential must not follow them there.
await expect(
downloadAuthFor('https://objects.githubusercontent.com/x/python.tar.gz')
).resolves.toBe('token TKN');
});
it('withholds mirror-token from a GitHub host when no token is set', async () => {
setInputs({'mirror-token': 'MTOK', mirror: 'https://cdn.example'});
await expect(
downloadAuthFor('https://objects.githubusercontent.com/x/python.tar.gz')
).resolves.toBeUndefined();
});
it('withholds mirror-token from a third host that is neither the mirror nor GitHub', async () => {
setInputs({
token: 'TKN',
'mirror-token': 'MTOK',
mirror: 'https://cdn.example'
});
await expect(
downloadAuthFor('https://other.example/py.tar.gz')
).resolves.toBeUndefined();
});
it('uses mirror-token for a GitHub mirror host when it is the nominated host', async () => {
setInputs({
token: 'TKN',
'mirror-token': 'token MTOK',
mirror: 'https://raw.githubusercontent.com/foo/bar/main'
});
await expect(
downloadAuthFor('https://raw.githubusercontent.com/foo/bar/py.tar.gz')
).resolves.toBe('token MTOK');
});
it('sends no auth when no tokens are configured', async () => {
await expect(
downloadAuthFor('https://github.com/o/r/releases/download/v/py.tar.gz')
).resolves.toBeUndefined();
});
});
+7 -1
View File
@@ -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."
default: false
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. This token is only sent to GitHub-owned hosts, never to a custom 'mirror'."
default: ${{ github.server_url == 'https://github.com' && github.token || '' }}
mirror:
description: "Base URL for downloading Python distributions (only applies to CPython; PyPy and GraalPy are unaffected). 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 the host named in 'mirror'. Sent verbatim as the Authorization header, so include a scheme if your mirror needs one (e.g. 'Bearer <token>')."
required: false
cache-dependency-path:
description: "Used to specify the path to dependency files. Supports wildcards or a list of file names for caching multiple dependencies."
update-environment:
+1175 -605
View File
File diff suppressed because it is too large Load Diff
+1510 -710
View File
File diff suppressed because it is too large Load Diff
+40
View File
@@ -524,6 +524,46 @@ 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.
#### 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 is decided by the host of each request, so neither credential reaches a server you did not nominate:
- Requests to the host named in `mirror` use `mirror-token`, sent **verbatim** as the `Authorization` header. Include a scheme if your mirror expects one — `Bearer <token>`, `Basic <base64>`, or `token <token>` for a GitHub host. This covers both the manifest fetch and the tarball downloads.
- Requests to `github.com`, `*.github.com`, or `*.githubusercontent.com` use `token`, sent as `token <token>`. A manifest that points its `download_url` at a GitHub host therefore keeps working without `mirror-token` being leaked to it.
- Any other host is requested anonymously.
- One exception: when `mirror` is a GitHub repo URL, the manifest is fetched from `api.github.com`, and `mirror-token` is preferred there (with the `token ` prefix the API requires) because naming a repo mirror is an explicit instruction to read that repo.
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 }}
```
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.
- 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
`setup-python` is able to configure **PyPy** from two sources:
+41 -83
View File
@@ -68,22 +68,6 @@
"minimatch": "^3.0.4"
}
},
"node_modules/@actions/cache/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/@actions/cache/node_modules/brace-expansion": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@actions/cache/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -1615,9 +1599,9 @@
}
},
"node_modules/@nodable/entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz",
"integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==",
"funding": [
{
"type": "github",
@@ -2679,15 +2663,15 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/browserslist": {
@@ -2961,12 +2945,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -3583,9 +3561,9 @@
}
},
"node_modules/fast-xml-parser": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.9.3.tgz",
"integrity": "sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==",
"version": "5.10.1",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz",
"integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==",
"funding": [
{
"type": "github",
@@ -3594,17 +3572,32 @@
],
"license": "MIT",
"dependencies": {
"@nodable/entities": "^2.2.0",
"@nodable/entities": "^3.0.0",
"fast-xml-builder": "^1.2.0",
"is-unsafe": "^1.0.1",
"path-expression-matcher": "^1.5.0",
"is-unsafe": "^2.0.0",
"path-expression-matcher": "^1.6.2",
"strnum": "^2.4.1",
"xml-naming": "^0.1.0"
"xml-naming": "^0.3.0"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fast-xml-parser/node_modules/xml-naming": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz",
"integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/fb-watchman": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
@@ -3814,23 +3807,6 @@
"node": ">=10.13.0"
}
},
"node_modules/glob/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
@@ -4072,9 +4048,9 @@
}
},
"node_modules/is-unsafe": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz",
"integrity": "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz",
"integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==",
"funding": [
{
"type": "github",
@@ -4765,9 +4741,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz",
"integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==",
"version": "3.15.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz",
"integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5190,9 +5166,9 @@
}
},
"node_modules/path-expression-matcher": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.0.tgz",
"integrity": "sha512-e5y7RCLHKjemsgQ4eqGJtPyr10ILz25HO7flzxhTV8bgvd5yHx98DGtCAtbVW9f2TqnYI/gEVZd+vz7snrdPTw==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz",
"integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==",
"funding": [
{
"type": "github",
@@ -5844,24 +5820,6 @@
"node": ">=8"
}
},
"node_modules/test-exclude/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/test-exclude/node_modules/brace-expansion": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/test-exclude/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -6080,9 +6038,9 @@
}
},
"node_modules/undici": {
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
"version": "6.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
"integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
+3
View File
@@ -8,6 +8,9 @@
"engines": {
"node": ">=24.0.0"
},
"overrides": {
"brace-expansion": "^5.0.8"
},
"scripts": {
"build": "ncc build -o dist/setup src/setup-python.ts && ncc build -o dist/cache-save src/cache-save.ts",
"format": "prettier --no-error-on-unmatched-pattern --write \"**/*.{ts,yml,yaml}\"",
+1 -1
View File
@@ -60,7 +60,7 @@ abstract class CacheDistributor {
.join(',')} or ${CACHE_DEPENDENCY_BACKUP_PATH}`
: this.cacheDependencyPath.split('\n').join(',');
throw new Error(
`No file in ${process.cwd()} matched to [${file}], make sure you have checked out the target repository`
`No file in ${process.cwd()} matched to [${file}] for ${this.packageManager}. Make sure you have checked out the target repository, or consider removing the cache step if there are no dependencies to cache.`
);
}
+1 -1
View File
@@ -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));
}
+177 -24
View File
@@ -9,12 +9,135 @@ import * as semver from 'semver';
import {IS_WINDOWS, IS_LINUX, getDownloadFileName} from './utils.js';
import {IToolRelease} from '@actions/tool-cache';
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';
export 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} 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 =
/^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/(?:refs\/heads\/)?([^/]+)\/?$/;
function getToken(): string {
return core.getInput('token');
}
function getMirrorToken(): string {
return core.getInput('mirror-token');
}
// Memoized per raw input value so the mirror is validated once per run rather
// than on every call. `getManifestUrl()` is also used to build the "version not
// found" message in find-python.ts, where re-validating would replace the real
// cause with an invalid-mirror error.
const mirrorCache = new Map<string, {url: string} | {error: Error}>();
function getMirror(): string {
const input = core.getInput('mirror') || DEFAULT_MIRROR;
let resolved = mirrorCache.get(input);
if (!resolved) {
const url = input.trim().replace(/\/+$/, '');
try {
new URL(url);
resolved = {url};
} catch {
resolved = {error: new Error(`Invalid 'mirror' URL: "${url}"`)};
}
mirrorCache.set(input, resolved);
}
if ('error' in resolved) throw resolved.error;
return resolved.url;
}
export function getManifestUrl(): string {
return `${getMirror()}/versions-manifest.json`;
}
// Whether the user set `mirror` to something other than the built-in default.
// action.yml gives `mirror` a default, so core.getInput('mirror') is never
// 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
// 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 {
return (
host === 'github.com' ||
host.endsWith('.github.com') ||
host.endsWith('.githubusercontent.com')
);
}
// Warned at most once per distinct mirror; resolveRepoCoords() is called from
// several paths within a single run.
const warnedMirrors = new Set<string>();
export function resolveRepoCoords(): {
owner: string;
repo: string;
branch: string;
} | null {
const mirror = getMirror();
const m = REPO_COORDS_RE.exec(mirror);
if (m) return {owner: m[1], repo: m[2], branch: m[3]};
// A raw.githubusercontent.com URL that doesn't parse is a branch name
// containing a slash (e.g. .../{owner}/{repo}/feature/riscv), which is
// indistinguishable from a deeper path. getMirror() succeeded above, so the
// URL is well-formed and this parse cannot throw.
const isRawGitHub = new URL(mirror).host === 'raw.githubusercontent.com';
if (!warnedMirrors.has(mirror) && isRawGitHub) {
warnedMirrors.add(mirror);
core.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. ` +
`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;
}
// Mirror origin with `mirror-token` set gets the token verbatim, so internal
// mirrors can choose their own scheme (Bearer, Basic, ...). GitHub hosts get
// `token ${token}`. Anything else is anonymous — neither credential is sent to
// a host the user didn't nominate.
function authForUrl(url: string): string | undefined {
let parsed: URL;
try {
parsed = new URL(url);
} catch {
return undefined;
}
const mirrorToken = getMirrorToken();
if (mirrorToken && parsed.origin === getMirrorOrigin()) return mirrorToken;
const token = getToken();
if (token && isGitHubHost(parsed.host)) return `token ${token}`;
return undefined;
}
interface LinuxOsRelease {
id: string;
@@ -205,15 +328,24 @@ async function fetchValidManifest(
}
export async function getManifest(): Promise<tc.IToolRelease[]> {
try {
return await fetchValidManifest('the GitHub API', getManifestFromRepo);
} catch (err) {
core.debug('Fetching the manifest via the API failed.');
if (err instanceof Error) {
core.debug(err.message);
} else {
core.debug('An unexpected error occurred while fetching the manifest.');
// Only GitHub repo mirrors can be fetched via the API. Checking up front
// avoids burning MANIFEST_FETCH_MAX_ATTEMPTS with backoff on a throw that
// could never succeed.
if (resolveRepoCoords()) {
try {
return await fetchValidManifest('the GitHub API', getManifestFromRepo);
} catch (err) {
core.debug('Fetching the manifest via the API failed.');
if (err instanceof Error) {
core.debug(err.message);
} else {
core.debug('An unexpected error occurred while fetching the manifest.');
}
}
} else {
core.debug(
`Mirror "${getMirror()}" is not a GitHub repo URL; fetching the manifest by URL.`
);
}
try {
@@ -229,24 +361,41 @@ export async function getManifest(): 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(
`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
`Getting manifest from ${coords.owner}/${coords.repo}@${coords.branch}`
);
// This only runs for GitHub repo mirrors, where `mirror-token` is the user's
// explicit intent for that repo. The target is always api.github.com, which
// requires the `token ` prefix, so the host rule in authForUrl() doesn't
// apply here.
const token = getToken();
const mirrorToken = getMirrorToken();
const auth = mirrorToken
? `token ${mirrorToken}`
: token
? `token ${token}`
: undefined;
return tc.getManifestFromRepo(coords.owner, coords.repo, auth, coords.branch);
}
export async function getManifestFromURL(): Promise<tc.IToolRelease[]> {
core.debug('Falling back to fetching the manifest using raw URL.');
const manifestUrl = getManifestUrl();
const http: httpm.HttpClient = new httpm.HttpClient('tool-cache');
const response = await http.getJson<tc.IToolRelease[]>(MANIFEST_URL);
const auth = authForUrl(manifestUrl);
const response = await http.getJson<tc.IToolRelease[]>(
manifestUrl,
auth ? {authorization: auth} : undefined
);
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;
}
@@ -291,7 +440,11 @@ export async function installCpythonFromRelease(release: tc.IToolRelease) {
let pythonPath = '';
try {
const fileName = getDownloadFileName(downloadUrl);
pythonPath = await tc.downloadTool(downloadUrl, fileName, AUTH);
pythonPath = await tc.downloadTool(
downloadUrl,
fileName,
authForUrl(downloadUrl)
);
core.info('Extract downloaded archive');
let pythonExtractedFolder;
if (IS_WINDOWS) {
+18
View File
@@ -2,6 +2,7 @@ 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';
@@ -23,6 +24,21 @@ 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;
@@ -102,6 +118,7 @@ async function run() {
core.startGroup('Installed versions');
for (const version of versions) {
if (isPyPyVersion(version)) {
warnIfMirrorUnsupported(version);
const installed = await finderPyPy.findPyPyVersion(
version,
arch,
@@ -114,6 +131,7 @@ async function run() {
`Successfully set up PyPy ${installed.resolvedPyPyVersion} with Python (${installed.resolvedPythonVersion})`
);
} else if (isGraalPyVersion(version)) {
warnIfMirrorUnsupported(version);
const installed = await finderGraalPy.findGraalPyVersion(
version,
arch,