mirror of
https://gitea.com/actions/setup-python.git
synced 2026-09-11 06:10:31 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad3497abcd | |||
| 337b0725b8 | |||
| 751276eafc | |||
| 66fcabf4d0 | |||
| 9191ea1a55 |
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: ./
|
||||
|
||||
@@ -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:
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: brace-expansion
|
||||
version: 5.0.8
|
||||
version: 5.0.9
|
||||
type: npm
|
||||
summary: Brace expansion as known from sh/bash
|
||||
homepage:
|
||||
|
||||
Generated
+1
-1
@@ -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
|
||||
|
||||
@@ -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.`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -0,0 +1,590 @@
|
||||
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('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
@@ -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:
|
||||
|
||||
Vendored
+167
-17
@@ -16069,7 +16069,13 @@ function processHeader (request, key, val) {
|
||||
} else if (typeof val[i] === 'object') {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
} else {
|
||||
arr.push(`${val[i]}`)
|
||||
// Coerce primitives (and reject unsafe coercions such as functions
|
||||
// with a crafted toString/Symbol.toPrimitive).
|
||||
const str = `${val[i]}`
|
||||
if (!isValidHeaderValue(str)) {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
}
|
||||
arr.push(str)
|
||||
}
|
||||
}
|
||||
val = arr
|
||||
@@ -16080,7 +16086,12 @@ function processHeader (request, key, val) {
|
||||
} else if (val === null) {
|
||||
val = ''
|
||||
} else {
|
||||
// Coerce primitives (and reject unsafe coercions such as functions
|
||||
// with a crafted toString/Symbol.toPrimitive).
|
||||
val = `${val}`
|
||||
if (!isValidHeaderValue(val)) {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
}
|
||||
}
|
||||
|
||||
if (headerName === 'host') {
|
||||
@@ -17452,6 +17463,7 @@ const {
|
||||
RequestContentLengthMismatchError,
|
||||
ResponseContentLengthMismatchError,
|
||||
RequestAbortedError,
|
||||
InvalidArgumentError,
|
||||
HeadersTimeoutError,
|
||||
HeadersOverflowError,
|
||||
SocketError,
|
||||
@@ -18435,8 +18447,16 @@ function writeH1 (client, request) {
|
||||
}
|
||||
body = bodyStream.stream
|
||||
contentLength = bodyStream.length
|
||||
} else if (util.isBlobLike(body) && request.contentType == null && body.type) {
|
||||
headers.push('content-type', body.type)
|
||||
} else if (util.isBlobLike(body) && request.contentType == null) {
|
||||
const contentType = body.type
|
||||
if (contentType) {
|
||||
const contentTypeValue = `${contentType}`
|
||||
if (!util.isValidHeaderValue(contentTypeValue)) {
|
||||
util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
|
||||
return false
|
||||
}
|
||||
headers.push('content-type', contentTypeValue)
|
||||
}
|
||||
}
|
||||
|
||||
if (body && typeof body.read === 'function') {
|
||||
@@ -21909,6 +21929,28 @@ function calculateRetryAfterHeader (retryAfter) {
|
||||
return new Date(retryAfter).getTime() - current
|
||||
}
|
||||
|
||||
function validatePartialResponseContentLength (headers, range, statusCode, retryCount) {
|
||||
const contentLength = headers['content-length']
|
||||
if (contentLength == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const length = Number(contentLength)
|
||||
const expectedLength = range.end - range.start + 1
|
||||
if (!Number.isFinite(length) || length !== expectedLength) {
|
||||
return new RequestRetryError('Content-Length mismatch', statusCode, {
|
||||
headers,
|
||||
data: { count: retryCount }
|
||||
})
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
class RetryHandler {
|
||||
constructor (opts, handlers) {
|
||||
const { retryOptions, ...dispatchOpts } = opts
|
||||
@@ -22123,6 +22165,12 @@ class RetryHandler {
|
||||
return false
|
||||
}
|
||||
|
||||
const contentLengthError = validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)
|
||||
if (contentLengthError != null) {
|
||||
this.abort(contentLengthError)
|
||||
return false
|
||||
}
|
||||
|
||||
const { start, size, end = size - 1 } = contentRange
|
||||
|
||||
assert(this.start === start, 'content-range mismatch')
|
||||
@@ -22146,6 +22194,12 @@ class RetryHandler {
|
||||
)
|
||||
}
|
||||
|
||||
const contentLengthError = validatePartialResponseContentLength(headers, range, statusCode, this.retryCount)
|
||||
if (contentLengthError != null) {
|
||||
this.abort(contentLengthError)
|
||||
return false
|
||||
}
|
||||
|
||||
const { start, size, end = size - 1 } = range
|
||||
assert(
|
||||
start != null && Number.isFinite(start),
|
||||
@@ -26390,7 +26444,7 @@ function validateCookiePath (path) {
|
||||
|
||||
if (
|
||||
code < 0x20 || // exclude CTLs (0-31)
|
||||
code === 0x7F || // DEL
|
||||
code > 0x7E || // exclude DEL and non-ascii
|
||||
code === 0x3B // ;
|
||||
) {
|
||||
throw new Error('Invalid cookie path')
|
||||
@@ -26399,16 +26453,80 @@ function validateCookiePath (path) {
|
||||
}
|
||||
|
||||
/**
|
||||
* I have no idea why these values aren't allowed to be honest,
|
||||
* but Deno tests these. - Khafra
|
||||
* <let-dig> ::= <letter> | <digit>
|
||||
*
|
||||
* <letter> ::= any one of the 52 alphabetic characters A through Z in
|
||||
* upper case and a through z in lower case
|
||||
*
|
||||
* <digit> ::= any one of the ten digits 0 through 9r
|
||||
*
|
||||
* @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
|
||||
* @param {number} code
|
||||
*/
|
||||
function isLetterOrDigit (code) {
|
||||
return (
|
||||
(code >= 0x30 && code <= 0x39) || // 0-9
|
||||
(code >= 0x41 && code <= 0x5A) || // A-Z
|
||||
(code >= 0x61 && code <= 0x7A) // a-z
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a cookie domain against the "preferred name syntax".
|
||||
*
|
||||
* <domain> ::= <subdomain> | " "
|
||||
* <subdomain> ::= <label> | <subdomain> "." <label>
|
||||
* <label> ::= <let-dig> [ [ <ldh-str> ] <let-dig> ]
|
||||
* <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
|
||||
* <let-dig-hyp> ::= <let-dig> | "-"
|
||||
*
|
||||
* @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
|
||||
* @see https://www.rfc-editor.org/rfc/rfc1123#section-2.1
|
||||
* @see https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4
|
||||
* @param {string} domain
|
||||
*/
|
||||
function validateCookieDomain (domain) {
|
||||
if (
|
||||
domain.startsWith('-') ||
|
||||
domain.endsWith('.') ||
|
||||
domain.endsWith('-')
|
||||
) {
|
||||
// <domain> ::= <subdomain> | " "
|
||||
if (domain === ' ') {
|
||||
return
|
||||
}
|
||||
|
||||
if (domain.length > 255) {
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
|
||||
let labelLength = 0
|
||||
|
||||
for (let i = 0; i < domain.length; ++i) {
|
||||
const code = domain.charCodeAt(i)
|
||||
|
||||
if (code === 0x2E) {
|
||||
if (labelLength === 0) {
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
|
||||
if (domain.charCodeAt(i - 1) === 0x2D) { // "-"
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
|
||||
labelLength = 0
|
||||
continue
|
||||
}
|
||||
|
||||
if (labelLength === 0 && !isLetterOrDigit(code)) {
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
|
||||
if (!isLetterOrDigit(code) && code !== 0x2D) { // "-"
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
|
||||
if (++labelLength > 63) {
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
}
|
||||
|
||||
if (labelLength === 0 || domain.charCodeAt(domain.length - 1) === 0x2D) { // "-"
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
}
|
||||
@@ -26551,7 +26669,13 @@ function stringify (cookie) {
|
||||
|
||||
const [key, ...value] = part.split('=')
|
||||
|
||||
out.push(`${key.trim()}=${value.join('=')}`)
|
||||
const trimmedKey = key.trim()
|
||||
const joinedValue = value.join('=')
|
||||
|
||||
validateCookieName(trimmedKey)
|
||||
validateCookieValue(joinedValue)
|
||||
|
||||
out.push(`${trimmedKey}=${joinedValue}`)
|
||||
}
|
||||
|
||||
return out.join('; ')
|
||||
@@ -41084,7 +41208,7 @@ function combine(acc, pre, values, max, maxLength, dropEmpties) {
|
||||
}
|
||||
// The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`)
|
||||
// sequence body.
|
||||
function expandSequence(body, isAlphaSequence, max) {
|
||||
function expandSequence(body, isAlphaSequence, max, maxLength) {
|
||||
const n = body.split(/\.\./);
|
||||
const N = [];
|
||||
// A sequence body always splits into two or three parts, but the compiler
|
||||
@@ -41107,6 +41231,7 @@ function expandSequence(body, isAlphaSequence, max) {
|
||||
test = gte;
|
||||
}
|
||||
const pad = n.some(isPadded);
|
||||
let length = 0;
|
||||
for (let i = x; test(i, y) && N.length < max; i += incr) {
|
||||
let c;
|
||||
if (isAlphaSequence) {
|
||||
@@ -41130,7 +41255,10 @@ function expandSequence(body, isAlphaSequence, max) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (length + c.length > maxLength)
|
||||
break;
|
||||
N.push(c);
|
||||
length += c.length;
|
||||
}
|
||||
return N;
|
||||
}
|
||||
@@ -41184,7 +41312,7 @@ function expand_(str, max, maxLength, isTop) {
|
||||
}
|
||||
let values;
|
||||
if (isSequence) {
|
||||
values = expandSequence(m.body, isAlphaSequence, max);
|
||||
values = expandSequence(m.body, isAlphaSequence, max, maxLength);
|
||||
}
|
||||
else {
|
||||
let n = parseCommaParts(m.body);
|
||||
@@ -41202,9 +41330,31 @@ function expand_(str, max, maxLength, isTop) {
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
}
|
||||
// Values that `combine` is going to drop as empty produce no result, so
|
||||
// they must not count against `max` - otherwise `{a,,b}` with `max: 2`
|
||||
// would stop at `['a', '']` and yield one result instead of two. Skipping
|
||||
// them outright keeps `values` bounded while leaving `max` a bound on
|
||||
// *kept* results.
|
||||
let dropsEmpties = dropEmpties && !m.post.length && !pre;
|
||||
for (let d = 0; dropsEmpties && d < acc.length; d++) {
|
||||
if (acc[d]) {
|
||||
dropsEmpties = false;
|
||||
}
|
||||
}
|
||||
values = [];
|
||||
for (let j = 0; j < n.length; j++) {
|
||||
values.push.apply(values, expand_(n[j], max, maxLength, false));
|
||||
let valuesLength = 0;
|
||||
outer: for (let j = 0; j < n.length; j++) {
|
||||
const expanded = expand_(n[j], max, maxLength, false);
|
||||
for (let k = 0; k < expanded.length; k++) {
|
||||
const v = expanded[k];
|
||||
if (dropsEmpties && !v)
|
||||
continue;
|
||||
if (values.length >= max || valuesLength + v.length > maxLength) {
|
||||
break outer;
|
||||
}
|
||||
values.push(v);
|
||||
valuesLength += v.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length);
|
||||
@@ -97948,7 +98098,7 @@ class CacheDistributor {
|
||||
.split('\n')
|
||||
.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`);
|
||||
throw new Error(`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.`);
|
||||
}
|
||||
const cachePath = await this.getCacheGlobalDirectories();
|
||||
core.saveState(State.CACHE_PATHS, cachePath);
|
||||
|
||||
Vendored
+366
-48
@@ -16069,7 +16069,13 @@ function processHeader (request, key, val) {
|
||||
} else if (typeof val[i] === 'object') {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
} else {
|
||||
arr.push(`${val[i]}`)
|
||||
// Coerce primitives (and reject unsafe coercions such as functions
|
||||
// with a crafted toString/Symbol.toPrimitive).
|
||||
const str = `${val[i]}`
|
||||
if (!isValidHeaderValue(str)) {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
}
|
||||
arr.push(str)
|
||||
}
|
||||
}
|
||||
val = arr
|
||||
@@ -16080,7 +16086,12 @@ function processHeader (request, key, val) {
|
||||
} else if (val === null) {
|
||||
val = ''
|
||||
} else {
|
||||
// Coerce primitives (and reject unsafe coercions such as functions
|
||||
// with a crafted toString/Symbol.toPrimitive).
|
||||
val = `${val}`
|
||||
if (!isValidHeaderValue(val)) {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
}
|
||||
}
|
||||
|
||||
if (headerName === 'host') {
|
||||
@@ -17452,6 +17463,7 @@ const {
|
||||
RequestContentLengthMismatchError,
|
||||
ResponseContentLengthMismatchError,
|
||||
RequestAbortedError,
|
||||
InvalidArgumentError,
|
||||
HeadersTimeoutError,
|
||||
HeadersOverflowError,
|
||||
SocketError,
|
||||
@@ -18435,8 +18447,16 @@ function writeH1 (client, request) {
|
||||
}
|
||||
body = bodyStream.stream
|
||||
contentLength = bodyStream.length
|
||||
} else if (util.isBlobLike(body) && request.contentType == null && body.type) {
|
||||
headers.push('content-type', body.type)
|
||||
} else if (util.isBlobLike(body) && request.contentType == null) {
|
||||
const contentType = body.type
|
||||
if (contentType) {
|
||||
const contentTypeValue = `${contentType}`
|
||||
if (!util.isValidHeaderValue(contentTypeValue)) {
|
||||
util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
|
||||
return false
|
||||
}
|
||||
headers.push('content-type', contentTypeValue)
|
||||
}
|
||||
}
|
||||
|
||||
if (body && typeof body.read === 'function') {
|
||||
@@ -21909,6 +21929,28 @@ function calculateRetryAfterHeader (retryAfter) {
|
||||
return new Date(retryAfter).getTime() - current
|
||||
}
|
||||
|
||||
function validatePartialResponseContentLength (headers, range, statusCode, retryCount) {
|
||||
const contentLength = headers['content-length']
|
||||
if (contentLength == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const length = Number(contentLength)
|
||||
const expectedLength = range.end - range.start + 1
|
||||
if (!Number.isFinite(length) || length !== expectedLength) {
|
||||
return new RequestRetryError('Content-Length mismatch', statusCode, {
|
||||
headers,
|
||||
data: { count: retryCount }
|
||||
})
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
class RetryHandler {
|
||||
constructor (opts, handlers) {
|
||||
const { retryOptions, ...dispatchOpts } = opts
|
||||
@@ -22123,6 +22165,12 @@ class RetryHandler {
|
||||
return false
|
||||
}
|
||||
|
||||
const contentLengthError = validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)
|
||||
if (contentLengthError != null) {
|
||||
this.abort(contentLengthError)
|
||||
return false
|
||||
}
|
||||
|
||||
const { start, size, end = size - 1 } = contentRange
|
||||
|
||||
assert(this.start === start, 'content-range mismatch')
|
||||
@@ -22146,6 +22194,12 @@ class RetryHandler {
|
||||
)
|
||||
}
|
||||
|
||||
const contentLengthError = validatePartialResponseContentLength(headers, range, statusCode, this.retryCount)
|
||||
if (contentLengthError != null) {
|
||||
this.abort(contentLengthError)
|
||||
return false
|
||||
}
|
||||
|
||||
const { start, size, end = size - 1 } = range
|
||||
assert(
|
||||
start != null && Number.isFinite(start),
|
||||
@@ -26390,7 +26444,7 @@ function validateCookiePath (path) {
|
||||
|
||||
if (
|
||||
code < 0x20 || // exclude CTLs (0-31)
|
||||
code === 0x7F || // DEL
|
||||
code > 0x7E || // exclude DEL and non-ascii
|
||||
code === 0x3B // ;
|
||||
) {
|
||||
throw new Error('Invalid cookie path')
|
||||
@@ -26399,16 +26453,80 @@ function validateCookiePath (path) {
|
||||
}
|
||||
|
||||
/**
|
||||
* I have no idea why these values aren't allowed to be honest,
|
||||
* but Deno tests these. - Khafra
|
||||
* <let-dig> ::= <letter> | <digit>
|
||||
*
|
||||
* <letter> ::= any one of the 52 alphabetic characters A through Z in
|
||||
* upper case and a through z in lower case
|
||||
*
|
||||
* <digit> ::= any one of the ten digits 0 through 9r
|
||||
*
|
||||
* @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
|
||||
* @param {number} code
|
||||
*/
|
||||
function isLetterOrDigit (code) {
|
||||
return (
|
||||
(code >= 0x30 && code <= 0x39) || // 0-9
|
||||
(code >= 0x41 && code <= 0x5A) || // A-Z
|
||||
(code >= 0x61 && code <= 0x7A) // a-z
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a cookie domain against the "preferred name syntax".
|
||||
*
|
||||
* <domain> ::= <subdomain> | " "
|
||||
* <subdomain> ::= <label> | <subdomain> "." <label>
|
||||
* <label> ::= <let-dig> [ [ <ldh-str> ] <let-dig> ]
|
||||
* <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
|
||||
* <let-dig-hyp> ::= <let-dig> | "-"
|
||||
*
|
||||
* @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
|
||||
* @see https://www.rfc-editor.org/rfc/rfc1123#section-2.1
|
||||
* @see https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4
|
||||
* @param {string} domain
|
||||
*/
|
||||
function validateCookieDomain (domain) {
|
||||
if (
|
||||
domain.startsWith('-') ||
|
||||
domain.endsWith('.') ||
|
||||
domain.endsWith('-')
|
||||
) {
|
||||
// <domain> ::= <subdomain> | " "
|
||||
if (domain === ' ') {
|
||||
return
|
||||
}
|
||||
|
||||
if (domain.length > 255) {
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
|
||||
let labelLength = 0
|
||||
|
||||
for (let i = 0; i < domain.length; ++i) {
|
||||
const code = domain.charCodeAt(i)
|
||||
|
||||
if (code === 0x2E) {
|
||||
if (labelLength === 0) {
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
|
||||
if (domain.charCodeAt(i - 1) === 0x2D) { // "-"
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
|
||||
labelLength = 0
|
||||
continue
|
||||
}
|
||||
|
||||
if (labelLength === 0 && !isLetterOrDigit(code)) {
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
|
||||
if (!isLetterOrDigit(code) && code !== 0x2D) { // "-"
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
|
||||
if (++labelLength > 63) {
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
}
|
||||
|
||||
if (labelLength === 0 || domain.charCodeAt(domain.length - 1) === 0x2D) { // "-"
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
}
|
||||
@@ -26551,7 +26669,13 @@ function stringify (cookie) {
|
||||
|
||||
const [key, ...value] = part.split('=')
|
||||
|
||||
out.push(`${key.trim()}=${value.join('=')}`)
|
||||
const trimmedKey = key.trim()
|
||||
const joinedValue = value.join('=')
|
||||
|
||||
validateCookieName(trimmedKey)
|
||||
validateCookieValue(joinedValue)
|
||||
|
||||
out.push(`${trimmedKey}=${joinedValue}`)
|
||||
}
|
||||
|
||||
return out.join('; ')
|
||||
@@ -41084,7 +41208,7 @@ function combine(acc, pre, values, max, maxLength, dropEmpties) {
|
||||
}
|
||||
// The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`)
|
||||
// sequence body.
|
||||
function expandSequence(body, isAlphaSequence, max) {
|
||||
function expandSequence(body, isAlphaSequence, max, maxLength) {
|
||||
const n = body.split(/\.\./);
|
||||
const N = [];
|
||||
// A sequence body always splits into two or three parts, but the compiler
|
||||
@@ -41107,6 +41231,7 @@ function expandSequence(body, isAlphaSequence, max) {
|
||||
test = gte;
|
||||
}
|
||||
const pad = n.some(isPadded);
|
||||
let length = 0;
|
||||
for (let i = x; test(i, y) && N.length < max; i += incr) {
|
||||
let c;
|
||||
if (isAlphaSequence) {
|
||||
@@ -41130,7 +41255,10 @@ function expandSequence(body, isAlphaSequence, max) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (length + c.length > maxLength)
|
||||
break;
|
||||
N.push(c);
|
||||
length += c.length;
|
||||
}
|
||||
return N;
|
||||
}
|
||||
@@ -41184,7 +41312,7 @@ function expand_(str, max, maxLength, isTop) {
|
||||
}
|
||||
let values;
|
||||
if (isSequence) {
|
||||
values = expandSequence(m.body, isAlphaSequence, max);
|
||||
values = expandSequence(m.body, isAlphaSequence, max, maxLength);
|
||||
}
|
||||
else {
|
||||
let n = parseCommaParts(m.body);
|
||||
@@ -41202,9 +41330,31 @@ function expand_(str, max, maxLength, isTop) {
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
}
|
||||
// Values that `combine` is going to drop as empty produce no result, so
|
||||
// they must not count against `max` - otherwise `{a,,b}` with `max: 2`
|
||||
// would stop at `['a', '']` and yield one result instead of two. Skipping
|
||||
// them outright keeps `values` bounded while leaving `max` a bound on
|
||||
// *kept* results.
|
||||
let dropsEmpties = dropEmpties && !m.post.length && !pre;
|
||||
for (let d = 0; dropsEmpties && d < acc.length; d++) {
|
||||
if (acc[d]) {
|
||||
dropsEmpties = false;
|
||||
}
|
||||
}
|
||||
values = [];
|
||||
for (let j = 0; j < n.length; j++) {
|
||||
values.push.apply(values, expand_(n[j], max, maxLength, false));
|
||||
let valuesLength = 0;
|
||||
outer: for (let j = 0; j < n.length; j++) {
|
||||
const expanded = expand_(n[j], max, maxLength, false);
|
||||
for (let k = 0; k < expanded.length; k++) {
|
||||
const v = expanded[k];
|
||||
if (dropsEmpties && !v)
|
||||
continue;
|
||||
if (values.length >= max || valuesLength + v.length > maxLength) {
|
||||
break outer;
|
||||
}
|
||||
values.push(v);
|
||||
valuesLength += v.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length);
|
||||
@@ -98693,12 +98843,114 @@ function _unique(values) {
|
||||
|
||||
|
||||
|
||||
const TOKEN = getInput('token');
|
||||
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;
|
||||
const MANIFEST_REPO_OWNER = 'actions';
|
||||
const MANIFEST_REPO_NAME = 'python-versions';
|
||||
const MANIFEST_REPO_BRANCH = 'main';
|
||||
const MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`;
|
||||
const DEFAULT_REPO_OWNER = 'actions';
|
||||
const DEFAULT_REPO_NAME = 'python-versions';
|
||||
const DEFAULT_REPO_BRANCH = 'main';
|
||||
const DEFAULT_MIRROR = `https://raw.githubusercontent.com/${DEFAULT_REPO_OWNER}/${DEFAULT_REPO_NAME}/${DEFAULT_REPO_BRANCH}`;
|
||||
// Matches https://raw.githubusercontent.com/{owner}/{repo}/{branch} 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() {
|
||||
return getInput('token');
|
||||
}
|
||||
function getMirrorToken() {
|
||||
return 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();
|
||||
function getMirror() {
|
||||
const input = 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;
|
||||
}
|
||||
function getManifestUrl() {
|
||||
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.
|
||||
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) {
|
||||
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();
|
||||
function resolveRepoCoords() {
|
||||
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);
|
||||
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) {
|
||||
let parsed;
|
||||
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;
|
||||
}
|
||||
function getLinuxOsRelease() {
|
||||
try {
|
||||
const content = external_fs_namespaceObject.readFileSync('/etc/os-release', 'utf8');
|
||||
@@ -98824,17 +99076,25 @@ async function fetchValidManifest(source, fetcher) {
|
||||
throw new Error(`Failed to fetch a valid manifest from ${source} after ${attempts} attempt(s): ${lastError?.message}`);
|
||||
}
|
||||
async function getManifest() {
|
||||
try {
|
||||
return await fetchValidManifest('the GitHub API', install_python_getManifestFromRepo);
|
||||
// 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', install_python_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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
return await fetchValidManifest('the raw URL', getManifestFromURL);
|
||||
@@ -98846,15 +99106,32 @@ async function getManifest() {
|
||||
}
|
||||
}
|
||||
function install_python_getManifestFromRepo() {
|
||||
core_debug(`Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`);
|
||||
return getManifestFromRepo(MANIFEST_REPO_OWNER, MANIFEST_REPO_NAME, AUTH, MANIFEST_REPO_BRANCH);
|
||||
const coords = resolveRepoCoords();
|
||||
if (!coords) {
|
||||
throw new Error(`Mirror "${getMirror()}" is not a GitHub repo URL; falling back to raw URL fetch.`);
|
||||
}
|
||||
core_debug(`Getting manifest from ${coords.owner}/${coords.repo}@${coords.branch}`);
|
||||
// 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 getManifestFromRepo(coords.owner, coords.repo, auth, coords.branch);
|
||||
}
|
||||
async function getManifestFromURL() {
|
||||
core_debug('Falling back to fetching the manifest using raw URL.');
|
||||
const manifestUrl = getManifestUrl();
|
||||
const http = new lib_HttpClient('tool-cache');
|
||||
const response = await http.getJson(MANIFEST_URL);
|
||||
const auth = authForUrl(manifestUrl);
|
||||
const response = await http.getJson(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;
|
||||
}
|
||||
@@ -98897,7 +99174,7 @@ async function installCpythonFromRelease(release) {
|
||||
let pythonPath = '';
|
||||
try {
|
||||
const fileName = getDownloadFileName(downloadUrl);
|
||||
pythonPath = await downloadTool(downloadUrl, fileName, AUTH);
|
||||
pythonPath = await downloadTool(downloadUrl, fileName, authForUrl(downloadUrl));
|
||||
info('Extract downloaded archive');
|
||||
let pythonExtractedFolder;
|
||||
if (utils_IS_WINDOWS) {
|
||||
@@ -99015,7 +99292,7 @@ async function useCpythonVersion(version, architecture, updateEnvironment, check
|
||||
if (freethreaded) {
|
||||
msg.push(`Free threaded versions are only available for Python 3.13.0 and later.`);
|
||||
}
|
||||
msg.push(`The list of all available versions can be found here: ${MANIFEST_URL}`);
|
||||
msg.push(`The list of all available versions can be found here: ${getManifestUrl()}`);
|
||||
throw new Error(msg.join(external_os_.EOL));
|
||||
}
|
||||
const _binDir = binDir(installDir);
|
||||
@@ -99417,8 +99694,8 @@ function findPyPyInstallDirForWindows(pythonVersion) {
|
||||
|
||||
|
||||
|
||||
const install_graalpy_TOKEN = getInput('token');
|
||||
const install_graalpy_AUTH = !install_graalpy_TOKEN ? undefined : `token ${install_graalpy_TOKEN}`;
|
||||
const TOKEN = getInput('token');
|
||||
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;
|
||||
async function installGraalPy(graalpyVersion, architecture, allowPreReleases, releases) {
|
||||
let downloadDir;
|
||||
releases = releases ?? (await getAvailableGraalPyVersions());
|
||||
@@ -99441,7 +99718,7 @@ async function installGraalPy(graalpyVersion, architecture, allowPreReleases, re
|
||||
const downloadUrl = `${foundAsset.browser_download_url}`;
|
||||
info(`Downloading GraalPy from "${downloadUrl}" ...`);
|
||||
try {
|
||||
const graalpyPath = await downloadTool(downloadUrl, undefined, install_graalpy_AUTH);
|
||||
const graalpyPath = await downloadTool(downloadUrl, undefined, AUTH);
|
||||
info('Extracting downloaded archive...');
|
||||
if (utils_IS_WINDOWS) {
|
||||
downloadDir = await extractZip(graalpyPath);
|
||||
@@ -99482,8 +99759,8 @@ async function installGraalPy(graalpyVersion, architecture, allowPreReleases, re
|
||||
async function getAvailableGraalPyVersions() {
|
||||
const http = new lib_HttpClient('tool-cache');
|
||||
const headers = {};
|
||||
if (install_graalpy_AUTH) {
|
||||
headers.authorization = install_graalpy_AUTH;
|
||||
if (AUTH) {
|
||||
headers.authorization = AUTH;
|
||||
}
|
||||
/*
|
||||
Get releases first.
|
||||
@@ -100150,7 +100427,7 @@ function combine(acc, pre, values, max, maxLength, dropEmpties) {
|
||||
}
|
||||
// The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`)
|
||||
// sequence body.
|
||||
function expandSequence(body, isAlphaSequence, max) {
|
||||
function expandSequence(body, isAlphaSequence, max, maxLength) {
|
||||
const n = body.split(/\.\./);
|
||||
const N = [];
|
||||
// A sequence body always splits into two or three parts, but the compiler
|
||||
@@ -100173,6 +100450,7 @@ function expandSequence(body, isAlphaSequence, max) {
|
||||
test = gte;
|
||||
}
|
||||
const pad = n.some(isPadded);
|
||||
let length = 0;
|
||||
for (let i = x; test(i, y) && N.length < max; i += incr) {
|
||||
let c;
|
||||
if (isAlphaSequence) {
|
||||
@@ -100196,7 +100474,10 @@ function expandSequence(body, isAlphaSequence, max) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (length + c.length > maxLength)
|
||||
break;
|
||||
N.push(c);
|
||||
length += c.length;
|
||||
}
|
||||
return N;
|
||||
}
|
||||
@@ -100250,7 +100531,7 @@ function expand_(str, max, maxLength, isTop) {
|
||||
}
|
||||
let values;
|
||||
if (isSequence) {
|
||||
values = expandSequence(m.body, isAlphaSequence, max);
|
||||
values = expandSequence(m.body, isAlphaSequence, max, maxLength);
|
||||
}
|
||||
else {
|
||||
let n = parseCommaParts(m.body);
|
||||
@@ -100268,9 +100549,31 @@ function expand_(str, max, maxLength, isTop) {
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
}
|
||||
// Values that `combine` is going to drop as empty produce no result, so
|
||||
// they must not count against `max` - otherwise `{a,,b}` with `max: 2`
|
||||
// would stop at `['a', '']` and yield one result instead of two. Skipping
|
||||
// them outright keeps `values` bounded while leaving `max` a bound on
|
||||
// *kept* results.
|
||||
let dropsEmpties = dropEmpties && !m.post.length && !pre;
|
||||
for (let d = 0; dropsEmpties && d < acc.length; d++) {
|
||||
if (acc[d]) {
|
||||
dropsEmpties = false;
|
||||
}
|
||||
}
|
||||
values = [];
|
||||
for (let j = 0; j < n.length; j++) {
|
||||
values.push.apply(values, expand_(n[j], max, maxLength, false));
|
||||
let valuesLength = 0;
|
||||
outer: for (let j = 0; j < n.length; j++) {
|
||||
const expanded = expand_(n[j], max, maxLength, false);
|
||||
for (let k = 0; k < expanded.length; k++) {
|
||||
const v = expanded[k];
|
||||
if (dropsEmpties && !v)
|
||||
continue;
|
||||
if (values.length >= max || valuesLength + v.length > maxLength) {
|
||||
break outer;
|
||||
}
|
||||
values.push(v);
|
||||
valuesLength += v.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length);
|
||||
@@ -103175,7 +103478,7 @@ class CacheDistributor {
|
||||
.split('\n')
|
||||
.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`);
|
||||
throw new Error(`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.`);
|
||||
}
|
||||
const cachePath = await this.getCacheGlobalDirectories();
|
||||
saveState(State.CACHE_PATHS, cachePath);
|
||||
@@ -103432,12 +103735,25 @@ function getCacheDistributor(packageManager, pythonVersion, cacheDependencyPath)
|
||||
|
||||
|
||||
|
||||
|
||||
function isPyPyVersion(versionSpec) {
|
||||
return versionSpec.startsWith('pypy');
|
||||
}
|
||||
function isGraalPyVersion(versionSpec) {
|
||||
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) {
|
||||
if (!isMirrorCustomized()) {
|
||||
return;
|
||||
}
|
||||
const implementation = isPyPyVersion(versionSpec) ? 'PyPy' : 'GraalPy';
|
||||
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, pythonVersion) {
|
||||
const cacheDependencyPath = getInput('cache-dependency-path') || undefined;
|
||||
const cacheDistributor = getCacheDistributor(cache, pythonVersion, cacheDependencyPath);
|
||||
@@ -103499,11 +103815,13 @@ async function run() {
|
||||
startGroup('Installed versions');
|
||||
for (const version of versions) {
|
||||
if (isPyPyVersion(version)) {
|
||||
warnIfMirrorUnsupported(version);
|
||||
const installed = await findPyPyVersion(version, arch, updateEnvironment, checkLatest, allowPreReleases);
|
||||
pythonVersion = `${installed.resolvedPyPyVersion}-${installed.resolvedPythonVersion}`;
|
||||
info(`Successfully set up PyPy ${installed.resolvedPyPyVersion} with Python (${installed.resolvedPythonVersion})`);
|
||||
}
|
||||
else if (isGraalPyVersion(version)) {
|
||||
warnIfMirrorUnsupported(version);
|
||||
const installed = await findGraalPyVersion(version, arch, updateEnvironment, checkLatest, allowPreReleases);
|
||||
pythonVersion = `${installed}`;
|
||||
info(`Successfully set up GraalPy ${installed}`);
|
||||
|
||||
@@ -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:
|
||||
|
||||
Generated
+32
-32
@@ -2650,9 +2650,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.38",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz",
|
||||
"integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==",
|
||||
"version": "2.11.21",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz",
|
||||
"integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -2663,9 +2663,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
|
||||
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
|
||||
"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"
|
||||
@@ -2675,9 +2675,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
|
||||
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
|
||||
"version": "4.28.9",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz",
|
||||
"integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2695,11 +2695,11 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
"electron-to-chromium": "^1.5.328",
|
||||
"node-releases": "^2.0.36",
|
||||
"update-browserslist-db": "^1.2.3"
|
||||
"baseline-browser-mapping": "^2.11.20",
|
||||
"caniuse-lite": "^1.0.30001810",
|
||||
"electron-to-chromium": "^1.5.420",
|
||||
"node-releases": "^2.0.54",
|
||||
"update-browserslist-db": "^1.3.2"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@@ -2759,9 +2759,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001799",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
|
||||
"integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
|
||||
"version": "1.0.30001810",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
|
||||
"integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3034,9 +3034,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.376",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz",
|
||||
"integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==",
|
||||
"version": "1.5.425",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.425.tgz",
|
||||
"integrity": "sha512-QvPtl41EUOnuT1HBvMKgxXRIaHNcagBPs50u7VULzhZXaGfqTbZyE16LQsctZ/RQHlGu+FOWeDTR4mY6YbeF1g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -4741,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": {
|
||||
@@ -5011,9 +5011,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.48",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz",
|
||||
"integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==",
|
||||
"version": "2.0.54",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
|
||||
"integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -6038,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"
|
||||
@@ -6092,9 +6092,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
|
||||
"integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user