diff --git a/.github/workflows/e2e-cache-freethreaded.yml b/.github/workflows/e2e-cache-freethreaded.yml index 943a4d0f..21b46329 100644 --- a/.github/workflows/e2e-cache-freethreaded.yml +++ b/.github/workflows/e2e-cache-freethreaded.yml @@ -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: diff --git a/.github/workflows/e2e-cache.yml b/.github/workflows/e2e-cache.yml index 19b362b0..97047f39 100644 --- a/.github/workflows/e2e-cache.yml +++ b/.github/workflows/e2e-cache.yml @@ -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: diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index eaaded01..0fbe5db2 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -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' diff --git a/.github/workflows/publish-immutable-actions.yml b/.github/workflows/publish-immutable-actions.yml index a4bd83f4..4776796e 100644 --- a/.github/workflows/publish-immutable-actions.yml +++ b/.github/workflows/publish-immutable-actions.yml @@ -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 diff --git a/.github/workflows/test-graalpy.yml b/.github/workflows/test-graalpy.yml index ef8ab398..f7abb2ce 100644 --- a/.github/workflows/test-graalpy.yml +++ b/.github/workflows/test-graalpy.yml @@ -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 diff --git a/.github/workflows/test-pypy.yml b/.github/workflows/test-pypy.yml index 82888b11..1957a7ac 100644 --- a/.github/workflows/test-pypy.yml +++ b/.github/workflows/test-pypy.yml @@ -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: diff --git a/.github/workflows/test-python-freethreaded.yml b/.github/workflows/test-python-freethreaded.yml index 708213bc..be29ed1a 100644 --- a/.github/workflows/test-python-freethreaded.yml +++ b/.github/workflows/test-python-freethreaded.yml @@ -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: ./ diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 78d48cab..f963cdda 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -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: diff --git a/.licenses/npm/@nodable/entities.dep.yml b/.licenses/npm/@nodable/entities.dep.yml index c11ab9f1..b02eaad6 100644 --- a/.licenses/npm/@nodable/entities.dep.yml +++ b/.licenses/npm/@nodable/entities.dep.yml @@ -1,9 +1,9 @@ --- name: "@nodable/entities" -version: 2.2.0 +version: 3.0.0 type: npm summary: Entity parser for XML, HTML, External entites with security and NCR control -homepage: +homepage: license: mit licenses: - sources: README.md diff --git a/.licenses/npm/balanced-match-1.0.2.dep.yml b/.licenses/npm/balanced-match-1.0.2.dep.yml deleted file mode 100644 index 36095592..00000000 --- a/.licenses/npm/balanced-match-1.0.2.dep.yml +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: balanced-match -version: 1.0.2 -type: npm -summary: Match balanced character pairs, like "{" and "}" -homepage: https://github.com/juliangruber/balanced-match -license: mit -licenses: -- sources: LICENSE.md - text: | - (MIT) - - Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> - - Permission is hereby granted, free of charge, to any person obtaining a copy of - this software and associated documentation files (the "Software"), to deal in - the Software without restriction, including without limitation the rights to - use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies - of the Software, and to permit persons to whom the Software is furnished to do - so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -- sources: README.md - text: |- - (MIT) - - Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> - - Permission is hereby granted, free of charge, to any person obtaining a copy of - this software and associated documentation files (the "Software"), to deal in - the Software without restriction, including without limitation the rights to - use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies - of the Software, and to permit persons to whom the Software is furnished to do - so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -notices: [] diff --git a/.licenses/npm/balanced-match-4.0.4.dep.yml b/.licenses/npm/balanced-match.dep.yml similarity index 100% rename from .licenses/npm/balanced-match-4.0.4.dep.yml rename to .licenses/npm/balanced-match.dep.yml diff --git a/.licenses/npm/brace-expansion-1.1.15.dep.yml b/.licenses/npm/brace-expansion-1.1.15.dep.yml deleted file mode 100644 index cc67fe75..00000000 --- a/.licenses/npm/brace-expansion-1.1.15.dep.yml +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: brace-expansion -version: 1.1.15 -type: npm -summary: Brace expansion as known from sh/bash -homepage: https://github.com/juliangruber/brace-expansion -license: mit -licenses: -- sources: LICENSE - text: | - MIT License - - Copyright (c) 2013 Julian Gruber - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -- sources: README.md - text: |- - (MIT) - - Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> - - Permission is hereby granted, free of charge, to any person obtaining a copy of - this software and associated documentation files (the "Software"), to deal in - the Software without restriction, including without limitation the rights to - use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies - of the Software, and to permit persons to whom the Software is furnished to do - so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -notices: [] diff --git a/.licenses/npm/brace-expansion-5.0.6.dep.yml b/.licenses/npm/brace-expansion.dep.yml similarity index 98% rename from .licenses/npm/brace-expansion-5.0.6.dep.yml rename to .licenses/npm/brace-expansion.dep.yml index af66077e..fd7e18fe 100644 --- a/.licenses/npm/brace-expansion-5.0.6.dep.yml +++ b/.licenses/npm/brace-expansion.dep.yml @@ -1,6 +1,6 @@ --- name: brace-expansion -version: 5.0.6 +version: 5.0.9 type: npm summary: Brace expansion as known from sh/bash homepage: diff --git a/.licenses/npm/concat-map.dep.yml b/.licenses/npm/concat-map.dep.yml deleted file mode 100644 index 20216b95..00000000 --- a/.licenses/npm/concat-map.dep.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: concat-map -version: 0.0.1 -type: npm -summary: concatenative mapdashery -homepage: https://github.com/substack/node-concat-map#readme -license: other -licenses: -- sources: LICENSE - text: | - This software is released under the MIT license: - - Permission is hereby granted, free of charge, to any person obtaining a copy of - this software and associated documentation files (the "Software"), to deal in - the Software without restriction, including without limitation the rights to - use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - the Software, and to permit persons to whom the Software is furnished to do so, - subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -- sources: README.markdown - text: MIT -notices: [] diff --git a/.licenses/npm/fast-xml-parser.dep.yml b/.licenses/npm/fast-xml-parser.dep.yml index 8f13da7e..07fd88dd 100644 --- a/.licenses/npm/fast-xml-parser.dep.yml +++ b/.licenses/npm/fast-xml-parser.dep.yml @@ -1,6 +1,6 @@ --- name: fast-xml-parser -version: 5.9.3 +version: 5.10.1 type: npm summary: Validate XML, Parse XML, Build XML without C/C++ based libraries homepage: diff --git a/.licenses/npm/is-unsafe.dep.yml b/.licenses/npm/is-unsafe.dep.yml index 02edc8dd..a0e275d4 100644 --- a/.licenses/npm/is-unsafe.dep.yml +++ b/.licenses/npm/is-unsafe.dep.yml @@ -1,10 +1,10 @@ --- name: is-unsafe -version: 1.0.1 +version: 2.0.0 type: npm summary: Zero-dependency, DOM-free, pure predicate for detecting unsafe strings across HTML, XML, SVG, SQL, SHELL, and REGEX contexts -homepage: +homepage: license: mit licenses: - sources: LICENSE diff --git a/.licenses/npm/path-expression-matcher.dep.yml b/.licenses/npm/path-expression-matcher.dep.yml index dcb076d5..a671e7d7 100644 --- a/.licenses/npm/path-expression-matcher.dep.yml +++ b/.licenses/npm/path-expression-matcher.dep.yml @@ -1,6 +1,6 @@ --- name: path-expression-matcher -version: 1.6.0 +version: 1.6.2 type: npm summary: Efficient path tracking and pattern matching for XML/JSON parsers homepage: https://github.com/NaturalIntelligence/path-expression-matcher#readme diff --git a/.licenses/npm/undici.dep.yml b/.licenses/npm/undici.dep.yml index c46a5c7b..b339a448 100644 --- a/.licenses/npm/undici.dep.yml +++ b/.licenses/npm/undici.dep.yml @@ -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 diff --git a/.licenses/npm/xml-naming.dep.yml b/.licenses/npm/xml-naming-0.1.0.dep.yml similarity index 95% rename from .licenses/npm/xml-naming.dep.yml rename to .licenses/npm/xml-naming-0.1.0.dep.yml index 6871fb55..6df51e86 100644 --- a/.licenses/npm/xml-naming.dep.yml +++ b/.licenses/npm/xml-naming-0.1.0.dep.yml @@ -4,7 +4,7 @@ version: 0.1.0 type: npm summary: Validates XML name productions — Name, NCName, QName, NMToken, NMTokens — for XML 1.0 and 1.1 -homepage: +homepage: license: mit licenses: - sources: README.md diff --git a/.licenses/npm/xml-naming-0.3.0.dep.yml b/.licenses/npm/xml-naming-0.3.0.dep.yml new file mode 100644 index 00000000..bfc32c71 --- /dev/null +++ b/.licenses/npm/xml-naming-0.3.0.dep.yml @@ -0,0 +1,35 @@ +--- +name: xml-naming +version: 0.3.0 +type: npm +summary: Validates XML name productions — Name, NCName, QName, NMToken, NMTokens — + for XML 1.0 and 1.1 +homepage: +license: mit +licenses: +- sources: LICENSE + text: | + MIT License + + Copyright (c) 2026 Natural Intelligence + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +- sources: README.md + text: MIT +notices: [] diff --git a/__tests__/cache-restore.test.ts b/__tests__/cache-restore.test.ts index 914bd5e5..4b549f51 100644 --- a/__tests__/cache-restore.test.ts +++ b/__tests__/cache-restore.test.ts @@ -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.` ); } ); diff --git a/__tests__/install-python-mirror.test.ts b/__tests__/install-python-mirror.test.ts new file mode 100644 index 00000000..75cdac9c --- /dev/null +++ b/__tests__/install-python-mirror.test.ts @@ -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 = {}; + +// 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) => 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) { + Object.assign(inputs, values); +} + +beforeEach(() => { + jest.resetAllMocks(); + for (const key of Object.keys(inputs)) { + delete inputs[key]; + } + (core.getInput as jest.Mock).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).mockResolvedValue('/tmp/py.tgz'); + (tc.extractTar as jest.Mock).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).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).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).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).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).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).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).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).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).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).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).mockResolvedValue(mockManifest); + const getJson = jest.fn(async () => ({result: mockManifest})); + (httpm.HttpClient as jest.Mock).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).mockResolvedValue('/tmp/py.tgz'); + (tc.extractTar as jest.Mock).mockResolvedValue('/tmp/extracted'); + + await installCpythonFromRelease(makeRelease(downloadUrl)); + + const call = (tc.downloadTool as jest.Mock).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(); + }); +}); diff --git a/action.yml b/action.yml index df6c8235..645de4f5 100644 --- a/action.yml +++ b/action.yml @@ -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 ')." + 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: diff --git a/dist/cache-save/index.js b/dist/cache-save/index.js index 48944752..019704a0 100644 --- a/dist/cache-save/index.js +++ b/dist/cache-save/index.js @@ -1,285 +1,6 @@ import { createRequire as __WEBPACK_EXTERNAL_createRequire } from "module"; /******/ var __webpack_modules__ = ({ -/***/ 8638: -/***/ ((module) => { - - -module.exports = balanced; -function balanced(a, b, str) { - if (a instanceof RegExp) a = maybeMatch(a, str); - if (b instanceof RegExp) b = maybeMatch(b, str); - - var r = range(a, b, str); - - return r && { - start: r[0], - end: r[1], - pre: str.slice(0, r[0]), - body: str.slice(r[0] + a.length, r[1]), - post: str.slice(r[1] + b.length) - }; -} - -function maybeMatch(reg, str) { - var m = str.match(reg); - return m ? m[0] : null; -} - -balanced.range = range; -function range(a, b, str) { - var begs, beg, left, right, result; - var ai = str.indexOf(a); - var bi = str.indexOf(b, ai + 1); - var i = ai; - - if (ai >= 0 && bi > 0) { - if(a===b) { - return [ai, bi]; - } - begs = []; - left = str.length; - - while (i >= 0 && !result) { - if (i == ai) { - begs.push(i); - ai = str.indexOf(a, i + 1); - } else if (begs.length == 1) { - result = [ begs.pop(), bi ]; - } else { - beg = begs.pop(); - if (beg < left) { - left = beg; - right = bi; - } - - bi = str.indexOf(b, i + 1); - } - - i = ai < bi && ai >= 0 ? ai : bi; - } - - if (begs.length) { - result = [ left, right ]; - } - } - - return result; -} - - -/***/ }), - -/***/ 6533: -/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { - -var concatMap = __nccwpck_require__(7087); -var balanced = __nccwpck_require__(8638); - -module.exports = expandTop; - -var escSlash = '\0SLASH'+Math.random()+'\0'; -var escOpen = '\0OPEN'+Math.random()+'\0'; -var escClose = '\0CLOSE'+Math.random()+'\0'; -var escComma = '\0COMMA'+Math.random()+'\0'; -var escPeriod = '\0PERIOD'+Math.random()+'\0'; - -function numeric(str) { - return parseInt(str, 10) == str - ? parseInt(str, 10) - : str.charCodeAt(0); -} - -function escapeBraces(str) { - return str.split('\\\\').join(escSlash) - .split('\\{').join(escOpen) - .split('\\}').join(escClose) - .split('\\,').join(escComma) - .split('\\.').join(escPeriod); -} - -function unescapeBraces(str) { - return str.split(escSlash).join('\\') - .split(escOpen).join('{') - .split(escClose).join('}') - .split(escComma).join(',') - .split(escPeriod).join('.'); -} - - -// Basically just str.split(","), but handling cases -// where we have nested braced sections, which should be -// treated as individual members, like {a,{b,c},d} -function parseCommaParts(str) { - if (!str) - return ['']; - - var parts = []; - var m = balanced('{', '}', str); - - if (!m) - return str.split(','); - - var pre = m.pre; - var body = m.body; - var post = m.post; - var p = pre.split(','); - - p[p.length-1] += '{' + body + '}'; - var postParts = parseCommaParts(post); - if (post.length) { - p[p.length-1] += postParts.shift(); - p.push.apply(p, postParts); - } - - parts.push.apply(parts, p); - - return parts; -} - -function expandTop(str, options) { - if (!str) - return []; - - options = options || {}; - var max = options.max == null ? Infinity : options.max; - - // I don't know why Bash 4.3 does this, but it does. - // Anything starting with {} will have the first two bytes preserved - // but *only* at the top level, so {},a}b will not expand to anything, - // but a{},b}c will be expanded to [a}c,abc]. - // One could argue that this is a bug in Bash, but since the goal of - // this module is to match Bash's rules, we escape a leading {} - if (str.substr(0, 2) === '{}') { - str = '\\{\\}' + str.substr(2); - } - - return expand(escapeBraces(str), max, true).map(unescapeBraces); -} - -function identity(e) { - return e; -} - -function embrace(str) { - return '{' + str + '}'; -} -function isPadded(el) { - return /^-?0\d/.test(el); -} - -function lte(i, y) { - return i <= y; -} -function gte(i, y) { - return i >= y; -} - -function expand(str, max, isTop) { - var expansions = []; - - var m = balanced('{', '}', str); - if (!m || /\$$/.test(m.pre)) return [str]; - - var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); - var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); - var isSequence = isNumericSequence || isAlphaSequence; - var isOptions = m.body.indexOf(',') >= 0; - if (!isSequence && !isOptions) { - // {a},b} - if (m.post.match(/,(?!,).*\}/)) { - str = m.pre + '{' + m.body + escClose + m.post; - return expand(str, max, true); - } - return [str]; - } - - var n; - if (isSequence) { - n = m.body.split(/\.\./); - } else { - n = parseCommaParts(m.body); - if (n.length === 1) { - // x{{a,b}}y ==> x{a}y x{b}y - n = expand(n[0], max, false).map(embrace); - if (n.length === 1) { - var post = m.post.length - ? expand(m.post, max, false) - : ['']; - return post.map(function(p) { - return m.pre + n[0] + p; - }); - } - } - } - - // at this point, n is the parts, and we know it's not a comma set - // with a single entry. - - // no need to expand pre, since it is guaranteed to be free of brace-sets - var pre = m.pre; - var post = m.post.length - ? expand(m.post, max, false) - : ['']; - - var N; - - if (isSequence) { - var x = numeric(n[0]); - var y = numeric(n[1]); - var width = Math.max(n[0].length, n[1].length) - var incr = n.length == 3 - ? Math.max(Math.abs(numeric(n[2])), 1) - : 1; - var test = lte; - var reverse = y < x; - if (reverse) { - incr *= -1; - test = gte; - } - var pad = n.some(isPadded); - - N = []; - - for (var i = x; test(i, y) && N.length < max; i += incr) { - var c; - if (isAlphaSequence) { - c = String.fromCharCode(i); - if (c === '\\') - c = ''; - } else { - c = String(i); - if (pad) { - var need = width - c.length; - if (need > 0) { - var z = new Array(need + 1).join('0'); - if (i < 0) - c = '-' + z + c.slice(1); - else - c = z + c; - } - } - } - N.push(c); - } - } else { - N = concatMap(n, function(el) { return expand(el, max, false) }); - } - - for (var j = 0; j < N.length; j++) { - for (var k = 0; k < post.length && expansions.length < max; k++) { - var expansion = pre + N[j] + post[k]; - if (!isTop || isSequence || expansion) - expansions.push(expansion); - } - } - - return expansions; -} - - -/***/ }), - /***/ 4974: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { @@ -292,7 +13,7 @@ var path = (function () { try { return __nccwpck_require__(6928) } catch (e) {}} minimatch.sep = path.sep var GLOBSTAR = minimatch.GLOBSTAR = Minimatch.GLOBSTAR = {} -var expand = __nccwpck_require__(6533) +var expand = __nccwpck_require__(8968) var plTypes = { '!': { open: '(?:(?!(?:', close: '))[^/]*?)'}, @@ -8613,26 +8334,6 @@ class Agent extends http.Agent { exports.Agent = Agent; //# sourceMappingURL=index.js.map -/***/ }), - -/***/ 7087: -/***/ ((module) => { - -module.exports = function (xs, fn) { - var res = []; - for (var i = 0; i < xs.length; i++) { - var x = fn(xs[i], i); - if (isArray(x)) res.push.apply(res, x); - else res.push(x); - } - return res; -}; - -var isArray = Array.isArray || function (xs) { - return Object.prototype.toString.call(xs) === '[object Array]'; -}; - - /***/ }), /***/ 6110: @@ -16368,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 @@ -16379,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') { @@ -17751,6 +17463,7 @@ const { RequestContentLengthMismatchError, ResponseContentLengthMismatchError, RequestAbortedError, + InvalidArgumentError, HeadersTimeoutError, HeadersOverflowError, SocketError, @@ -18734,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') { @@ -22208,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 @@ -22422,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') @@ -22445,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), @@ -26689,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') @@ -26698,16 +26453,80 @@ function validateCookiePath (path) { } /** - * I have no idea why these values aren't allowed to be honest, - * but Deno tests these. - Khafra + * ::= | + * + * ::= any one of the 52 alphabetic characters A through Z in + * upper case and a through z in lower case + * + * ::= 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". + * + * ::= | " " + * ::=