Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot] dc0ab79732 Update configuration files 2026-07-12 04:15:04 +00:00
81 changed files with 104774 additions and 117140 deletions
+6
View File
@@ -0,0 +1,6 @@
# Ignore list
/*
# Do not ignore these folders:
!__tests__/
!src/
+51
View File
@@ -0,0 +1,51 @@
// This is a reusable configuration file copied from https://github.com/actions/reusable-workflows/tree/main/reusable-configurations. Please don't make changes to this file as it's the subject of an automatic update.
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:eslint-plugin-jest/recommended',
'eslint-config-prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'eslint-plugin-node', 'eslint-plugin-jest'],
rules: {
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description'
}
],
'no-console': 'error',
'yoda': 'error',
'prefer-const': [
'error',
{
destructuring: 'all'
}
],
'no-control-regex': 'off',
'no-constant-condition': ['error', {checkLoops: false}],
'node/no-extraneous-import': 'error'
},
overrides: [
{
files: ['**/*{test,spec}.ts'],
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'jest/no-standalone-expect': 'off',
'jest/no-conditional-expect': 'off',
'no-console': 'off',
}
}
],
env: {
node: true,
es6: true,
'jest/globals': true
}
};
+95 -60
View File
@@ -23,20 +23,17 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-22.04,
ubuntu-24.04-arm,
ubuntu-22.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
python-version: [3.13.14t, 3.14.6t]
exclude:
- os: windows-11-arm
python-version: 3.13.14t
python-version: [3.13.0t, 3.13.1t, 3.13.2t]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python
uses: ./
with:
@@ -53,20 +50,17 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
python-version: [3.13.14t, 3.14.6t]
exclude:
- os: windows-11-arm
python-version: 3.13.14t
python-version: [3.13.1t, 3.13.2t, 3.13.5t]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python
id: cache-pipenv
uses: ./
@@ -96,17 +90,17 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-22.04,
ubuntu-24.04-arm,
ubuntu-22.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
python-version: [3.13.14, 3.14.6]
python-version: [3.13.0, 3.13.1, 3.13.2]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Install poetry
run: pipx install poetry
- name: Init pyproject.toml
@@ -128,20 +122,17 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-22.04,
ubuntu-24.04-arm,
ubuntu-22.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
python-version: [3.13.14t, 3.14.6t]
exclude:
- os: windows-11-arm
python-version: 3.13.14t
python-version: [3.13.0t, 3.13.1t, 3.13.2t]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python
uses: ./
with:
@@ -159,20 +150,17 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-22.04,
ubuntu-24.04-arm,
ubuntu-22.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
python-version: [3.13.14t, 3.14.6t]
exclude:
- os: windows-11-arm
python-version: 3.13.14t
python-version: [3.13.1t, 3.13.2t, 3.13.5t]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python
id: cache-pipenv
uses: ./
@@ -203,20 +191,17 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-22.04,
ubuntu-24.04-arm,
ubuntu-22.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
python-version: [3.13.14t, 3.14.6t]
exclude:
- os: windows-11-arm
python-version: 3.13.14t
python-version: [3.13.0t, 3.13.1t, 3.13.2t]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python
uses: ./
with:
@@ -234,20 +219,17 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-22.04,
ubuntu-24.04-arm,
ubuntu-22.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
python-version: [3.13.14t, 3.14.6t]
exclude:
- os: windows-11-arm
python-version: 3.13.14t
python-version: [3.13.0t, 3.13.1t, 3.13.2t]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python
uses: ./
with:
@@ -257,3 +239,56 @@ jobs:
pip-version: '25.0.1'
- name: Install dependencies
run: pip install numpy pandas requests
python-pip-dependencies-caching-with-pip-install:
name: Test pip (Python ${{ matrix.python-version}}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
[
ubuntu-latest,
ubuntu-22.04,
ubuntu-24.04-arm,
ubuntu-22.04-arm,
windows-latest,
macos-latest,
macos-15-intel
]
python-version: [3.13.0t, 3.13.1t, 3.13.2t]
steps:
- uses: actions/checkout@v6
- name: Setup Python
uses: ./
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
pip-install: numpy pandas requests
python-pip-dependencies-caching-path-with-pip-install:
name: Test pip (Python ${{ matrix.python-version}}, ${{ matrix.os }}, caching path)
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
[
ubuntu-latest,
ubuntu-22.04,
ubuntu-24.04-arm,
ubuntu-22.04-arm,
windows-latest,
macos-latest,
macos-15-intel
]
python-version: [3.13.0t, 3.13.1t, 3.13.2t]
steps:
- uses: actions/checkout@v6
- name: Setup Python
uses: ./
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: __tests__/data/requirements.txt
pip-install: numpy pandas requests
+136 -67
View File
@@ -23,26 +23,33 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
python-version:
['3.10', '3.11', 'pypy-3.11-v7.x', '3.12', '3.13', '3.14']
[
'3.9',
'pypy-3.9-v7.x',
'3.10',
'pypy-3.10-v7.x',
'3.11',
'pypy-3.11-v7.x',
'3.12',
'3.13'
]
exclude:
- os: windows-latest
python-version: pypy-3.11-v7.x
- os: windows-11-arm
python-version: pypy-3.11-v7.x
- os: windows-11-arm
python-version: '3.10'
- os: macos-latest
python-version: pypy-3.9-v7.x
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python
uses: ./
with:
@@ -59,24 +66,29 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
python-version: ['3.11', 'pypy-3.11-v7.x', '3.12', '3.13', '3.14']
python-version:
['3.10', 'pypy-3.10-v7.x', '3.11', 'pypy-3.11-v7.x', '3.12', '3.13']
exclude:
- os: windows-latest
python-version: pypy-3.11-v7.x
- os: ubuntu-22.04
python-version: pypy-3.11-v7.x
- os: windows-11-arm
- os: ubuntu-22.04-arm
python-version: pypy-3.10-v7.x
- os: ubuntu-22.04-arm
python-version: pypy-3.11-v7.x
- os: ubuntu-22.04-arm
python-version: pypy-3.10-v7.x
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python
id: cache-pipenv
uses: ./
@@ -111,27 +123,27 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
python-version:
['3.10', '3.11', 'pypy-3.11-v7.x', '3.12', '3.13', '3.14']
exclude:
- os: windows-latest
python-version: pypy-3.11-v7.x
- os: ubuntu-22.04
python-version: pypy-3.11-v7.x
- os: windows-11-arm
python-version: pypy-3.11-v7.x
- os: windows-11-arm
python-version: '3.10'
[
'3.9',
'pypy-3.9-v7.x',
'3.10',
'pypy-3.10-v7.x',
'3.11',
'pypy-3.11-v7.x',
'3.12',
'3.13'
]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Install poetry
run: |
pipx install poetry
@@ -157,25 +169,32 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
python-version:
['3.10', '3.11', 'pypy-3.11-v7.x', '3.12', '3.13', '3.14']
[
'3.9',
'pypy-3.9-v7.x',
'3.10',
'pypy-3.10-v7.x',
'3.11',
'pypy-3.11-v7.x',
'3.12',
'3.13'
]
exclude:
- os: windows-latest
python-version: pypy-3.11-v7.x
- os: windows-11-arm
python-version: pypy-3.11-v7.x
- os: windows-11-arm
python-version: '3.10'
- os: macos-latest
python-version: pypy-3.9-v7.x
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python
uses: ./
with:
@@ -193,24 +212,27 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
python-version: ['3.11', 'pypy-3.11-v7.x', '3.12', '3.13', '3.14']
python-version:
['3.10', 'pypy-3.10-v7.x', '3.11', 'pypy-3.11-v7.x', '3.12', '3.13']
exclude:
- os: windows-latest
python-version: pypy-3.11-v7.x
- os: ubuntu-22.04
python-version: pypy-3.11-v7.x
- os: windows-11-arm
- os: ubuntu-22.04-arm
python-version: pypy-3.10-v7.x
- os: ubuntu-22.04-arm
python-version: pypy-3.11-v7.x
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python
id: cache-pipenv
uses: ./
@@ -246,20 +268,17 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
exclude:
- os: windows-11-arm
python-version: '3.10'
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python
uses: ./
with:
@@ -277,20 +296,17 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
exclude:
- os: windows-11-arm
python-version: '3.10'
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python
uses: ./
with:
@@ -300,3 +316,56 @@ jobs:
pip-version: '25.0.1'
- name: Install dependencies
run: pip install numpy pandas requests
python-pip-dependencies-caching-with-pip-install:
name: Test pip (Python ${{ matrix.python-version}}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
[
ubuntu-latest,
ubuntu-24.04-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
windows-latest,
macos-latest,
macos-15-intel
]
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v6
- name: Setup Python
uses: ./
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
pip-install: numpy pandas requests
python-pip-dependencies-caching-path-with-pip-install:
name: Test pip (Python ${{ matrix.python-version}}, ${{ matrix.os }}, caching path)
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
[
ubuntu-latest,
ubuntu-24.04-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
windows-latest,
macos-latest,
macos-15-intel
]
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v6
- name: Setup Python
uses: ./
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: __tests__/data/requirements.txt
pip-install: numpy pandas requests
+38 -40
View File
@@ -19,25 +19,30 @@ jobs:
matrix:
operating-system:
[
windows-latest,
ubuntu-22.04,
ubuntu-22.04-arm,
ubuntu-latest,
ubuntu-24.04-arm,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
macos-latest,
macos-15-intel
]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: Run with setup-python 3.9.13
uses: ./
with:
python-version: 3.9.13
- name: Verify 3.9.13
run: python __tests__/verify-python.py 3.9.13
- name: Run with setup-python 3.10.11
if: matrix.operating-system != 'windows-11-arm'
uses: ./
with:
python-version: 3.10.11
- name: Verify 3.10.11
if: matrix.operating-system != 'windows-11-arm'
run: python __tests__/verify-python.py 3.10.11
- name: Run with setup-python 3.11.9
@@ -47,59 +52,52 @@ jobs:
- name: Verify 3.11.9
run: python __tests__/verify-python.py 3.11.9
- name: Run with setup-python 3.12.10
- name: Run with setup-python 3.12.7
uses: ./
with:
python-version: 3.12.10
- name: Verify 3.12.10
run: python __tests__/verify-python.py 3.12.10
python-version: 3.12.7
- name: Verify 3.12.7
run: python __tests__/verify-python.py 3.12.7
- name: Run with setup-python 3.13.14
- name: Run with setup-python 3.13.0
uses: ./
with:
python-version: 3.13.14
- name: Verify 3.13.14
run: python __tests__/verify-python.py 3.13.14
python-version: 3.13.0
- name: Verify 3.13.0
run: python __tests__/verify-python.py 3.13.0
- name: Run with setup-python 3.14.6
- name: Run with setup-python 3.13
id: cp313
uses: ./
with:
python-version: 3.14.6
- name: Verify 3.14.6
run: python __tests__/verify-python.py 3.14.6
- name: Run with setup-python 3.14
id: cp314
uses: ./
with:
python-version: '3.14'
- name: Verify 3.14
run: python __tests__/verify-python.py 3.14
python-version: '3.13'
- name: Verify 3.13
run: python __tests__/verify-python.py 3.13
- name: Install pipx (Windows)
if: runner.os == 'Windows'
if: runner. os == 'Windows'
run: python -m pip install pipx
- name: Run python-path sample 3.14
run: pipx run --python '${{ steps.cp314.outputs.python-path }}' nox --version
- name: Run python-path sample 3.13
run: pipx run --python '${{ steps.cp313.outputs.python-path }}' nox --version
env:
PIPX_HOME: '${{ runner.temp }}/pipx'
PIPX_BIN_DIR: '${{ runner.temp }}/pipx/bin'
- name: Run with setup-python ==3.14
- name: Run with setup-python ==3.13
uses: ./
with:
python-version: '==3.14'
- name: Verify ==3.14
run: python __tests__/verify-python.py 3.14
- name: Run with setup-python <3.14
uses: ./
with:
python-version: '<3.14'
- name: Verify <3.14
python-version: '==3.13'
- name: Verify ==3.13
run: python __tests__/verify-python.py 3.13
- name: Run with setup-python <3.13
uses: ./
with:
python-version: '<3.13'
- name: Verify <3.13
run: python __tests__/verify-python.py 3.12
- name: Test Raw Endpoint Access
run: |
curl -L https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json | jq empty
@@ -14,7 +14,7 @@ jobs:
steps:
- name: Checking out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: Publish
id: publish
uses: actions/publish-immutable-action@4bc8754ffc40f27910afb20287dbbbb675a4e978 # v0.0.4
uses: actions/publish-immutable-action@v0.0.4
+8 -7
View File
@@ -28,13 +28,14 @@ jobs:
macos-15-intel
]
graalpy:
- 'graalpy-22.3'
- 'graalpy-23.0'
- 'graalpy-23.1'
- 'graalpy-24.2'
- 'graalpy-25.0'
- 'graalpy-24.1'
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python ${{ matrix.graalpy }}
id: setup-python
@@ -81,11 +82,11 @@ jobs:
ubuntu-latest,
macos-15-intel
]
graalpy: ['graalpy23.1', 'graalpy24.2', 'graalpy25.0']
graalpy: ['graalpy22.3', 'graalpy23.0', 'graalpy23.1', 'graalpy24.1']
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python ${{ matrix.graalpy }}
id: setup-python
@@ -107,12 +108,12 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest, macos-15-intel]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup GraalPy and check latest
uses: ./
id: graalpy
with:
python-version: 'graalpy-25.x'
python-version: 'graalpy-24.x'
check-latest: true
- name: GraalPy and Python version
run: python --version
+55 -43
View File
@@ -22,24 +22,31 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
macos-15-intel,
macos-latest
ubuntu-22.04,
ubuntu-22.04-arm,
ubuntu-24.04-arm,
ubuntu-latest,
macos-15-intel
]
pypy:
- 'pypy-3.11'
- 'pypy-3.11-v7.3.23'
- 'pypy-3.11-v7.3.x'
- 'pypy-3.11-v7.x'
- 'pypy-2.7'
- 'pypy-3.10'
- 'pypy3.9'
- 'pypy-2.7-v7.3.17'
- 'pypy-3.10-v7.3.17'
- 'pypy-3.10-v7.3.16'
- 'pypy-3.10-v7.3.x'
- 'pypy-3.10-v7.x'
- 'pypy-2.7-v7.3.12rc1'
- 'pypy-3.11-nightly'
- 'pypy3.11-v7.3.23'
- 'pypy3.10-v7.3.17'
- 'pypy3.11-v7.3.19'
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python ${{ matrix.pypy }}
id: setup-python
@@ -78,19 +85,20 @@ jobs:
fail-fast: false
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
windows-latest,
macos-15-intel,
macos-latest
]
pypy: ['pypy-3.11']
- macos-15-intel
- macos-14
- macos-15
- windows-2022
- windows-2025
- ubuntu-22.04
- ubuntu-24.04
- ubuntu-22.04-arm
- ubuntu-24.04-arm
pypy: ['pypy-2.7', 'pypy-3.10', 'pypy-3.11']
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python ${{ matrix.pypy }}
id: setup-python
@@ -130,18 +138,19 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
macos-15-intel,
macos-latest
ubuntu-22.04,
ubuntu-22.04-arm,
ubuntu-24.04-arm,
ubuntu-latest,
macos-15-intel
]
pypy: ['pypy3.11-nightly', 'pypy3.11']
pypy: ['pypy2.7', 'pypy3.9', 'pypy3.11-nightly', 'pypy3.11']
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python ${{ matrix.pypy }}
id: setup-python
@@ -163,15 +172,16 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
macos-15-intel,
macos-latest
ubuntu-22.04,
ubuntu-22.04-arm,
ubuntu-24.04-arm,
ubuntu-latest,
macos-15-intel
]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup PyPy and check latest
uses: ./
with:
@@ -204,21 +214,23 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
macos-15-intel,
macos-latest
ubuntu-22.04,
ubuntu-22.04-arm,
ubuntu-24.04-arm,
ubuntu-latest,
macos-15-intel
]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup PyPy and check latest
uses: ./
with:
python-version: |
pypy-3.11-v7.3.x
pypy-3.10-v7.3.x
pypy3.9
check-latest: true
- name: PyPy and Python version
run: python --version
@@ -236,13 +248,13 @@ jobs:
run: |
EXECUTABLE="pypy-3.11-v7.3.x"
EXECUTABLE=${EXECUTABLE/-/} # remove the first '-' in "pypy-X.Y" -> "pypyX.Y" to match executable name
EXECUTABLE=${EXECUTABLE%%-*} # remove any -* suffix
EXECUTABLE=${EXECUTABLE%%-*} # remove any -* suffixe
${EXECUTABLE} --version
shell: bash
- name: Assert expected binaries (or symlinks) are present
run: |
EXECUTABLE='pypy-3.10-v7.3.x'
EXECUTABLE='pypy3.9'
EXECUTABLE=${EXECUTABLE/pypy-/pypy} # remove the first '-' in "pypy-X.Y" -> "pypyX.Y" to match executable name
EXECUTABLE=${EXECUTABLE%%-*} # remove any -* suffix
EXECUTABLE=${EXECUTABLE%%-*} # remove any -* suffixe
${EXECUTABLE} --version
shell: bash
+120 -120
View File
@@ -22,18 +22,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.13.14t, 3.14.6t]
python: [3.13.0t, 3.13.1t, 3.13.2t]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python ${{ matrix.python }}
id: setup-python
@@ -59,18 +59,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.13.14t, 3.14.6t]
python: [3.13.0t, 3.13.1t, 3.13.2t]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: build-version-file ${{ matrix.python }}
run: echo ${{ matrix.python }} > .python-version
@@ -99,18 +99,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.13.14t, 3.14.6t]
python: [3.13.0t, 3.13.1t, 3.13.2t]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: build-version-file ${{ matrix.python }}
run: echo ${{ matrix.python }} > .python-version
@@ -137,18 +137,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.13.14, 3.14.6]
python: [3.13.0, 3.13.1, 3.13.2]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: build-version-file ${{ matrix.python }}
run: |
@@ -178,18 +178,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.13.14, 3.14.6]
python: [3.13.0, 3.13.1, 3.13.2]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: build-version-file ${{ matrix.python }}
run: |
@@ -219,18 +219,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.13.14t, 3.14.6t, 3.15t-dev]
python: [3.13.0t, 3.13.1t, 3.13.2t, 3.14t-dev]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: build-tool-versions-file ${{ matrix.python }}
run: |
@@ -250,18 +250,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.13t, 3.15t-dev]
python: [3.13t, 3.14t-dev]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: build-version-file ${{ matrix.python }}
run: |
@@ -290,18 +290,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.13.14t, 3.14.6t, 3.15t-dev]
python: [3.13.0t, 3.13.1t, 3.13.2t, 3.14t-dev]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: build-version-file ${{ matrix.python }}
run: |
@@ -323,30 +323,30 @@ jobs:
run: python -c 'import math; print(math.factorial(5))'
setup-pre-release-version-from-manifest:
name: Setup 3.15.0-beta.3 ${{ matrix.os }}
name: Setup 3.14.0-alpha.6 ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python 3.15.0-beta.3
- name: setup-python 3.14.0-alpha.6
id: setup-python
uses: ./
with:
python-version: '3.15.0-beta.3'
python-version: '3.14.0-alpha.6'
freethreaded: true
- name: Check python-path
@@ -360,67 +360,67 @@ jobs:
run: python -c 'import math; print(math.factorial(5))'
setup-dev-version:
name: Setup 3.15t-dev ${{ matrix.os }}
name: Setup 3.14t-dev ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
ubuntu-22.04,
ubuntu-22.04-arm,
ubuntu-24.04-arm,
ubuntu-latest,
macos-15-intel
]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python 3.15t-dev
- name: setup-python 3.14t-dev
id: setup-python
uses: ./
with:
python-version: '3.15t-dev'
python-version: '3.14t-dev'
- name: Check python-path
run: ./__tests__/check-python-path.sh '${{ steps.setup-python.outputs.python-path }}'
shell: bash
- name: Validate version
run: ${{ startsWith(steps.setup-python.outputs.python-version, '3.15.') }}
run: ${{ startsWith(steps.setup-python.outputs.python-version, '3.14.') }}
shell: bash
- name: Run simple code
run: python -c 'import math; print(math.factorial(5))'
setup-prerelease-version:
name: Setup 3.15t ${{ matrix.os }}
name: Setup 3.14t ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
ubuntu-22.04,
ubuntu-22.04-arm,
ubuntu-24.04-arm,
ubuntu-latest,
macos-15-intel
]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python 3.15t
- name: setup-python 3.14t
id: setup-python
uses: ./
with:
python-version: '3.15t'
python-version: '3.14t'
allow-prereleases: true
- name: Check python-path
@@ -428,7 +428,7 @@ jobs:
shell: bash
- name: Validate version
run: ${{ startsWith(steps.setup-python.outputs.python-version, '3.15.') }}
run: ${{ startsWith(steps.setup-python.outputs.python-version, '3.14.') }}
shell: bash
- name: Run simple code
@@ -442,18 +442,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.13.14t, 3.14.6t]
python: [3.13.0t, 3.13.1t, 3.13.2t]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python ${{ matrix.python }}
id: setup-python
@@ -475,17 +475,17 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
ubuntu-22.04,
ubuntu-22.04-arm,
ubuntu-24.04-arm,
ubuntu-latest,
macos-15-intel
]
python-version: [3.13t, 3.15t-dev]
python-version: [3.13t, 3.14t-dev]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python and check latest
id: setup-python
uses: ./
@@ -502,24 +502,24 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
ubuntu-22.04,
ubuntu-22.04-arm,
ubuntu-24.04-arm,
ubuntu-latest,
macos-15-intel
]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup multiple Python versions
- uses: actions/checkout@v6
- name: Setup Python and check latest
id: setup-python
uses: ./
with:
python-version: |
3.13.14t
3.14.6t
3.15t-dev
3.13.1t
3.13.2t
3.14t-dev
- name: Verify Python version
run: ${{ steps.setup-python.outputs.python-path }} -VVV
@@ -531,18 +531,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.13.14, 3.14.6, 3.15-dev, 3.15.0-beta.3]
python: [3.13.1, 3.13.2, 3.14-dev, 3.14.0-alpha.6]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python ${{ matrix.python }}
id: setup-python
uses: ./
+115 -171
View File
@@ -22,21 +22,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.10.11, 3.11.9, 3.12.10, 3.13.14, 3.14.6]
exclude:
- os: windows-11-arm
python: 3.10.11
python: [3.9.13, 3.10.11, 3.11.9, 3.12.3, 3.13.2]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python ${{ matrix.python }}
id: setup-python
@@ -61,32 +58,6 @@ 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 }}
@@ -95,21 +66,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.10.11, 3.11.9, 3.12.10, 3.13.14, 3.14.6]
exclude:
- os: windows-11-arm
python: 3.10.11
python: [3.9.13, 3.10.11, 3.11.9, 3.12.3, 3.13.2]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: build-version-file ${{ matrix.python }}
run: echo ${{ matrix.python }} > .python-version
@@ -145,21 +113,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.10.11, 3.11.9, 3.12.10, 3.13.14, 3.14.6]
exclude:
- os: windows-11-arm
python: 3.10.11
python: [3.9.13, 3.10.11, 3.11.9, 3.12.3, 3.13.2]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: build-version-file ${{ matrix.python }}
run: echo ${{ matrix.python }} > .python-version
@@ -193,21 +158,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.10.11, 3.11.9, '==3.12.10', 3.13.14, 3.14.6]
exclude:
- os: windows-11-arm
python: 3.10.11
python: [3.9.13, 3.10.11, 3.11.9, '==3.12.3', 3.13.2]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: build-version-file ${{ matrix.python }}
run: |
@@ -246,21 +208,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.10.11, 3.11.9, 3.12.10, 3.13.14, 3.14.6]
exclude:
- os: windows-11-arm
python: 3.10.11
python: [3.9.13, 3.10.11, 3.11.9, 3.12.3, 3.13.2]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: build-version-file ${{ matrix.python }}
run: |
@@ -299,25 +258,21 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [pypy3.11-7.3.18, graalpy-24.1.2, 3.13.14, 3.15-dev]
python: [pypy3.11-7.3.18, graalpy-24.1.2, 3.13.2, 3.14-dev]
exclude:
- os: windows-latest
python: graalpy-24.1.2
- os: windows-11-arm
python: graalpy-24.1.2
- os: windows-11-arm
python: pypy3.11-7.3.18
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: build-tool-versions-file ${{ matrix.python }}
run: |
@@ -337,21 +292,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.10.11, 3.11.9, 3.13.14, 3.14.6]
exclude:
- os: windows-11-arm
python: 3.10.11
python: [3.9.13, 3.10.11, 3.11.9, 3.13.2]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: build-version-file ${{ matrix.python }}
run: |
@@ -390,21 +342,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: [3.10.11, 3.11.9, 3.13.14, 3.14.6]
exclude:
- os: windows-11-arm
python: 3.10.11
python: [3.9.13, 3.10.11, 3.11.9, 3.13.2]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: build-version-file ${{ matrix.python }}
run: |
@@ -436,30 +385,30 @@ jobs:
run: python -c 'import math; print(math.factorial(5))'
setup-pre-release-version-from-manifest:
name: Setup 3.15.0-beta.3 ${{ matrix.os }}
name: Setup 3.14.0-alpha.6 ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python 3.15.0-beta.3
- name: setup-python 3.14.0-alpha.6
id: setup-python
uses: ./
with:
python-version: '3.15.0-beta.3'
python-version: '3.14.0-alpha.6'
- name: Check python-path
run: ./__tests__/check-python-path.sh '${{ steps.setup-python.outputs.python-path }}'
@@ -468,8 +417,8 @@ jobs:
- name: Validate version
run: |
$pythonVersion = (python --version)
if ("Python 3.15.0b3" -ne "$pythonVersion"){
Write-Host "The current version is $pythonVersion; expected version is 3.15.0b3"
if ("Python 3.14.0a6" -ne "$pythonVersion"){
Write-Host "The current version is $pythonVersion; expected version is 3.14.0a6"
exit 1
}
$pythonVersion
@@ -479,67 +428,67 @@ jobs:
run: python -c 'import math; print(math.factorial(5))'
setup-dev-version:
name: Setup 3.15-dev ${{ matrix.os }}
name: Setup 3.14-dev ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
ubuntu-22.04,
ubuntu-22.04-arm,
ubuntu-24.04-arm,
ubuntu-latest,
macos-15-intel
]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python 3.15-dev
- name: setup-python 3.14-dev
id: setup-python
uses: ./
with:
python-version: '3.15-dev'
python-version: '3.14-dev'
- name: Check python-path
run: ./__tests__/check-python-path.sh '${{ steps.setup-python.outputs.python-path }}'
shell: bash
- name: Validate version
run: ${{ startsWith(steps.setup-python.outputs.python-version, '3.15.') }}
run: ${{ startsWith(steps.setup-python.outputs.python-version, '3.14.') }}
shell: bash
- name: Run simple code
run: python -c 'import math; print(math.factorial(5))'
setup-prerelease-version:
name: Setup 3.15 ${{ matrix.os }}
name: Setup 3.14 ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
ubuntu-22.04,
ubuntu-22.04-arm,
ubuntu-24.04-arm,
ubuntu-latest,
macos-15-intel
]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python 3.15
- name: setup-python 3.14
id: setup-python
uses: ./
with:
python-version: '3.15'
python-version: '3.14'
allow-prereleases: true
- name: Check python-path
@@ -547,7 +496,7 @@ jobs:
shell: bash
- name: Validate version
run: ${{ startsWith(steps.setup-python.outputs.python-version, '3.15.') }}
run: ${{ startsWith(steps.setup-python.outputs.python-version, '3.14.') }}
shell: bash
- name: Run simple code
@@ -561,21 +510,18 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
ubuntu-22.04,
ubuntu-22.04-arm,
macos-15-intel,
macos-latest
ubuntu-latest,
ubuntu-24.04-arm
]
python: ['3.10', '3.11', '3.12', '3.13', '3.14']
exclude:
- os: windows-11-arm
python: '3.10'
python: ['3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v6
- name: setup-python ${{ matrix.python }}
id: setup-python
@@ -597,20 +543,17 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
ubuntu-22.04,
ubuntu-22.04-arm,
ubuntu-24.04-arm,
ubuntu-latest,
macos-15-intel
]
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
exclude:
- os: windows-11-arm
python-version: '3.10'
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python and check latest
uses: ./
with:
@@ -633,30 +576,31 @@ jobs:
matrix:
os:
[
ubuntu-22.04,
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
windows-11-arm,
macos-15-intel,
macos-latest
ubuntu-22.04,
ubuntu-22.04-arm,
ubuntu-24.04-arm,
ubuntu-latest,
macos-15-intel
]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v6
- name: Setup Python and check latest
uses: ./
with:
python-version: |
3.9
3.10
3.11
3.12
3.13
3.14
check-latest: true
- name: Validate version
run: |
$pythonVersion = (python --version)
if ("$pythonVersion" -NotMatch "3.14"){
Write-Host "The current version is $pythonVersion; expected version is 3.14"
if ("$pythonVersion" -NotMatch "3.13"){
Write-Host "The current version is $pythonVersion; expected version is 3.13"
exit 1
}
$pythonVersion
+1 -3
View File
@@ -14,6 +14,4 @@ allowed:
reviewed:
npm:
- "@actions/http-client"
- "balanced-match"
- "brace-expansion"
- "@actions/http-client"
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@actions/cache"
version: 6.2.0
version: 5.1.0
type: npm
summary: Actions cache lib
homepage: https://github.com/actions/toolkit/tree/main/packages/cache
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@actions/core"
version: 3.0.1
version: 2.0.3
type: npm
summary: Actions core lib
homepage: https://github.com/actions/toolkit/tree/main/packages/core
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@actions/exec"
version: 3.0.0
version: 2.0.0
type: npm
summary: Actions exec lib
homepage: https://github.com/actions/toolkit/tree/main/packages/exec
-20
View File
@@ -1,20 +0,0 @@
---
name: "@actions/glob"
version: 0.7.0
type: npm
summary: Actions glob lib
homepage: https://github.com/actions/toolkit/tree/main/packages/glob
license: mit
licenses:
- sources: LICENSE.md
text: |-
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
notices: []
@@ -1,6 +1,6 @@
---
name: "@actions/glob"
version: 0.6.1
version: 0.5.1
type: npm
summary: Actions glob lib
homepage: https://github.com/actions/toolkit/tree/main/packages/glob
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@actions/http-client"
version: 4.0.1
version: 3.0.2
type: npm
summary: Actions Http Client
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@actions/io"
version: 3.0.2
version: 2.0.0
type: npm
summary: Actions io lib
homepage: https://github.com/actions/toolkit/tree/main/packages/io
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@actions/tool-cache"
version: 4.0.0
version: 3.0.1
type: npm
summary: Actions tool-cache lib
homepage: https://github.com/actions/toolkit/tree/main/packages/tool-cache
@@ -1,17 +1,16 @@
---
name: xml-naming
version: 0.3.0
name: "@azure/abort-controller"
version: 1.1.0
type: npm
summary: Validates XML name productions — Name, NCName, QName, NMToken, NMTokens —
for XML 1.0 and 1.1
homepage:
summary: Microsoft Azure SDK for JavaScript - Aborter
homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/abort-controller/README.md
license: mit
licenses:
- sources: LICENSE
text: |
MIT License
The MIT License (MIT)
Copyright (c) 2026 Natural Intelligence
Copyright (c) 2020 Microsoft
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -30,6 +29,4 @@ licenses:
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: []
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@azure/core-client"
version: 1.10.2
version: 1.10.1
type: npm
summary: Core library for interfacing with AutoRest generated code
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-client/
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@azure/core-http-compat"
version: 2.4.0
version: 2.3.1
type: npm
summary: Core HTTP Compatibility Library to bridge the gap between Core V1 & V2 packages.
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-compat/
+2 -2
View File
@@ -1,9 +1,9 @@
---
name: "@azure/core-rest-pipeline"
version: 1.24.0
version: 1.22.2
type: npm
summary: Isomorphic client library for making HTTP requests in node.js and browser.
homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/core-rest-pipeline/README.md
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-rest-pipeline/
license: mit
licenses:
- sources: LICENSE
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@azure/core-xml"
version: 1.5.1
version: 1.5.0
type: npm
summary: Core library for interacting with XML payloads
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-xml/
+2 -2
View File
@@ -1,9 +1,9 @@
---
name: "@azure/storage-blob"
version: 12.32.0
version: 12.29.1
type: npm
summary: Microsoft Azure Storage SDK for JavaScript - Blob
homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/storage/storage-blob/README.md
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/storage/storage-blob/
license: mit
licenses:
- sources: LICENSE
+2 -2
View File
@@ -1,9 +1,9 @@
---
name: "@azure/storage-common"
version: 12.4.0
version: 12.1.1
type: npm
summary: Azure Storage Common Client Library for JavaScript
homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/storage/storage-common/README.md
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/storage/storage-internal-avro/
license: mit
licenses:
- sources: LICENSE
+2 -2
View File
@@ -1,9 +1,9 @@
---
name: "@nodable/entities"
version: 3.0.0
version: 2.2.0
type: npm
summary: Entity parser for XML, HTML, External entites with security and NCR control
homepage:
homepage:
license: mit
licenses:
- sources: README.md
+2 -2
View File
@@ -1,9 +1,9 @@
---
name: "@typespec/ts-http-runtime"
version: 0.3.6
version: 0.3.2
type: npm
summary: Isomorphic client library for making HTTP requests in node.js and browser.
homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/ts-http-runtime/README.md
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/ts-http-runtime/
license: mit
licenses:
- sources: LICENSE
+26 -5
View File
@@ -1,18 +1,39 @@
---
name: balanced-match
version: 4.0.4
version: 1.0.2
type: npm
summary: Match balanced character pairs, like "{" and "}"
homepage:
license: other
homepage: https://github.com/juliangruber/balanced-match
license: mit
licenses:
- sources: LICENSE.md
text: |
(MIT)
Original code Copyright Julian Gruber <julian@juliangruber.com>
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
Port to TypeScript Copyright Isaac Z. Schlueter <i@izs.me>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- sources: README.md
text: |-
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
+27 -6
View File
@@ -1,18 +1,16 @@
---
name: brace-expansion
version: 5.0.9
version: 1.1.13
type: npm
summary: Brace expansion as known from sh/bash
homepage:
license: other
homepage: https://github.com/juliangruber/brace-expansion
license: mit
licenses:
- sources: LICENSE
text: |
MIT License
Copyright Julian Gruber <julian@juliangruber.com>
TypeScript port Copyright Isaac Z. Schlueter <i@izs.me>
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
@@ -24,6 +22,29 @@ licenses:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- sources: README.md
text: |-
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+31
View File
@@ -0,0 +1,31 @@
---
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: []
+2 -2
View File
@@ -1,9 +1,9 @@
---
name: fast-xml-parser
version: 5.10.1
version: 5.9.2
type: npm
summary: Validate XML, Parse XML, Build XML without C/C++ based libraries
homepage:
homepage:
license: mit
licenses:
- sources: LICENSE
+2 -2
View File
@@ -1,10 +1,10 @@
---
name: is-unsafe
version: 2.0.0
version: 1.0.1
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
-66
View File
@@ -1,66 +0,0 @@
---
name: minimatch
version: 10.2.5
type: npm
summary: a glob matcher in javascript
homepage:
license: blueoak-1.0.0
licenses:
- sources: LICENSE.md
text: |
# Blue Oak Model License
Version 1.0.0
## Purpose
This license gives everyone as much permission to work with
this software as possible, while protecting contributors
from liability.
## Acceptance
In order to receive this license, you must agree to its
rules. The rules of this license are both obligations
under that agreement and conditions to your license.
You must not do anything with this software that triggers
a rule that you cannot or will not follow.
## Copyright
Each contributor licenses you to do everything with this
software that would otherwise infringe that contributor's
copyright in it.
## Notices
You must ensure that everyone who gets a copy of
any part of this software from you, with or without
changes, also gets the text of this license or a link to
<https://blueoakcouncil.org/license/1.0.0>.
## Excuse
If anyone notifies you in writing that you have not
complied with [Notices](#notices), you can keep your
license by taking all practical steps to comply within 30
days after the notice. If you do not do so, your license
ends immediately.
## Patent
Each contributor licenses you to do everything with this
software that would otherwise infringe any patent claims
they can license or become able to license.
## Reliability
No contributor can revoke this license.
## No Liability
**_As far as the law allows, this software comes as is,
without any warranty or condition, and no contributor
will be liable to anyone for any damages related to this
software or this license, under any kind of legal claim._**
notices: []
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: path-expression-matcher
version: 1.6.2
version: 1.5.0
type: npm
summary: Efficient path tracking and pattern matching for XML/JSON parsers
homepage: https://github.com/NaturalIntelligence/path-expression-matcher#readme
+26
View File
@@ -0,0 +1,26 @@
---
name: semver
version: 6.3.1
type: npm
summary: The semantic version parser used by npm.
homepage:
license: isc
licenses:
- sources: LICENSE
text: |
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
notices: []
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: undici
version: 6.28.0
version: 6.27.0
type: npm
summary: An HTTP/1.1 client, written from scratch for Node.js
homepage: https://undici.nodejs.org
@@ -4,7 +4,7 @@ version: 0.1.0
type: npm
summary: Validates XML name productions — Name, NCName, QName, NMToken, NMTokens —
for XML 1.0 and 1.1
homepage:
homepage:
license: mit
licenses:
- sources: README.md
+11
View File
@@ -0,0 +1,11 @@
// This is a reusable configuration file copied from https://github.com/actions/reusable-workflows/tree/main/reusable-configurations. Please don't make changes to this file as it's the subject of an automatic update.
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
trailingComma: 'none',
bracketSpacing: false,
arrowParens: 'avoid'
};
+11 -14
View File
@@ -11,10 +11,6 @@ This action provides the following functionality for GitHub Actions users:
- Optionally caching dependencies for pip, pipenv and poetry
- Registering problem matchers for error output
## What's new in V7
- Migrated action internals to ESM for compatibility with latest `@actions/*` packages. No changes to action inputs, outputs, or behavior.
## Breaking changes in V6
- Upgraded action from node20 to node24
@@ -29,8 +25,8 @@ See [action.yml](action.yml)
**Python**
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.13'
- run: python my_script.py
@@ -39,8 +35,8 @@ steps:
**PyPy**
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 'pypy3.10'
- run: python my_script.py
@@ -49,8 +45,8 @@ steps:
**GraalPy**
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 'graalpy-24.0'
- run: python my_script.py
@@ -59,8 +55,8 @@ steps:
**Free threaded Python**
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.13t'
- run: python my_script.py
@@ -94,8 +90,8 @@ The action defaults to searching for a dependency file (`requirements.txt` or `p
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pip' # caching pip dependencies
@@ -122,6 +118,7 @@ See examples of using `cache` and `cache-dependency-path` for `pipenv` and `poet
- [Using `setup-python` on GHES](docs/advanced-usage.md#using-setup-python-on-ghes)
- [Allow pre-releases](docs/advanced-usage.md#allow-pre-releases)
- [Using the pip-version input](docs/advanced-usage.md#using-the-pip-version-input)
- [Using the pip-install input](docs/advanced-usage.md#using-the-pip-install-input)
## Recommended permissions
+50 -109
View File
@@ -1,62 +1,10 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {fileURLToPath} from 'url';
import * as path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Mock @actions modules before importing anything that depends on them
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)
}));
jest.unstable_mockModule('@actions/cache', () => ({
saveCache: jest.fn(),
restoreCache: jest.fn(),
isFeatureAvailable: jest.fn()
}));
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
jest.unstable_mockModule('@actions/io', () => ({
which: jest.fn(),
mkdirP: jest.fn(),
rmRF: jest.fn(),
mv: jest.fn(),
cp: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const cache = await import('@actions/cache');
const exec = await import('@actions/exec');
const io = await import('@actions/io');
const {getCacheDistributor} =
await import('../src/cache-distributions/cache-factory.js');
const {State} = await import('../src/cache-distributions/cache-distributor.js');
import * as core from '@actions/core';
import * as cache from '@actions/cache';
import * as exec from '@actions/exec';
import * as io from '@actions/io';
import {getCacheDistributor} from '../src/cache-distributions/cache-factory';
import {State} from '../src/cache-distributions/cache-distributor';
describe('restore-cache', () => {
const pipFileLockHash =
@@ -76,38 +24,43 @@ virtualenvs.in-project = true
virtualenvs.path = "{cache-dir}/virtualenvs" # /Users/patrick/Library/Caches/pypoetry/virtualenvs
`;
let infoSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let saveStateSpy: jest.Mock;
let getStateSpy: jest.Mock;
let setOutputSpy: jest.Mock;
let restoreCacheSpy: jest.Mock;
let getExecOutputSpy: jest.Mock;
let whichSpy: jest.Mock;
// core spy
let infoSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let saveStateSpy: jest.SpyInstance;
let getStateSpy: jest.SpyInstance;
let setOutputSpy: jest.SpyInstance;
// cache spy
let restoreCacheSpy: jest.SpyInstance;
// exec spy
let getExecOutputSpy: jest.SpyInstance;
// io spy
let whichSpy: jest.SpyInstance;
beforeEach(() => {
process.env['RUNNER_OS'] = process.env['RUNNER_OS'] ?? 'linux';
infoSpy = core.info as jest.Mock;
infoSpy.mockImplementation(() => undefined);
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(input => undefined);
warningSpy = core.warning as jest.Mock;
warningSpy.mockImplementation(() => undefined);
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(input => undefined);
debugSpy = core.debug as jest.Mock;
debugSpy.mockImplementation(() => undefined);
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(input => undefined);
saveStateSpy = core.saveState as jest.Mock;
saveStateSpy.mockImplementation(() => undefined);
saveStateSpy = jest.spyOn(core, 'saveState');
saveStateSpy.mockImplementation(input => undefined);
getStateSpy = core.getState as jest.Mock;
getStateSpy.mockImplementation(() => undefined);
getStateSpy = jest.spyOn(core, 'getState');
getStateSpy.mockImplementation(input => undefined);
getExecOutputSpy = exec.getExecOutput as jest.Mock;
(
getExecOutputSpy as jest.Mock<typeof exec.getExecOutput>
).mockImplementation(async (input: string) => {
getExecOutputSpy = jest.spyOn(exec, 'getExecOutput');
getExecOutputSpy.mockImplementation((input: string) => {
if (input.includes('pip')) {
return {stdout: 'pip', stderr: '', exitCode: 0};
}
@@ -121,19 +74,17 @@ virtualenvs.path = "{cache-dir}/virtualenvs" # /Users/patrick/Library/Caches/py
return {stdout: '', stderr: 'Error occured', exitCode: 2};
});
setOutputSpy = core.setOutput as jest.Mock;
setOutputSpy.mockImplementation(() => undefined);
setOutputSpy = jest.spyOn(core, 'setOutput');
setOutputSpy.mockImplementation(input => undefined);
restoreCacheSpy = cache.restoreCache as jest.Mock;
(
restoreCacheSpy as jest.Mock<typeof cache.restoreCache>
).mockImplementation(
(cachePaths: string[], primaryKey: string, restoreKey?: string[]) => {
return Promise.resolve(primaryKey);
restoreCacheSpy = jest.spyOn(cache, 'restoreCache');
restoreCacheSpy.mockImplementation(
(cachePaths: string[], primaryKey: string, restoreKey?: string) => {
return primaryKey;
}
);
whichSpy = io.which as jest.Mock;
whichSpy = jest.spyOn(io, 'which');
whichSpy.mockImplementation(() => '/path/to/python');
});
@@ -212,13 +163,9 @@ virtualenvs.path = "{cache-dir}/virtualenvs" # /Users/patrick/Library/Caches/py
fileHash,
cachePaths
) => {
(
restoreCacheSpy as jest.Mock<typeof cache.restoreCache>
).mockImplementation(
(cachePaths: string[], primaryKey: string, restoreKey?: string[]) => {
return Promise.resolve(
primaryKey.includes(fileHash) ? primaryKey : ''
);
restoreCacheSpy.mockImplementation(
(cachePaths: string[], primaryKey: string, restoreKey?: string) => {
return primaryKey.includes(fileHash) ? primaryKey : '';
}
);
@@ -237,8 +184,8 @@ virtualenvs.path = "{cache-dir}/virtualenvs" # /Users/patrick/Library/Caches/py
);
}
const restoredKeys = await Promise.all(
restoreCacheSpy.mock.results.map(result => result.value)
const restoredKeys = restoreCacheSpy.mock.results.map(
result => result.value
);
restoredKeys.forEach(restoredKey => {
@@ -276,9 +223,7 @@ 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(
','
)}] for ${packageManager}. Make sure you have checked out the target repository, or consider removing the cache step if there are no dependencies to cache.`
.join(',')}], make sure you have checked out the target repository`
);
}
);
@@ -339,13 +284,9 @@ virtualenvs.path = "{cache-dir}/virtualenvs" # /Users/patrick/Library/Caches/py
])(
'restored dependencies for %s by primaryKey',
async (packageManager, pythonVersion, dependencyFile, fileHash) => {
(
restoreCacheSpy as jest.Mock<typeof cache.restoreCache>
).mockImplementation(
(cachePaths: string[], primaryKey: string, restoreKey?: string[]) => {
return Promise.resolve(
primaryKey !== fileHash && restoreKey ? pipFileLockHash : ''
);
restoreCacheSpy.mockImplementation(
(cachePaths: string[], primaryKey: string, restoreKey?: string) => {
return primaryKey !== fileHash && restoreKey ? pipFileLockHash : '';
}
);
const cacheDistributor = getCacheDistributor(
+79 -143
View File
@@ -1,61 +1,8 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {fileURLToPath} from 'url';
import path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Mock @actions modules before importing anything that depends on them
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)
}));
class MockValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}
jest.unstable_mockModule('@actions/cache', () => ({
saveCache: jest.fn(),
restoreCache: jest.fn(),
isFeatureAvailable: jest.fn(),
ValidationError: MockValidationError,
ReserveCacheError: MockValidationError
}));
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const cache = await import('@actions/cache');
const exec = await import('@actions/exec');
const {run} = await import('../src/cache-save.js');
const {State} = await import('../src/cache-distributions/cache-distributor.js');
import * as core from '@actions/core';
import * as cache from '@actions/cache';
import * as exec from '@actions/exec';
import {run} from '../src/cache-save';
import {State} from '../src/cache-distributions/cache-distributor';
describe('run', () => {
const pipFileLockHash =
@@ -67,54 +14,53 @@ describe('run', () => {
const poetryLockHash =
'571bf984f8d210e6a97f854e479fdd4a2b5af67b5fdac109ec337a0ea16e7836';
let infoSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let saveStateSpy: jest.Mock;
let getStateSpy: jest.Mock;
let getInputSpy: jest.Mock;
let setFailedSpy: jest.Mock;
let saveCacheSpy: jest.Mock;
let getExecOutputSpy: jest.Mock;
// core spy
let infoSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let saveStateSpy: jest.SpyInstance;
let getStateSpy: jest.SpyInstance;
let getInputSpy: jest.SpyInstance;
let setFailedSpy: jest.SpyInstance;
// cache spy
let saveCacheSpy: jest.SpyInstance;
// exec spy
let getExecOutputSpy: jest.SpyInstance;
let inputs = {} as any;
beforeEach(() => {
process.env['RUNNER_OS'] = process.env['RUNNER_OS'] ?? 'linux';
infoSpy = core.info as jest.Mock;
infoSpy.mockImplementation(() => undefined);
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(input => undefined);
warningSpy = core.warning as jest.Mock;
warningSpy.mockImplementation(() => undefined);
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(input => undefined);
debugSpy = core.debug as jest.Mock;
debugSpy.mockImplementation(() => undefined);
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(input => undefined);
saveStateSpy = core.saveState as jest.Mock;
saveStateSpy.mockImplementation(() => undefined);
saveStateSpy = jest.spyOn(core, 'saveState');
saveStateSpy.mockImplementation(input => undefined);
getStateSpy = core.getState as jest.Mock;
(getStateSpy as jest.Mock<typeof core.getState>).mockImplementation(
(input: string) => {
if (input === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
}
return requirementsHash;
getStateSpy = jest.spyOn(core, 'getState');
getStateSpy.mockImplementation(input => {
if (input === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
}
);
return requirementsHash;
});
setFailedSpy = core.setFailed as jest.Mock;
setFailedSpy = jest.spyOn(core, 'setFailed');
getInputSpy = core.getInput as jest.Mock;
(getInputSpy as jest.Mock<typeof core.getInput>).mockImplementation(
(input: string) => inputs[input]
);
getInputSpy = jest.spyOn(core, 'getInput');
getInputSpy.mockImplementation(input => inputs[input]);
getExecOutputSpy = exec.getExecOutput as jest.Mock;
(
getExecOutputSpy as jest.Mock<typeof exec.getExecOutput>
).mockImplementation(async (input: string) => {
getExecOutputSpy = jest.spyOn(exec, 'getExecOutput');
getExecOutputSpy.mockImplementation((input: string) => {
if (input.includes('pip')) {
return {stdout: 'pip', stderr: '', exitCode: 0};
}
@@ -122,7 +68,7 @@ describe('run', () => {
return {stdout: '', stderr: 'Error occured', exitCode: 2};
});
saveCacheSpy = cache.saveCache as jest.Mock;
saveCacheSpy = jest.spyOn(cache, 'saveCache');
saveCacheSpy.mockImplementation(() => undefined);
});
@@ -178,17 +124,15 @@ describe('run', () => {
it('saves cache from pip', async () => {
inputs['cache'] = 'pip';
inputs['python-version'] = '3.10.0';
(getStateSpy as jest.Mock<typeof core.getState>).mockImplementation(
(name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return requirementsHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return pipFileLockHash;
}
getStateSpy.mockImplementation((name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return requirementsHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return pipFileLockHash;
}
);
});
await run();
@@ -207,17 +151,15 @@ describe('run', () => {
it('saves cache from pipenv', async () => {
inputs['cache'] = 'pipenv';
inputs['python-version'] = '3.10.0';
(getStateSpy as jest.Mock<typeof core.getState>).mockImplementation(
(name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return pipFileLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
}
getStateSpy.mockImplementation((name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return pipFileLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
}
);
});
await run();
@@ -236,17 +178,15 @@ describe('run', () => {
it('saves cache from poetry', async () => {
inputs['cache'] = 'poetry';
inputs['python-version'] = '3.10.0';
(getStateSpy as jest.Mock<typeof core.getState>).mockImplementation(
(name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return poetryLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
}
getStateSpy.mockImplementation((name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return poetryLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
}
);
});
await run();
@@ -265,17 +205,15 @@ describe('run', () => {
it('saves with -1 cacheId , should not fail workflow', async () => {
inputs['cache'] = 'poetry';
inputs['python-version'] = '3.10.0';
(getStateSpy as jest.Mock<typeof core.getState>).mockImplementation(
(name: string) => {
if (name === State.STATE_CACHE_PRIMARY_KEY) {
return poetryLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
}
getStateSpy.mockImplementation((name: string) => {
if (name === State.STATE_CACHE_PRIMARY_KEY) {
return poetryLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
}
);
});
saveCacheSpy.mockImplementation(() => {
return -1;
@@ -296,17 +234,15 @@ describe('run', () => {
it('saves with error from toolkit, should not fail the workflow', async () => {
inputs['cache'] = 'npm';
inputs['python-version'] = '3.10.0';
(getStateSpy as jest.Mock<typeof core.getState>).mockImplementation(
(name: string) => {
if (name === State.STATE_CACHE_PRIMARY_KEY) {
return poetryLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
}
getStateSpy.mockImplementation((name: string) => {
if (name === State.STATE_CACHE_PRIMARY_KEY) {
return poetryLockHash;
} else if (name === State.CACHE_PATHS) {
return JSON.stringify([__dirname]);
} else {
return requirementsHash;
}
);
});
saveCacheSpy.mockImplementation(() => {
throw new cache.ValidationError('Validation failed');
+1 -1
View File
@@ -5,7 +5,7 @@ name = "pypi"
[packages]
flake8 = "==6.0.0"
numpy = "==2.4.6"
numpy = "==2.2.4"
[dev-packages]
+58 -75
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "0060d5c4915f1f35aae1f3e9fa6de3f65d1a851e6e33ac57a04665d5d1c796fa"
"sha256": "9d0bc7ff1f9078396b19f05bc5cd57894f3783c68f96fa9d623b1ea5a1b168f5"
},
"pipfile-spec": 6,
"requires": {
@@ -35,82 +35,65 @@
},
"numpy": {
"hashes": [
"sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1",
"sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4",
"sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f",
"sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079",
"sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096",
"sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47",
"sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66",
"sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d",
"sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1",
"sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e",
"sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147",
"sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd",
"sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75",
"sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063",
"sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73",
"sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab",
"sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4",
"sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41",
"sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402",
"sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698",
"sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7",
"sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8",
"sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b",
"sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8",
"sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0",
"sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662",
"sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91",
"sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0",
"sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f",
"sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3",
"sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f",
"sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67",
"sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6",
"sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997",
"sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b",
"sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e",
"sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538",
"sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627",
"sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93",
"sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02",
"sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853",
"sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c",
"sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43",
"sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd",
"sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8",
"sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089",
"sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778",
"sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1",
"sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb",
"sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261",
"sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb",
"sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a",
"sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8",
"sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359",
"sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5",
"sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7",
"sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751",
"sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8",
"sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605",
"sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e",
"sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45",
"sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2",
"sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895",
"sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe",
"sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb",
"sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a",
"sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577",
"sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d",
"sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a",
"sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda",
"sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6",
"sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20"
"sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286",
"sha256:0d54974f9cf14acf49c60f0f7f4084b6579d24d439453d5fc5805d46a165b542",
"sha256:11c43995255eb4127115956495f43e9343736edb7fcdb0d973defd9de14cd84f",
"sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d",
"sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0",
"sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7",
"sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3",
"sha256:218f061d2faa73621fa23d6359442b0fc658d5b9a70801373625d958259eaca3",
"sha256:2aad3c17ed2ff455b8eaafe06bcdae0062a1db77cb99f4b9cbb5f4ecb13c5146",
"sha256:2fa8fa7697ad1646b5c93de1719965844e004fcad23c91228aca1cf0800044a1",
"sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6",
"sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc",
"sha256:4ba5054787e89c59c593a4169830ab362ac2bee8a969249dc56e5d7d20ff8df9",
"sha256:4f92084defa704deadd4e0a5ab1dc52d8ac9e8a8ef617f3fbb853e79b0ea3592",
"sha256:65ef3468b53269eb5fdb3a5c09508c032b793da03251d5f8722b1194f1790c00",
"sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298",
"sha256:7051ee569db5fbac144335e0f3b9c2337e0c8d5c9fee015f259a5bd70772b7e8",
"sha256:7716e4a9b7af82c06a2543c53ca476fa0b57e4d760481273e09da04b74ee6ee2",
"sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392",
"sha256:7a4e84a6283b36632e2a5b56e121961f6542ab886bc9e12f8f9818b3c266bfbb",
"sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8",
"sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd",
"sha256:8146f3550d627252269ac42ae660281d673eb6f8b32f113538e0cc2a9aed42b9",
"sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0",
"sha256:892c10d6a73e0f14935c31229e03325a7b3093fafd6ce0af704be7f894d95687",
"sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc",
"sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f",
"sha256:9eeea959168ea555e556b8188da5fa7831e21d91ce031e95ce23747b7609f8a4",
"sha256:a0258ad1f44f138b791327961caedffbf9612bfa504ab9597157806faa95194a",
"sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39",
"sha256:a7b9084668aa0f64e64bd00d27ba5146ef1c3a8835f3bd912e7a9e01326804c4",
"sha256:a84eda42bd12edc36eb5b53bbcc9b406820d3353f1994b6cfe453a33ff101775",
"sha256:ab2939cd5bec30a7430cbdb2287b63151b77cf9624de0532d629c9a1c59b1d5c",
"sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd",
"sha256:adf8c1d66f432ce577d0197dceaac2ac00c0759f573f28516246351c58a85020",
"sha256:b4adfbbc64014976d2f91084915ca4e626fbf2057fb81af209c1a6d776d23e3d",
"sha256:bb649f8b207ab07caebba230d851b579a3c8711a851d29efe15008e31bb4de24",
"sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7",
"sha256:bd3ad3b0a40e713fc68f99ecfd07124195333f1e689387c180813f0e94309d6f",
"sha256:c3f7ac96b16955634e223b579a3e5798df59007ca43e8d451a0e6a50f6bfdfba",
"sha256:cf28633d64294969c019c6df4ff37f5698e8326db68cc2b66576a51fad634880",
"sha256:d0f35b19894a9e08639fd60a1ec1978cb7f5f7f1eace62f38dd36be8aecdef4d",
"sha256:db1f1c22173ac1c58db249ae48aa7ead29f534b9a948bc56828337aa84a32ed6",
"sha256:dbe512c511956b893d2dacd007d955a3f03d555ae05cfa3ff1c1ff6df8851854",
"sha256:df2f57871a96bbc1b69733cd4c51dc33bea66146b8c63cacbfed73eec0883017",
"sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8",
"sha256:e642d86b8f956098b564a45e6f6ce68a22c2c97a04f5acd3f221f57b8cb850ae",
"sha256:e9e0a277bb2eb5d8a7407e14688b85fd8ad628ee4e0c7930415687b6564207a4",
"sha256:ea2bb7e2ae9e37d96835b3576a4fa4b3a97592fbea8ef7c3587078b0068b8f09",
"sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff",
"sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960",
"sha256:f34dc300df798742b3d06515aa2a0aee20941c13579d7a2f2e10af01ae4901ee",
"sha256:f4162988a360a29af158aeb4a2f4f09ffed6a969c9776f8f3bdee9b06a8ab7e5",
"sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c",
"sha256:f7de08cbe5551911886d1ab60de58448c6df0f67d9feb7d1fb21e9875ef95e91"
],
"index": "pypi",
"markers": "python_version >= '3.11'",
"version": "==2.4.6"
"markers": "python_version >= '3.10'",
"version": "==2.2.4"
},
"pycodestyle": {
"hashes": [
+84 -141
View File
@@ -1,70 +1,18 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {fileURLToPath} from 'url';
import fs from 'fs';
import * as path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
import {HttpClient} from '@actions/http-client';
import * as ifm from '@actions/http-client/lib/interfaces';
import * as tc from '@actions/tool-cache';
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import * as path from 'path';
import * as semver from 'semver';
// Mock @actions modules
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)
}));
import * as finder from '../src/find-graalpy';
import {IGraalPyManifestRelease} from '../src/utils';
jest.unstable_mockModule('@actions/tool-cache', () => ({
find: jest.fn(),
findAllVersions: jest.fn(),
downloadTool: jest.fn(),
extractZip: jest.fn(),
extractTar: jest.fn(),
extract7z: jest.fn(),
extractXar: jest.fn(),
cacheDir: jest.fn(),
cacheFile: jest.fn(),
getManifestFromRepo: jest.fn(),
findFromManifest: jest.fn(),
evaluateVersions: jest.fn()
}));
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const exec = await import('@actions/exec');
const finder = await import('../src/find-graalpy.js');
const utils = await import('../src/utils.js');
// Non-mocked imports
import {HttpClient} from '@actions/http-client';
import type * as ifm from '@actions/http-client/lib/interfaces';
import type {IGraalPyManifestRelease} from '../src/utils.js';
import manifestData from './data/graalpy.json' with {type: 'json'};
import manifestData from './data/graalpy.json';
const architecture = 'x64';
@@ -93,41 +41,39 @@ describe('parseGraalPyVersion', () => {
describe('findGraalPyToolCache', () => {
const actualGraalPyVersion = '23.0.0';
const graalpyPath = path.join('GraalPy', actualGraalPyVersion, architecture);
let tcFind: jest.Mock;
let infoSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let addPathSpy: jest.Mock;
let exportVariableSpy: jest.Mock;
let setOutputSpy: jest.Mock;
let tcFind: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let addPathSpy: jest.SpyInstance;
let exportVariableSpy: jest.SpyInstance;
let setOutputSpy: jest.SpyInstance;
beforeEach(() => {
tcFind = tc.find as jest.Mock;
(tcFind as jest.Mock<typeof tc.find>).mockImplementation(
(toolname: string, pythonVersion: string) => {
const semverVersion = new semver.Range(pythonVersion);
return semver.satisfies(actualGraalPyVersion, semverVersion)
? graalpyPath
: '';
}
);
tcFind = jest.spyOn(tc, 'find');
tcFind.mockImplementation((toolname: string, pythonVersion: string) => {
const semverVersion = new semver.Range(pythonVersion);
return semver.satisfies(actualGraalPyVersion, semverVersion)
? graalpyPath
: '';
});
infoSpy = core.info as jest.Mock;
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => null);
warningSpy = core.warning as jest.Mock;
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = core.debug as jest.Mock;
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
addPathSpy = core.addPath as jest.Mock;
addPathSpy = jest.spyOn(core, 'addPath');
addPathSpy.mockImplementation(() => null);
exportVariableSpy = core.exportVariable as jest.Mock;
exportVariableSpy = jest.spyOn(core, 'exportVariable');
exportVariableSpy.mockImplementation(() => null);
setOutputSpy = core.setOutput as jest.Mock;
setOutputSpy = jest.spyOn(core, 'setOutput');
setOutputSpy.mockImplementation(() => null);
});
@@ -160,74 +106,73 @@ describe('findGraalPyToolCache', () => {
});
describe('findGraalPyVersion', () => {
let getBooleanInputSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let infoSpy: jest.Mock;
let addPathSpy: jest.Mock;
let exportVariableSpy: jest.Mock;
let setOutputSpy: jest.Mock;
let tcFind: jest.Mock;
let spyExtractZip: jest.Mock;
let spyExtractTar: jest.Mock;
let spyHttpClient: jest.SpiedFunction<typeof HttpClient.prototype.getJson>;
let spyExistsSync: jest.SpiedFunction<typeof fs.existsSync>;
let spyExec: jest.Mock;
let spySymlinkSync: jest.SpiedFunction<typeof fs.symlinkSync>;
let spyDownloadTool: jest.Mock;
let spyFsReadDir: jest.SpiedFunction<typeof fs.readdirSync>;
let spyCacheDir: jest.Mock;
let spyChmodSync: jest.SpiedFunction<typeof fs.chmodSync>;
let spyCoreAddPath: jest.Mock;
let spyCoreExportVariable: jest.Mock;
let getBooleanInputSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let addPathSpy: jest.SpyInstance;
let exportVariableSpy: jest.SpyInstance;
let setOutputSpy: jest.SpyInstance;
let tcFind: jest.SpyInstance;
let spyExtractZip: jest.SpyInstance;
let spyExtractTar: jest.SpyInstance;
let spyHttpClient: jest.SpyInstance;
let spyExistsSync: jest.SpyInstance;
let spyExec: jest.SpyInstance;
let spySymlinkSync: jest.SpyInstance;
let spyDownloadTool: jest.SpyInstance;
let spyFsReadDir: jest.SpyInstance;
let spyCacheDir: jest.SpyInstance;
let spyChmodSync: jest.SpyInstance;
let spyCoreAddPath: jest.SpyInstance;
let spyCoreExportVariable: jest.SpyInstance;
const env = process.env;
beforeEach(() => {
getBooleanInputSpy = core.getBooleanInput as jest.Mock;
getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
getBooleanInputSpy.mockImplementation(() => false);
infoSpy = core.info as jest.Mock;
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => {});
warningSpy = core.warning as jest.Mock;
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = core.debug as jest.Mock;
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
addPathSpy = core.addPath as jest.Mock;
addPathSpy = jest.spyOn(core, 'addPath');
addPathSpy.mockImplementation(() => null);
exportVariableSpy = core.exportVariable as jest.Mock;
exportVariableSpy = jest.spyOn(core, 'exportVariable');
exportVariableSpy.mockImplementation(() => null);
setOutputSpy = core.setOutput as jest.Mock;
setOutputSpy = jest.spyOn(core, 'setOutput');
setOutputSpy.mockImplementation(() => null);
jest.resetModules();
process.env = {...env};
tcFind = tc.find as jest.Mock;
(tcFind as jest.Mock<typeof tc.find>).mockImplementation(
(tool: string, version: string) => {
const semverRange = new semver.Range(version);
let graalpyPath = '';
if (semver.satisfies('23.0.0', semverRange)) {
graalpyPath = path.join(toolDir, 'GraalPy', '23.0.0', architecture);
}
return graalpyPath;
tcFind = jest.spyOn(tc, 'find');
tcFind.mockImplementation((tool: string, version: string) => {
const semverRange = new semver.Range(version);
let graalpyPath = '';
if (semver.satisfies('23.0.0', semverRange)) {
graalpyPath = path.join(toolDir, 'GraalPy', '23.0.0', architecture);
}
);
return graalpyPath;
});
spyDownloadTool = tc.downloadTool as jest.Mock;
spyDownloadTool = jest.spyOn(tc, 'downloadTool');
spyDownloadTool.mockImplementation(() => path.join(tempDir, 'GraalPy'));
spyExtractZip = tc.extractZip as jest.Mock;
spyExtractZip = jest.spyOn(tc, 'extractZip');
spyExtractZip.mockImplementation(() => tempDir);
spyExtractTar = tc.extractTar as jest.Mock;
spyExtractTar = jest.spyOn(tc, 'extractTar');
spyExtractTar.mockImplementation(() => tempDir);
spyFsReadDir = jest.spyOn(fs, 'readdirSync');
spyFsReadDir.mockImplementation(() => ['GraalPyTest'] as any);
spyFsReadDir.mockImplementation((directory: string) => ['GraalPyTest']);
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient.mockImplementation(
@@ -241,7 +186,7 @@ describe('findGraalPyVersion', () => {
}
);
spyExec = exec.exec as jest.Mock;
spyExec = jest.spyOn(exec, 'exec');
spyExec.mockImplementation(() => undefined);
spySymlinkSync = jest.spyOn(fs, 'symlinkSync');
@@ -250,9 +195,9 @@ describe('findGraalPyVersion', () => {
spyExistsSync = jest.spyOn(fs, 'existsSync');
spyExistsSync.mockReturnValue(true);
spyCoreAddPath = core.addPath as jest.Mock;
spyCoreAddPath = jest.spyOn(core, 'addPath');
spyCoreExportVariable = core.exportVariable as jest.Mock;
spyCoreExportVariable = jest.spyOn(core, 'exportVariable');
});
afterEach(() => {
@@ -290,7 +235,7 @@ describe('findGraalPyVersion', () => {
});
it('found and install successfully', async () => {
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.0.0', architecture)
);
@@ -317,7 +262,7 @@ describe('findGraalPyVersion', () => {
});
it('found and install successfully without environment update', async () => {
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.0.0', architecture)
);
@@ -365,7 +310,7 @@ describe('findGraalPyVersion', () => {
});
it('check-latest enabled version found and install successfully', async () => {
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.0.0', architecture)
);
@@ -384,16 +329,14 @@ describe('findGraalPyVersion', () => {
});
it('check-latest enabled version is not found and used from toolcache', async () => {
(tcFind as jest.Mock<typeof tc.find>).mockImplementationOnce(
(tool: string, version: string) => {
const semverRange = new semver.Range(version);
let graalpyPath = '';
if (semver.satisfies('22.3.4', semverRange)) {
graalpyPath = path.join(toolDir, 'GraalPy', '22.3.4', architecture);
}
return graalpyPath;
tcFind.mockImplementationOnce((tool: string, version: string) => {
const semverRange = new semver.Range(version);
let graalpyPath = '';
if (semver.satisfies('22.3.4', semverRange)) {
graalpyPath = path.join(toolDir, 'GraalPy', '22.3.4', architecture);
}
);
return graalpyPath;
});
await expect(
finder.findGraalPyVersion(
'graalpy-22.3.4',
@@ -410,7 +353,7 @@ describe('findGraalPyVersion', () => {
});
it('found and install successfully, pre-release fallback', async () => {
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '24.1', architecture)
);
+98 -160
View File
@@ -1,83 +1,23 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {fileURLToPath} from 'url';
import fs from 'fs';
import * as path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
import * as utils from '../src/utils';
import {HttpClient} from '@actions/http-client';
import * as ifm from '@actions/http-client/lib/interfaces';
import * as tc from '@actions/tool-cache';
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import * as path from 'path';
import * as semver from 'semver';
// Mock @actions modules
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)
}));
import * as finder from '../src/find-pypy';
import {
IPyPyManifestRelease,
IS_WINDOWS,
getPyPyVersionFromPath
} from '../src/utils';
jest.unstable_mockModule('@actions/tool-cache', () => ({
find: jest.fn(),
findAllVersions: jest.fn(),
downloadTool: jest.fn(),
extractZip: jest.fn(),
extractTar: jest.fn(),
extract7z: jest.fn(),
extractXar: jest.fn(),
cacheDir: jest.fn(),
cacheFile: jest.fn(),
getManifestFromRepo: jest.fn(),
findFromManifest: jest.fn(),
evaluateVersions: jest.fn()
}));
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');
// Mock local utils module for readExactPyPyVersionFile/writeExactPyPyVersionFile
jest.unstable_mockModule('../src/utils.js', () => ({
...realUtils,
readExactPyPyVersionFile: jest.fn(),
writeExactPyPyVersionFile: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const exec = await import('@actions/exec');
const utils = await import('../src/utils.js');
const finder = await import('../src/find-pypy.js');
// Non-mocked imports
import {HttpClient} from '@actions/http-client';
import type * as ifm from '@actions/http-client/lib/interfaces';
import type {IPyPyManifestRelease} from '../src/utils.js';
import manifestData from './data/pypy.json' with {type: 'json'};
const IS_WINDOWS = utils.IS_WINDOWS;
const getPyPyVersionFromPath = utils.getPyPyVersionFromPath;
import manifestData from './data/pypy.json';
let architecture: string;
@@ -139,45 +79,43 @@ describe('findPyPyToolCache', () => {
const actualPythonVersion = '3.6.17';
const actualPyPyVersion = '7.5.4';
const pypyPath = path.join('PyPy', actualPythonVersion, architecture);
let tcFind: jest.Mock;
let spyReadExactPyPyVersion: jest.Mock;
let infoSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let addPathSpy: jest.Mock;
let exportVariableSpy: jest.Mock;
let setOutputSpy: jest.Mock;
let tcFind: jest.SpyInstance;
let spyReadExactPyPyVersion: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let addPathSpy: jest.SpyInstance;
let exportVariableSpy: jest.SpyInstance;
let setOutputSpy: jest.SpyInstance;
beforeEach(() => {
tcFind = tc.find as jest.Mock;
(tcFind as jest.Mock<typeof tc.find>).mockImplementation(
(toolname: string, pythonVersion: string) => {
const semverVersion = new semver.Range(pythonVersion);
return semver.satisfies(actualPythonVersion, semverVersion)
? pypyPath
: '';
}
);
tcFind = jest.spyOn(tc, 'find');
tcFind.mockImplementation((toolname: string, pythonVersion: string) => {
const semverVersion = new semver.Range(pythonVersion);
return semver.satisfies(actualPythonVersion, semverVersion)
? pypyPath
: '';
});
spyReadExactPyPyVersion = utils.readExactPyPyVersionFile as jest.Mock;
spyReadExactPyPyVersion = jest.spyOn(utils, 'readExactPyPyVersionFile');
spyReadExactPyPyVersion.mockImplementation(() => actualPyPyVersion);
infoSpy = core.info as jest.Mock;
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => null);
warningSpy = core.warning as jest.Mock;
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = core.debug as jest.Mock;
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
addPathSpy = core.addPath as jest.Mock;
addPathSpy = jest.spyOn(core, 'addPath');
addPathSpy.mockImplementation(() => null);
exportVariableSpy = core.exportVariable as jest.Mock;
exportVariableSpy = jest.spyOn(core, 'exportVariable');
exportVariableSpy.mockImplementation(() => null);
setOutputSpy = core.setOutput as jest.Mock;
setOutputSpy = jest.spyOn(core, 'setOutput');
setOutputSpy.mockImplementation(() => null);
});
@@ -221,82 +159,84 @@ describe('findPyPyToolCache', () => {
});
describe('findPyPyVersion', () => {
let getBooleanInputSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let infoSpy: jest.Mock;
let addPathSpy: jest.Mock;
let exportVariableSpy: jest.Mock;
let setOutputSpy: jest.Mock;
let tcFind: jest.Mock;
let spyExtractZip: jest.Mock;
let spyExtractTar: jest.Mock;
let spyHttpClient: jest.SpiedFunction<typeof HttpClient.prototype.getJson>;
let spyExistsSync: jest.SpiedFunction<typeof fs.existsSync>;
let spyExec: jest.Mock;
let spySymlinkSync: jest.SpiedFunction<typeof fs.symlinkSync>;
let spyDownloadTool: jest.Mock;
let spyReadExactPyPyVersion: jest.Mock;
let spyFsReadDir: jest.SpiedFunction<typeof fs.readdirSync>;
let spyWriteExactPyPyVersionFile: jest.Mock;
let spyCacheDir: jest.Mock;
let spyChmodSync: jest.SpiedFunction<typeof fs.chmodSync>;
let spyCoreAddPath: jest.Mock;
let spyCoreExportVariable: jest.Mock;
let getBooleanInputSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let addPathSpy: jest.SpyInstance;
let exportVariableSpy: jest.SpyInstance;
let setOutputSpy: jest.SpyInstance;
let tcFind: jest.SpyInstance;
let spyExtractZip: jest.SpyInstance;
let spyExtractTar: jest.SpyInstance;
let spyHttpClient: jest.SpyInstance;
let spyExistsSync: jest.SpyInstance;
let spyExec: jest.SpyInstance;
let spySymlinkSync: jest.SpyInstance;
let spyDownloadTool: jest.SpyInstance;
let spyReadExactPyPyVersion: jest.SpyInstance;
let spyFsReadDir: jest.SpyInstance;
let spyWriteExactPyPyVersionFile: jest.SpyInstance;
let spyCacheDir: jest.SpyInstance;
let spyChmodSync: jest.SpyInstance;
let spyCoreAddPath: jest.SpyInstance;
let spyCoreExportVariable: jest.SpyInstance;
const env = process.env;
beforeEach(() => {
getBooleanInputSpy = core.getBooleanInput as jest.Mock;
getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
getBooleanInputSpy.mockImplementation(() => false);
infoSpy = core.info as jest.Mock;
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => {});
warningSpy = core.warning as jest.Mock;
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = core.debug as jest.Mock;
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
addPathSpy = core.addPath as jest.Mock;
addPathSpy = jest.spyOn(core, 'addPath');
addPathSpy.mockImplementation(() => null);
exportVariableSpy = core.exportVariable as jest.Mock;
exportVariableSpy = jest.spyOn(core, 'exportVariable');
exportVariableSpy.mockImplementation(() => null);
setOutputSpy = core.setOutput as jest.Mock;
setOutputSpy = jest.spyOn(core, 'setOutput');
setOutputSpy.mockImplementation(() => null);
jest.resetModules();
process.env = {...env};
tcFind = tc.find as jest.Mock;
(tcFind as jest.Mock<typeof tc.find>).mockImplementation(
(tool: string, version: string) => {
const semverRange = new semver.Range(version);
let pypyPath = '';
if (semver.satisfies('3.6.12', semverRange)) {
pypyPath = path.join(toolDir, 'PyPy', '3.6.12', architecture);
}
return pypyPath;
tcFind = jest.spyOn(tc, 'find');
tcFind.mockImplementation((tool: string, version: string) => {
const semverRange = new semver.Range(version);
let pypyPath = '';
if (semver.satisfies('3.6.12', semverRange)) {
pypyPath = path.join(toolDir, 'PyPy', '3.6.12', architecture);
}
);
return pypyPath;
});
spyWriteExactPyPyVersionFile = utils.writeExactPyPyVersionFile as jest.Mock;
spyWriteExactPyPyVersionFile = jest.spyOn(
utils,
'writeExactPyPyVersionFile'
);
spyWriteExactPyPyVersionFile.mockImplementation(() => null);
spyReadExactPyPyVersion = utils.readExactPyPyVersionFile as jest.Mock;
spyReadExactPyPyVersion = jest.spyOn(utils, 'readExactPyPyVersionFile');
spyReadExactPyPyVersion.mockImplementation(() => '7.3.3');
spyDownloadTool = tc.downloadTool as jest.Mock;
spyDownloadTool = jest.spyOn(tc, 'downloadTool');
spyDownloadTool.mockImplementation(() => path.join(tempDir, 'PyPy'));
spyExtractZip = tc.extractZip as jest.Mock;
spyExtractZip = jest.spyOn(tc, 'extractZip');
spyExtractZip.mockImplementation(() => tempDir);
spyExtractTar = tc.extractTar as jest.Mock;
spyExtractTar = jest.spyOn(tc, 'extractTar');
spyExtractTar.mockImplementation(() => tempDir);
spyFsReadDir = jest.spyOn(fs, 'readdirSync');
spyFsReadDir.mockImplementation(() => ['PyPyTest'] as any);
spyFsReadDir.mockImplementation((directory: string) => ['PyPyTest']);
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient.mockImplementation(
@@ -310,7 +250,7 @@ describe('findPyPyVersion', () => {
}
);
spyExec = exec.exec as jest.Mock;
spyExec = jest.spyOn(exec, 'exec');
spyExec.mockImplementation(() => undefined);
spySymlinkSync = jest.spyOn(fs, 'symlinkSync');
@@ -319,9 +259,9 @@ describe('findPyPyVersion', () => {
spyExistsSync = jest.spyOn(fs, 'existsSync');
spyExistsSync.mockReturnValue(true);
spyCoreAddPath = core.addPath as jest.Mock;
spyCoreAddPath = jest.spyOn(core, 'addPath');
spyCoreExportVariable = core.exportVariable as jest.Mock;
spyCoreExportVariable = jest.spyOn(core, 'exportVariable');
});
afterEach(() => {
@@ -368,7 +308,7 @@ describe('findPyPyVersion', () => {
});
it('found and install successfully', async () => {
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'PyPy', '3.7.7', architecture)
);
@@ -398,7 +338,7 @@ describe('findPyPyVersion', () => {
});
it('found and install successfully without environment update', async () => {
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'PyPy', '3.7.7', architecture)
);
@@ -454,7 +394,7 @@ describe('findPyPyVersion', () => {
});
it('check-latest enabled version found and install successfully', async () => {
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'PyPy', '3.7.7', architecture)
);
@@ -478,16 +418,14 @@ describe('findPyPyVersion', () => {
});
it('check-latest enabled version is not found and used from toolcache', async () => {
(tcFind as jest.Mock<typeof tc.find>).mockImplementationOnce(
(tool: string, version: string) => {
const semverRange = new semver.Range(version);
let pypyPath = '';
if (semver.satisfies('3.8.8', semverRange)) {
pypyPath = path.join(toolDir, 'PyPy', '3.8.8', architecture);
}
return pypyPath;
tcFind.mockImplementationOnce((tool: string, version: string) => {
const semverRange = new semver.Range(version);
let pypyPath = '';
if (semver.satisfies('3.8.8', semverRange)) {
pypyPath = path.join(toolDir, 'PyPy', '3.8.8', architecture);
}
);
return pypyPath;
});
await expect(
finder.findPyPyVersion(
'pypy-3.8-v7.3.x',
@@ -507,7 +445,7 @@ describe('findPyPyVersion', () => {
});
it('found and install successfully, pre-release fallback', async () => {
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'PyPy', '3.8.12', architecture)
);
+1 -2
View File
@@ -1,5 +1,4 @@
import {describe, it, expect} from '@jest/globals';
import {desugarVersion, pythonVersionToSemantic} from '../src/find-python.js';
import {desugarVersion, pythonVersionToSemantic} from '../src/find-python';
describe('desugarVersion', () => {
it.each([
+59 -97
View File
@@ -1,12 +1,8 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {fileURLToPath} from 'url';
import * as io from '@actions/io';
import os from 'os';
import fs from 'fs';
import path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const toolDir = path.join(
__dirname,
'runner',
@@ -23,78 +19,26 @@ const tempDir = path.join(
process.env['RUNNER_TOOL_CACHE'] = toolDir;
process.env['RUNNER_TEMP'] = tempDir;
// Mock @actions modules
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)
}));
import * as tc from '@actions/tool-cache';
import * as core from '@actions/core';
import * as finder from '../src/find-python';
import * as installer from '../src/install-python';
// Pre-import real @actions/tool-cache before any mocks
const realTc = await import('@actions/tool-cache');
jest.unstable_mockModule('@actions/tool-cache', () => ({
...realTc,
find: jest.fn(realTc.find),
getManifestFromRepo: jest.fn()
}));
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
// Pre-import real install-python AFTER all its dependency mocks are registered
// so it captures the mocked @actions/tool-cache, @actions/core, @actions/exec
const realInstaller = await import('../src/install-python.js');
// Mock local install-python module - keep real getManifest/findReleaseFromManifest
jest.unstable_mockModule('../src/install-python.js', () => ({
...realInstaller,
installCpythonFromRelease: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const finder = await import('../src/find-python.js');
const installer = await import('../src/install-python.js');
import manifestData from './data/versions-manifest.json' with {type: 'json'};
import manifestData from './data/versions-manifest.json';
describe('Finder tests', () => {
let writeSpy: jest.SpiedFunction<typeof process.stdout.write>;
let spyCoreAddPath: jest.Mock;
let spyCoreExportVariable: jest.Mock;
let writeSpy: jest.SpyInstance;
let spyCoreAddPath: jest.SpyInstance;
let spyCoreExportVariable: jest.SpyInstance;
const env = process.env;
beforeEach(() => {
writeSpy = jest.spyOn(process.stdout, 'write');
writeSpy.mockImplementation(() => true);
writeSpy.mockImplementation(() => {});
jest.resetModules();
process.env = {...env};
spyCoreAddPath = core.addPath as jest.Mock;
spyCoreExportVariable = core.exportVariable as jest.Mock;
// Restore real tc.find default (cleared by jest.resetAllMocks)
(tc.find as jest.Mock).mockImplementation(realTc.find as any);
spyCoreAddPath = jest.spyOn(core, 'addPath');
spyCoreExportVariable = jest.spyOn(core, 'exportVariable');
});
afterEach(() => {
@@ -105,8 +49,8 @@ describe('Finder tests', () => {
});
it('Finds Python if it is installed', async () => {
const getBooleanInputSpy = core.getBooleanInput as jest.Mock;
getBooleanInputSpy.mockImplementation(() => false);
const getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
getBooleanInputSpy.mockImplementation(input => false);
const pythonDir: string = path.join(toolDir, 'Python', '3.0.0', 'x64');
await io.mkdirP(pythonDir);
@@ -135,13 +79,16 @@ describe('Finder tests', () => {
});
it('Finds stable Python version if it is not installed, but exists in the manifest', async () => {
const findSpy = tc.getManifestFromRepo as jest.Mock;
findSpy.mockImplementation(() => manifestData);
const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo');
findSpy.mockImplementation(() => <tc.IToolRelease[]>manifestData);
const getBooleanInputSpy = core.getBooleanInput as jest.Mock;
getBooleanInputSpy.mockImplementation(() => false);
const getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
getBooleanInputSpy.mockImplementation(input => false);
const installSpy = installer.installCpythonFromRelease as jest.Mock;
const installSpy: jest.SpyInstance = jest.spyOn(
installer,
'installCpythonFromRelease'
);
installSpy.mockImplementation(async () => {
const pythonDir: string = path.join(toolDir, 'Python', '1.2.3', 'x64');
await io.mkdirP(pythonDir);
@@ -166,13 +113,16 @@ describe('Finder tests', () => {
});
it('Finds pre-release Python version in the manifest', async () => {
const findSpy = tc.getManifestFromRepo as jest.Mock;
findSpy.mockImplementation(() => manifestData);
const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo');
findSpy.mockImplementation(() => <tc.IToolRelease[]>manifestData);
const getBooleanInputSpy = core.getBooleanInput as jest.Mock;
getBooleanInputSpy.mockImplementation(() => false);
const getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
getBooleanInputSpy.mockImplementation(input => false);
const installSpy = installer.installCpythonFromRelease as jest.Mock;
const installSpy: jest.SpyInstance = jest.spyOn(
installer,
'installCpythonFromRelease'
);
installSpy.mockImplementation(async () => {
const pythonDir: string = path.join(
toolDir,
@@ -200,34 +150,40 @@ describe('Finder tests', () => {
});
it('Check-latest true, finds the latest version in the manifest', async () => {
const findSpy = tc.getManifestFromRepo as jest.Mock;
findSpy.mockImplementation(() => manifestData);
const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo');
findSpy.mockImplementation(() => <tc.IToolRelease[]>manifestData);
const getBooleanInputSpy = core.getBooleanInput as jest.Mock;
getBooleanInputSpy.mockImplementation(() => true);
const getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
getBooleanInputSpy.mockImplementation(input => true);
const cnSpy = jest.spyOn(process.stdout, 'write');
cnSpy.mockImplementation(() => true);
const cnSpy: jest.SpyInstance = jest.spyOn(process.stdout, 'write');
cnSpy.mockImplementation(line => {
// uncomment to debug
// process.stderr.write('write:' + line + '\n');
});
const addPathSpy = core.addPath as jest.Mock;
const addPathSpy: jest.SpyInstance = jest.spyOn(core, 'addPath');
addPathSpy.mockImplementation(() => null);
const infoSpy = core.info as jest.Mock;
const infoSpy: jest.SpyInstance = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => {});
const debugSpy = core.debug as jest.Mock;
const debugSpy: jest.SpyInstance = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => {});
const pythonDir: string = path.join(toolDir, 'Python', '1.2.2', 'x64');
const expPath: string = path.join(toolDir, 'Python', '1.2.3', 'x64');
const installSpy = installer.installCpythonFromRelease as jest.Mock;
const installSpy: jest.SpyInstance = jest.spyOn(
installer,
'installCpythonFromRelease'
);
installSpy.mockImplementation(async () => {
await io.mkdirP(expPath);
fs.writeFileSync(`${expPath}.complete`, 'hello');
});
const tcFindSpy = tc.find as jest.Mock;
const tcFindSpy: jest.SpyInstance = jest.spyOn(tc, 'find');
tcFindSpy
.mockImplementationOnce(() => '')
.mockImplementationOnce(() => expPath);
@@ -268,10 +224,13 @@ describe('Finder tests', () => {
});
it('Finds stable Python version if it is not installed, but exists in the manifest, skipping newer pre-release', async () => {
const findSpy = tc.getManifestFromRepo as jest.Mock;
findSpy.mockImplementation(() => manifestData);
const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo');
findSpy.mockImplementation(() => <tc.IToolRelease[]>manifestData);
const installSpy = installer.installCpythonFromRelease as jest.Mock;
const installSpy: jest.SpyInstance = jest.spyOn(
installer,
'installCpythonFromRelease'
);
installSpy.mockImplementation(async () => {
const pythonDir: string = path.join(toolDir, 'Python', '1.2.3', 'x64');
await io.mkdirP(pythonDir);
@@ -287,10 +246,13 @@ describe('Finder tests', () => {
});
it('Finds Python version if it is not installed, but exists in the manifest, pre-release fallback', async () => {
const findSpy = tc.getManifestFromRepo as jest.Mock;
findSpy.mockImplementation(() => manifestData);
const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo');
findSpy.mockImplementation(() => <tc.IToolRelease[]>manifestData);
const installSpy = installer.installCpythonFromRelease as jest.Mock;
const installSpy: jest.SpyInstance = jest.spyOn(
installer,
'installCpythonFromRelease'
);
installSpy.mockImplementation(async () => {
const pythonDir: string = path.join(
toolDir,
+44 -105
View File
@@ -1,75 +1,20 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {fileURLToPath} from 'url';
import fs from 'fs';
import {HttpClient} from '@actions/http-client';
import * as ifm from '@actions/http-client/lib/interfaces';
import * as tc from '@actions/tool-cache';
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import * as path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Mock @actions modules
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)
}));
jest.unstable_mockModule('@actions/tool-cache', () => ({
find: jest.fn(),
findAllVersions: jest.fn(),
downloadTool: jest.fn(),
extractZip: jest.fn(),
extractTar: jest.fn(),
extract7z: jest.fn(),
extractXar: jest.fn(),
cacheDir: jest.fn(),
cacheFile: jest.fn(),
getManifestFromRepo: jest.fn(),
findFromManifest: jest.fn(),
evaluateVersions: jest.fn()
}));
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const exec = await import('@actions/exec');
// Non-mocked imports
import {HttpClient} from '@actions/http-client';
import type * as ifm from '@actions/http-client/lib/interfaces';
const installer = await import('../src/install-graalpy.js');
const utils = await import('../src/utils.js');
import type {
import * as installer from '../src/install-graalpy';
import {
IGraalPyManifestRelease,
IGraalPyManifestAsset
} from '../src/utils.js';
import manifestData from './data/graalpy.json' with {type: 'json'};
IGraalPyManifestAsset,
IS_WINDOWS
} from '../src/utils';
const IS_WINDOWS = utils.IS_WINDOWS;
import manifestData from './data/graalpy.json';
const architecture = 'x64';
@@ -103,18 +48,18 @@ describe('findRelease', () => {
browser_download_url: `https://github.com/graalvm/graal-languages-ea-builds/releases/download/graalpy-24.1.0-ea.09/graalpy-24.1.0-ea.09-${extensionName}`
};
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let infoSpy: jest.Mock;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
beforeEach(() => {
infoSpy = core.info as jest.Mock;
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => {});
warningSpy = core.warning as jest.Mock;
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = core.debug as jest.Mock;
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
});
@@ -170,57 +115,51 @@ describe('findRelease', () => {
resolvedGraalPyVersion: '24.1.0-ea.9'
});
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
jest.restoreAllMocks();
});
});
describe('installGraalPy', () => {
let tcFind: jest.Mock;
let infoSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let spyExtractZip: jest.Mock;
let spyExtractTar: jest.Mock;
let spyFsReadDir: jest.SpiedFunction<typeof fs.readdirSync>;
let spyFsWriteFile: jest.SpiedFunction<typeof fs.writeFileSync>;
let spyHttpClient: jest.SpiedFunction<typeof HttpClient.prototype.getJson>;
let spyExistsSync: jest.SpiedFunction<typeof fs.existsSync>;
let spyExec: jest.Mock;
let spySymlinkSync: jest.SpiedFunction<typeof fs.symlinkSync>;
let spyDownloadTool: jest.Mock;
let spyCacheDir: jest.Mock;
let spyChmodSync: jest.SpiedFunction<typeof fs.chmodSync>;
let tcFind: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let spyExtractZip: jest.SpyInstance;
let spyExtractTar: jest.SpyInstance;
let spyFsReadDir: jest.SpyInstance;
let spyFsWriteFile: jest.SpyInstance;
let spyHttpClient: jest.SpyInstance;
let spyExistsSync: jest.SpyInstance;
let spyExec: jest.SpyInstance;
let spySymlinkSync: jest.SpyInstance;
let spyDownloadTool: jest.SpyInstance;
let spyCacheDir: jest.SpyInstance;
let spyChmodSync: jest.SpyInstance;
beforeEach(() => {
tcFind = tc.find as jest.Mock;
tcFind = jest.spyOn(tc, 'find');
tcFind.mockImplementation(() =>
path.join('GraalPy', '3.6.12', architecture)
);
spyDownloadTool = tc.downloadTool as jest.Mock;
spyDownloadTool = jest.spyOn(tc, 'downloadTool');
spyDownloadTool.mockImplementation(() => path.join(tempDir, 'GraalPy'));
spyExtractZip = tc.extractZip as jest.Mock;
spyExtractZip = jest.spyOn(tc, 'extractZip');
spyExtractZip.mockImplementation(() => tempDir);
spyExtractTar = tc.extractTar as jest.Mock;
spyExtractTar = jest.spyOn(tc, 'extractTar');
spyExtractTar.mockImplementation(() => tempDir);
infoSpy = core.info as jest.Mock;
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => {});
warningSpy = core.warning as jest.Mock;
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = core.debug as jest.Mock;
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
spyFsReadDir = jest.spyOn(fs, 'readdirSync');
spyFsReadDir.mockImplementation(() => ['GraalPyTest'] as any);
spyFsReadDir.mockImplementation(() => ['GraalPyTest']);
spyFsWriteFile = jest.spyOn(fs, 'writeFileSync');
spyFsWriteFile.mockImplementation(() => undefined);
@@ -237,7 +176,7 @@ describe('installGraalPy', () => {
}
);
spyExec = exec.exec as jest.Mock;
spyExec = jest.spyOn(exec, 'exec');
spyExec.mockImplementation(() => undefined);
spySymlinkSync = jest.spyOn(fs, 'symlinkSync');
@@ -266,7 +205,7 @@ describe('installGraalPy', () => {
});
it('found and install GraalPy', async () => {
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '21.3.0', architecture)
);
@@ -288,7 +227,7 @@ describe('installGraalPy', () => {
});
it('found and install GraalPy, pre-release fallback', async () => {
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '24.1.0', architecture)
);
+47 -103
View File
@@ -1,72 +1,20 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {fileURLToPath} from 'url';
import fs from 'fs';
import {HttpClient} from '@actions/http-client';
import * as ifm from '@actions/http-client/lib/interfaces';
import * as tc from '@actions/tool-cache';
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import * as path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
import * as installer from '../src/install-pypy';
import {
IPyPyManifestRelease,
IPyPyManifestAsset,
IS_WINDOWS
} from '../src/utils';
// Mock @actions modules
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)
}));
jest.unstable_mockModule('@actions/tool-cache', () => ({
find: jest.fn(),
findAllVersions: jest.fn(),
downloadTool: jest.fn(),
extractZip: jest.fn(),
extractTar: jest.fn(),
extract7z: jest.fn(),
extractXar: jest.fn(),
cacheDir: jest.fn(),
cacheFile: jest.fn(),
getManifestFromRepo: jest.fn(),
findFromManifest: jest.fn(),
evaluateVersions: jest.fn()
}));
jest.unstable_mockModule('@actions/exec', () => ({
exec: jest.fn(),
getExecOutput: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const exec = await import('@actions/exec');
// Non-mocked imports
import {HttpClient} from '@actions/http-client';
import type * as ifm from '@actions/http-client/lib/interfaces';
const installer = await import('../src/install-pypy.js');
const utils = await import('../src/utils.js');
import type {IPyPyManifestRelease, IPyPyManifestAsset} from '../src/utils.js';
import manifestData from './data/pypy.json' with {type: 'json'};
const IS_WINDOWS = utils.IS_WINDOWS;
import manifestData from './data/pypy.json';
let architecture: string;
if (IS_WINDOWS) {
@@ -110,18 +58,19 @@ describe('findRelease', () => {
download_url: `https://test.download.python.org/pypy/pypy3.6-v7.4.0rc1-${extensionName}`
};
let infoSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let getBooleanInputSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
beforeEach(() => {
infoSpy = core.info as jest.Mock;
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => {});
warningSpy = core.warning as jest.Mock;
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = core.debug as jest.Mock;
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
});
@@ -266,55 +215,50 @@ describe('findRelease', () => {
resolvedPyPyVersion: pypyVersion
});
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
jest.restoreAllMocks();
});
});
describe('installPyPy', () => {
let tcFind: jest.Mock;
let infoSpy: jest.Mock;
let warningSpy: jest.Mock;
let debugSpy: jest.Mock;
let spyExtractZip: jest.Mock;
let spyExtractTar: jest.Mock;
let spyFsReadDir: jest.SpiedFunction<typeof fs.readdirSync>;
let spyFsWriteFile: jest.SpiedFunction<typeof fs.writeFileSync>;
let spyHttpClient: jest.SpiedFunction<typeof HttpClient.prototype.getJson>;
let spyExistsSync: jest.SpiedFunction<typeof fs.existsSync>;
let spyExec: jest.Mock;
let spySymlinkSync: jest.SpiedFunction<typeof fs.symlinkSync>;
let spyDownloadTool: jest.Mock;
let spyCacheDir: jest.Mock;
let spyChmodSync: jest.SpiedFunction<typeof fs.chmodSync>;
let tcFind: jest.SpyInstance;
let getBooleanInputSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let spyExtractZip: jest.SpyInstance;
let spyExtractTar: jest.SpyInstance;
let spyFsReadDir: jest.SpyInstance;
let spyFsWriteFile: jest.SpyInstance;
let spyHttpClient: jest.SpyInstance;
let spyExistsSync: jest.SpyInstance;
let spyExec: jest.SpyInstance;
let spySymlinkSync: jest.SpyInstance;
let spyDownloadTool: jest.SpyInstance;
let spyCacheDir: jest.SpyInstance;
let spyChmodSync: jest.SpyInstance;
beforeEach(() => {
tcFind = tc.find as jest.Mock;
tcFind = jest.spyOn(tc, 'find');
tcFind.mockImplementation(() => path.join('PyPy', '3.6.12', architecture));
spyDownloadTool = tc.downloadTool as jest.Mock;
spyDownloadTool = jest.spyOn(tc, 'downloadTool');
spyDownloadTool.mockImplementation(() => path.join(tempDir, 'PyPy'));
spyExtractZip = tc.extractZip as jest.Mock;
spyExtractZip = jest.spyOn(tc, 'extractZip');
spyExtractZip.mockImplementation(() => tempDir);
spyExtractTar = tc.extractTar as jest.Mock;
spyExtractTar = jest.spyOn(tc, 'extractTar');
spyExtractTar.mockImplementation(() => tempDir);
infoSpy = core.info as jest.Mock;
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => {});
warningSpy = core.warning as jest.Mock;
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = core.debug as jest.Mock;
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
spyFsReadDir = jest.spyOn(fs, 'readdirSync');
spyFsReadDir.mockImplementation(() => ['PyPyTest'] as any);
spyFsReadDir.mockImplementation(() => ['PyPyTest']);
spyFsWriteFile = jest.spyOn(fs, 'writeFileSync');
spyFsWriteFile.mockImplementation(() => undefined);
@@ -331,7 +275,7 @@ describe('installPyPy', () => {
}
);
spyExec = exec.exec as jest.Mock;
spyExec = jest.spyOn(exec, 'exec');
spyExec.mockImplementation(() => undefined);
spySymlinkSync = jest.spyOn(fs, 'symlinkSync');
@@ -360,7 +304,7 @@ describe('installPyPy', () => {
});
it('found and install PyPy', async () => {
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'PyPy', '3.6.12', architecture)
);
@@ -384,7 +328,7 @@ describe('installPyPy', () => {
});
it('found and install PyPy, pre-release fallback', async () => {
spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'PyPy', '3.6.12', architecture)
);
-590
View File
@@ -1,590 +0,0 @@
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();
});
});
+21 -151
View File
@@ -1,76 +1,16 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import {
getManifest,
getManifestFromRepo,
getManifestFromURL
} from '../src/install-python';
import * as httpm from '@actions/http-client';
import * as tc from '@actions/tool-cache';
class MockHttpClientError extends Error {
statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.name = 'HttpClientError';
this.statusCode = statusCode;
}
}
// Mock @actions/http-client
jest.unstable_mockModule('@actions/http-client', () => ({
HttpClient: jest.fn().mockImplementation(() => ({
getJson: jest.fn()
})),
HttpClientError: MockHttpClientError,
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', () => ({
jest.mock('@actions/http-client');
jest.mock('@actions/tool-cache');
jest.mock('@actions/tool-cache', () => ({
getManifestFromRepo: jest.fn()
}));
// 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()
}));
// Dynamic imports after mocking
const httpm = await import('@actions/http-client');
const tc = await import('@actions/tool-cache');
const {getManifest, getManifestFromRepo, getManifestFromURL} =
await import('../src/install-python.js');
const mockManifest = [
{
version: '1.0.0',
@@ -91,115 +31,45 @@ describe('getManifest', () => {
jest.resetAllMocks();
});
afterEach(() => {
jest.useRealTimers();
});
it('should return manifest from repo', async () => {
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue(mockManifest);
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue(mockManifest);
const manifest = await getManifest();
expect(manifest).toEqual(mockManifest);
});
it('should return manifest from URL if repo fetch fails', async () => {
jest.useFakeTimers();
(tc.getManifestFromRepo as jest.Mock<any>).mockRejectedValue(
(tc.getManifestFromRepo as jest.Mock).mockRejectedValue(
new Error('Fetch failed')
);
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({
getJson: jest.fn(async () => ({result: mockManifest}))
}));
const promise = getManifest();
await jest.runAllTimersAsync();
const manifest = await promise;
expect(manifest).toEqual(mockManifest);
});
it('should fall back to URL if repo returns a truncated/empty manifest', async () => {
jest.useFakeTimers();
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue([]);
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({
getJson: jest.fn(async () => ({result: mockManifest}))
}));
const promise = getManifest();
await jest.runAllTimersAsync();
const manifest = await promise;
expect(manifest).toEqual(mockManifest);
});
it('should retry on a transient invalid manifest and then succeed', async () => {
jest.useFakeTimers();
(tc.getManifestFromRepo as jest.Mock<any>)
.mockResolvedValueOnce([])
.mockResolvedValueOnce(mockManifest);
const promise = getManifest();
await jest.runAllTimersAsync();
const manifest = await promise;
expect(manifest).toEqual(mockManifest);
expect(tc.getManifestFromRepo as jest.Mock<any>).toHaveBeenCalledTimes(2);
});
it('should fail loudly when the manifest is truncated/empty on every source', async () => {
jest.useFakeTimers();
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue([]);
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({
getJson: jest.fn(async () => ({result: []}))
}));
const promise = getManifest();
// Prevent unhandled rejection before timers advance.
const catchPromise = promise.catch(() => {});
await jest.runAllTimersAsync();
await catchPromise;
await expect(promise).rejects.toThrow(
'Failed to fetch the Python versions manifest'
);
});
it('should not retry the API on a rate-limit error and fall back to URL immediately', async () => {
const rateLimitError = Object.assign(new Error('API rate limit exceeded'), {
httpStatusCode: 403
(httpm.HttpClient.prototype.getJson as jest.Mock).mockResolvedValue({
result: mockManifest
});
(tc.getManifestFromRepo as jest.Mock<any>).mockRejectedValue(
rateLimitError
);
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({
getJson: jest.fn(async () => ({result: mockManifest}))
}));
const manifest = await getManifest();
expect(manifest).toEqual(mockManifest);
expect(tc.getManifestFromRepo as jest.Mock<any>).toHaveBeenCalledTimes(1);
});
});
describe('getManifestFromRepo', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should return manifest from repo', async () => {
(tc.getManifestFromRepo as jest.Mock<any>).mockResolvedValue(mockManifest);
(tc.getManifestFromRepo as jest.Mock).mockResolvedValue(mockManifest);
const manifest = await getManifestFromRepo();
expect(manifest).toEqual(mockManifest);
});
});
describe('getManifestFromURL', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should return manifest from URL', async () => {
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({
getJson: jest.fn(async () => ({result: mockManifest}))
}));
(httpm.HttpClient.prototype.getJson as jest.Mock).mockResolvedValue({
result: mockManifest
});
const manifest = await getManifestFromURL();
expect(manifest).toEqual(mockManifest);
});
it('should throw error if unable to get manifest from URL', async () => {
(httpm.HttpClient as jest.Mock<any>).mockImplementation(() => ({
getJson: jest.fn(async () => ({result: null}))
}));
(httpm.HttpClient.prototype.getJson as jest.Mock).mockResolvedValue({
result: null
});
await expect(getManifestFromURL()).rejects.toThrow(
'Unable to get manifest from'
);
+14 -61
View File
@@ -1,57 +1,11 @@
import {
jest,
describe,
it,
expect,
afterAll,
afterEach,
beforeEach
} from '@jest/globals';
import {fileURLToPath} from 'url';
import path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Mock @actions/cache
jest.unstable_mockModule('@actions/cache', () => ({
isFeatureAvailable: jest.fn(),
saveCache: jest.fn(),
restoreCache: jest.fn()
}));
// Mock @actions/core
jest.unstable_mockModule('@actions/core', () => ({
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
setOutput: jest.fn(),
setFailed: jest.fn(),
warning: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
startGroup: jest.fn(),
endGroup: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
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)
}));
const cache = await import('@actions/cache');
const core = await import('@actions/core');
import * as cache from '@actions/cache';
import * as core from '@actions/core';
import * as io from '@actions/io';
import fs from 'fs';
import path from 'path';
const {
import {
validateVersion,
validatePythonVersionFormatForPyPy,
isCacheFeatureAvailable,
@@ -64,7 +18,10 @@ const {
IS_WINDOWS,
getDownloadFileName,
getVersionInputFromToolVersions
} = await import('../src/utils.js');
} from '../src/utils';
jest.mock('@actions/cache');
jest.mock('@actions/core');
describe('validatePythonVersionFormatForPyPy', () => {
it.each([
@@ -98,8 +55,8 @@ describe('validateVersion', () => {
describe('isCacheFeatureAvailable', () => {
it('isCacheFeatureAvailable disabled on GHES', () => {
(cache.isFeatureAvailable as jest.Mock).mockImplementation(() => false);
const infoMock = core.warning as jest.Mock;
jest.spyOn(cache, 'isFeatureAvailable').mockImplementation(() => false);
const infoMock = jest.spyOn(core, 'warning');
const message =
'Caching is only supported on GHES version >= 3.5. If you are on a version >= 3.5, please check with your GHES admin if the Actions cache service is enabled or not.';
try {
@@ -112,8 +69,8 @@ describe('isCacheFeatureAvailable', () => {
});
it('isCacheFeatureAvailable disabled on dotcom', () => {
(cache.isFeatureAvailable as jest.Mock).mockImplementation(() => false);
const infoMock = core.warning as jest.Mock;
jest.spyOn(cache, 'isFeatureAvailable').mockImplementation(() => false);
const infoMock = jest.spyOn(core, 'warning');
const message =
'The runner was not able to contact the cache service. Caching will be skipped';
try {
@@ -126,14 +83,9 @@ describe('isCacheFeatureAvailable', () => {
});
it('isCacheFeatureAvailable is enabled', () => {
(cache.isFeatureAvailable as jest.Mock).mockImplementation(() => true);
jest.spyOn(cache, 'isFeatureAvailable').mockImplementation(() => true);
expect(isCacheFeatureAvailable()).toBe(true);
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
});
const tempDir = path.join(
@@ -393,6 +345,7 @@ describe('isGhes', () => {
const pristineEnv = process.env;
beforeEach(() => {
jest.resetModules();
process.env = {...pristineEnv};
});
+3 -7
View File
@@ -16,14 +16,8 @@ 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. This token is only sent to GitHub-owned hosts, never to a custom 'mirror'."
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."
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:
@@ -37,6 +31,8 @@ inputs:
default: false
pip-version:
description: "Used to specify the version of pip to install with the Python. Supported format: major[.minor][.patch]."
pip-install:
description: "Used to specify the packages to install with pip after setting up Python. Can be a requirements file or package names."
outputs:
python-version:
description: "The installed Python or PyPy version. Useful when given a version range as input."
+48612 -53151
View File
File diff suppressed because one or more lines are too long
-3
View File
@@ -1,3 +0,0 @@
{
"type": "module"
}
+52825 -58797
View File
File diff suppressed because one or more lines are too long
-3
View File
@@ -1,3 +0,0 @@
{
"type": "module"
}
+84 -104
View File
@@ -23,6 +23,7 @@
- [Using `setup-python` on GHES](advanced-usage.md#using-setup-python-on-ghes)
- [Allow pre-releases](advanced-usage.md#allow-pre-releases)
- [Using the pip-version input](advanced-usage.md#using-the-pip-version-input)
- [Using the pip-install input](advanced-usage.md#using-the-pip-install-input)
## Using the `python-version` input
@@ -32,8 +33,8 @@ If there is a specific version of Python that you need and you don't want to wor
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.12.6'
- run: python my_script.py
@@ -46,8 +47,8 @@ You can specify **only a major and minor version** if you are okay with the most
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.13'
- run: python my_script.py
@@ -60,8 +61,8 @@ You can specify the version with **prerelease tag** to download and set up an ac
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.14.0-alpha.1'
- run: python my_script.py
@@ -71,8 +72,8 @@ It's also possible to use **x.y-dev syntax** to download and set up the latest p
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.14-dev'
- run: python my_script.py
@@ -84,8 +85,8 @@ Free threaded Python is only available starting with the 3.13 release.
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.13t'
- run: python my_script.py
@@ -95,8 +96,8 @@ Note that the **t** suffix is not `semver` syntax. If you wish to specify a rang
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '>=3.13'
freethreaded: true
@@ -109,8 +110,8 @@ You can also use several types of ranges that are specified in [semver](https://
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '>=3.9 <3.14'
- run: python my_script.py
@@ -120,8 +121,8 @@ steps:
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.13.0-alpha - 3.13.0'
- run: python my_script.py
@@ -131,8 +132,8 @@ steps:
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.x'
- run: python my_script.py
@@ -164,8 +165,8 @@ jobs:
- 'pypy3.10' # the latest available version of PyPy that supports Python 3.10
- 'pypy3.10-v7.3.17' # Python 3.10 and PyPy 7.3.17
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- run: python my_script.py
@@ -182,8 +183,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: |
3.11
@@ -199,8 +200,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: |
pypy-3.10-v7.3.x
@@ -216,8 +217,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: |
3.11
@@ -241,9 +242,9 @@ jobs:
python-version: ['3.x', 'pypy3.8', 'pypy3.9' ]
name: Python ${{ matrix.python-version }} sample
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v7
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
architecture: x64
@@ -267,9 +268,9 @@ jobs:
- os: windows-latest
python-version: '3.9'
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v7
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Display Python version
@@ -285,8 +286,8 @@ jobs:
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version-file: '.python-version' # Read python version from a file .python-version
- run: python my_script.py
@@ -294,8 +295,8 @@ steps:
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version-file: 'pyproject.toml' # Read python version from a file pyproject.toml
- run: python my_script.py
@@ -303,8 +304,8 @@ steps:
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version-file: '.tool-versions' # Read python version from a file .tool-versions
- run: python my_script.py
@@ -312,8 +313,8 @@ steps:
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version-file: 'Pipfile' # Read python version from a file Pipfile
- run: python my_script.py
@@ -327,8 +328,8 @@ If `check-latest` is set to `true`, the action first checks if the cached versio
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.13'
check-latest: true
@@ -342,8 +343,8 @@ steps:
**Caching pipenv dependencies:**
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pipenv'
@@ -355,10 +356,10 @@ steps:
**Caching poetry dependencies:**
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Install poetry
run: pipx install poetry
- uses: actions/setup-python@v7
- uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'poetry'
@@ -370,8 +371,8 @@ steps:
**Using a list of file paths to cache dependencies**
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pipenv'
@@ -385,8 +386,8 @@ steps:
**Using wildcard patterns to cache dependencies**
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pip'
@@ -397,8 +398,8 @@ steps:
**Using a list of wildcard patterns to cache dependencies**
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pip'
@@ -412,8 +413,8 @@ steps:
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pip'
@@ -435,8 +436,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
id: cp312
with:
python-version: "3.9.0 - 3.12.0"
@@ -452,8 +453,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
id: cp313
with:
python-version: "3.13"
@@ -468,8 +469,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
id: cp313
with:
python-version: "3.13.0"
@@ -499,8 +500,8 @@ Such a requirement on side-effect could be because you don't want your composite
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
id: cp313
with:
python-version: '3.13'
@@ -524,46 +525,6 @@ 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:
@@ -685,8 +646,8 @@ jobs:
python_version: ["3.14"]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "${{ matrix.python_version }}"
allow-prereleases: true
@@ -700,9 +661,9 @@ The version of Pip should be specified in the format `major`, `major.minor`, or
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v7
uses: actions/setup-python@v6
with:
python-version: '3.13'
pip-version: '25.0.1'
@@ -712,3 +673,22 @@ The version of Pip should be specified in the format `major`, `major.minor`, or
> The `pip-version` input is supported only with standard Python versions. It is not available when using PyPy or GraalPy.
> Using a specific or outdated version of pip may result in compatibility or security issues and can cause job failures. For best practices and guidance, refer to the official [pip documentation](https://pip.pypa.io/en/stable/).
## Using the pip-install input
The `pip-install` input allows you to install dependencies as part of the Python setup step.
```yaml
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
pip-install: -r requirements.txt
```
> Note: This feature is intended for standard pip-based dependency installations.
For complex workflows, or alternative package managers (e.g., poetry, pipenv), we recommend using separate steps to maintain clarity and flexibility.
> The `pip-install` input mirrors the flexibility of a standard pip install command and supports most of its arguments.
+11
View File
@@ -0,0 +1,11 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}
-24
View File
@@ -1,24 +0,0 @@
export default {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
roots: ['<rootDir>'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': [
'ts-jest',
{
useESM: true,
diagnostics: {
ignoreCodes: [151002]
}
}
]
},
extensionsToTreatAsEsm: ['.ts'],
transformIgnorePatterns: ['node_modules/(?!(@actions)/)'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1'
},
verbose: true
}
+1777 -2303
View File
File diff suppressed because it is too large Load Diff
+30 -32
View File
@@ -1,24 +1,20 @@
{
"name": "setup-python",
"version": "7.0.0",
"type": "module",
"version": "6.3.0",
"private": true,
"description": "Setup python action",
"main": "dist/index.js",
"engines": {
"node": ">=24.0.0"
},
"overrides": {
"brace-expansion": "^5.0.8"
},
"scripts": {
"build": "ncc build -o dist/setup src/setup-python.ts && ncc build -o dist/cache-save src/cache-save.ts",
"format": "prettier --no-error-on-unmatched-pattern --write \"**/*.{ts,yml,yaml}\"",
"format-check": "prettier --no-error-on-unmatched-pattern --check \"**/*.{ts,yml,yaml}\"",
"lint": "eslint \"**/*.ts\"",
"lint:fix": "eslint \"**/*.ts\" --fix",
"format": "prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --write \"**/*.{ts,yml,yaml}\"",
"format-check": "prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --check \"**/*.{ts,yml,yaml}\"",
"lint": "eslint --config ./.eslintrc.js \"**/*.ts\"",
"lint:fix": "eslint --config ./.eslintrc.js \"**/*.ts\" --fix",
"release": "ncc build -o dist/setup src/setup-python.ts && ncc build -o dist/cache-save src/cache-save.ts && git add -f dist/",
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --runInBand --coverage"
"test": "jest --runInBand --coverage"
},
"repository": {
"type": "git",
@@ -32,32 +28,34 @@
"author": "GitHub",
"license": "MIT",
"dependencies": {
"@actions/cache": "^6.2.0",
"@actions/core": "^3.0.1",
"@actions/exec": "^3.0.0",
"@actions/glob": "^0.7.0",
"@actions/http-client": "^4.0.1",
"@actions/io": "^3.0.2",
"@actions/tool-cache": "^4.0.0",
"@actions/cache": "^5.1.0",
"@actions/core": "^2.0.3",
"@actions/exec": "^2.0.0",
"@actions/glob": "^0.5.1",
"@actions/http-client": "^3.0.2",
"@actions/io": "^2.0.0",
"@actions/tool-cache": "^3.0.1",
"@iarna/toml": "^3.0.0",
"semver": "^7.8.5"
"semver": "^7.7.1"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@jest/globals": "^30.4.1",
"@types/jest": "^29.5.12",
"@types/node": "^24.10.1",
"@types/semver": "^7.7.0",
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.62.0",
"@vercel/ncc": "^0.44.0",
"eslint": "^10.5.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-jest": "^29.15.2",
"eslint-plugin-n": "^18.1.0",
"globals": "^17.7.0",
"jest": "^30.4.2",
"prettier": "^3.8.4",
"ts-jest": "^29.4.11",
"typescript": "^6.0.3"
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.54.0",
"@vercel/ncc": "^0.38.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-node": "^11.1.0",
"jest": "^29.7.0",
"jest-circus": "^29.7.0",
"prettier": "^3.6.2",
"ts-jest": "^29.3.2",
"typescript": "^5.9.3"
},
"overrides": {
"fast-xml-parser": "^5.9.2"
}
}
+3 -3
View File
@@ -1,7 +1,7 @@
import * as cache from '@actions/cache';
import * as core from '@actions/core';
import {getOSInfo, IS_LINUX} from '../utils.js';
import {CACHE_DEPENDENCY_BACKUP_PATH} from './constants.js';
import {getOSInfo, IS_LINUX} from '../utils';
import {CACHE_DEPENDENCY_BACKUP_PATH} from './constants';
export enum State {
STATE_CACHE_PRIMARY_KEY = 'cache-primary-key',
@@ -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}] 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.`
`No file in ${process.cwd()} matched to [${file}], make sure you have checked out the target repository`
);
}
+3 -3
View File
@@ -1,6 +1,6 @@
import PipCache from './pip-cache.js';
import PipenvCache from './pipenv-cache.js';
import PoetryCache from './poetry-cache.js';
import PipCache from './pip-cache';
import PipenvCache from './pipenv-cache';
import PoetryCache from './poetry-cache';
export enum PackageManagers {
Pip = 'pip',
+3 -3
View File
@@ -6,9 +6,9 @@ import utils from 'util';
import * as path from 'path';
import os from 'os';
import CacheDistributor from './cache-distributor.js';
import {IS_WINDOWS} from '../utils.js';
import {CACHE_DEPENDENCY_BACKUP_PATH} from './constants.js';
import CacheDistributor from './cache-distributor';
import {IS_WINDOWS} from '../utils';
import {CACHE_DEPENDENCY_BACKUP_PATH} from './constants';
class PipCache extends CacheDistributor {
private cacheDependencyBackupPath: string = CACHE_DEPENDENCY_BACKUP_PATH;
+1 -1
View File
@@ -3,7 +3,7 @@ import * as os from 'os';
import * as path from 'path';
import * as core from '@actions/core';
import CacheDistributor from './cache-distributor.js';
import CacheDistributor from './cache-distributor';
class PipenvCache extends CacheDistributor {
constructor(
+2 -2
View File
@@ -4,8 +4,8 @@ import * as path from 'path';
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import CacheDistributor from './cache-distributor.js';
import {logWarning} from '../utils.js';
import CacheDistributor from './cache-distributor';
import {logWarning} from '../utils';
class PoetryCache extends CacheDistributor {
constructor(
+1 -1
View File
@@ -2,7 +2,7 @@ import * as core from '@actions/core';
import * as cache from '@actions/cache';
import fs from 'fs';
import {State} from './cache-distributions/cache-distributor.js';
import {State} from './cache-distributions/cache-distributor';
// Added early exit to resolve issue with slow post action step:
// - https://github.com/actions/setup-node/issues/878
+2 -2
View File
@@ -1,6 +1,6 @@
import * as path from 'path';
import * as graalpyInstall from './install-graalpy.js';
import {IS_WINDOWS, validateVersion, IGraalPyManifestRelease} from './utils.js';
import * as graalpyInstall from './install-graalpy';
import {IS_WINDOWS, validateVersion, IGraalPyManifestRelease} from './utils';
import * as semver from 'semver';
import * as core from '@actions/core';
+2 -2
View File
@@ -1,5 +1,5 @@
import * as path from 'path';
import * as pypyInstall from './install-pypy.js';
import * as pypyInstall from './install-pypy';
import {
IS_WINDOWS,
WINDOWS_ARCHS,
@@ -9,7 +9,7 @@ import {
validatePythonVersionFormatForPyPy,
IPyPyManifestRelease,
getBinaryDirectory
} from './utils.js';
} from './utils';
import * as semver from 'semver';
import * as core from '@actions/core';
+3 -3
View File
@@ -1,10 +1,10 @@
import * as os from 'os';
import * as path from 'path';
import {IS_WINDOWS, IS_LINUX, getOSInfo} from './utils.js';
import {IS_WINDOWS, IS_LINUX, getOSInfo} from './utils';
import * as semver from 'semver';
import * as installer from './install-python.js';
import * as installer from './install-python';
import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
@@ -137,7 +137,7 @@ export async function useCpythonVersion(
);
}
msg.push(
`The list of all available versions can be found here: ${installer.getManifestUrl()}`
`The list of all available versions can be found here: ${installer.MANIFEST_URL}`
);
throw new Error(msg.join(os.EOL));
}
+1 -1
View File
@@ -16,7 +16,7 @@ import {
createSymlinkInFolder,
isNightlyKeyword,
getNextPageUrl
} from './utils.js';
} from './utils';
const TOKEN = core.getInput('token');
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;
+1 -1
View File
@@ -17,7 +17,7 @@ import {
writeExactPyPyVersionFile,
getBinaryDirectory,
getDownloadFileName
} from './utils.js';
} from './utils';
export async function installPyPy(
pypyVersion: string,
+36 -265
View File
@@ -2,142 +2,19 @@ import * as path from 'path';
import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
import * as exec from '@actions/exec';
import {ExecOptions} from '@actions/exec';
import * as httpm from '@actions/http-client';
import * as fs from 'fs';
import * as semver from 'semver';
import {IS_WINDOWS, IS_LINUX, getDownloadFileName} from './utils.js';
import {ExecOptions} from '@actions/exec/lib/interfaces';
import {IS_WINDOWS, IS_LINUX, getDownloadFileName} from './utils';
import {IToolRelease} from '@actions/tool-cache';
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;
}
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`;
interface LinuxOsRelease {
id: string;
@@ -203,9 +80,6 @@ function findRhelRelease(
return undefined;
}
const MANIFEST_FETCH_MAX_ATTEMPTS = 3;
const MANIFEST_FETCH_RETRY_BASE_DELAY_MS = 1000;
export async function findReleaseFromManifest(
semanticVersionSpec: string,
architecture: string,
@@ -259,143 +133,49 @@ function isIToolRelease(obj: any): obj is IToolRelease {
);
}
// Rejects empty or truncated manifest responses.
function isValidManifest(manifest: unknown): manifest is tc.IToolRelease[] {
return (
Array.isArray(manifest) &&
manifest.length > 0 &&
manifest.every(isIToolRelease)
);
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// HTTP 403/429 from http-client (`statusCode`) or tool-cache (`httpStatusCode`).
function isRateLimitError(err: unknown): boolean {
const e = err as
| {httpStatusCode?: number; statusCode?: number}
| null
| undefined;
const status = e?.httpStatusCode ?? e?.statusCode;
return status === 403 || status === 429;
}
// Fetches and validates a manifest, retrying transient failures with backoff.
async function fetchValidManifest(
source: string,
fetcher: () => Promise<tc.IToolRelease[]>
): Promise<tc.IToolRelease[]> {
let lastError: Error | undefined;
let attempts = 0;
for (let attempt = 1; attempt <= MANIFEST_FETCH_MAX_ATTEMPTS; attempt++) {
attempts = attempt;
try {
const manifest = await fetcher();
if (isValidManifest(manifest)) {
return manifest;
}
throw new Error(
`The manifest fetched from ${source} is empty, truncated, or does not contain any valid tool release entries.`
);
} catch (err) {
lastError = err instanceof Error ? err : new Error(String(err));
core.debug(
`Attempt ${attempt}/${MANIFEST_FETCH_MAX_ATTEMPTS} to fetch the manifest from ${source} failed: ${lastError.message}`
);
// Rate limits won't clear within the backoff window; fall back instead.
if (isRateLimitError(err)) {
core.debug(
`${source} is rate-limited; skipping retries for this source.`
);
break;
}
if (attempt < MANIFEST_FETCH_MAX_ATTEMPTS) {
const delay = MANIFEST_FETCH_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1);
core.debug(`Retrying in ${delay}ms...`);
await sleep(delay);
}
}
}
throw new Error(
`Failed to fetch a valid manifest from ${source} after ${attempts} attempt(s): ${lastError?.message}`
);
}
export async function getManifest(): Promise<tc.IToolRelease[]> {
// 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 {
return await fetchValidManifest('the raw URL', getManifestFromURL);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
// Fail loudly so the action doesn't exit 0 without installing Python.
const repoManifest = await getManifestFromRepo();
if (
Array.isArray(repoManifest) &&
repoManifest.length &&
repoManifest.every(isIToolRelease)
) {
return repoManifest;
}
throw new Error(
`Failed to fetch the Python versions manifest. The response was empty, truncated, or invalid, and all retries were exhausted. ${message}`,
{cause: err}
'The repository manifest is invalid or does not include any valid tool release (IToolRelease) entries.'
);
} catch (err) {
core.debug('Fetching the manifest via the API failed.');
if (err instanceof Error) {
core.debug(err.message);
} else {
core.error('An unexpected error occurred while fetching the manifest.');
}
}
return await getManifestFromURL();
}
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 ${coords.owner}/${coords.repo}@${coords.branch}`
`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
);
// 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 auth = authForUrl(manifestUrl);
const response = await http.getJson<tc.IToolRelease[]>(
manifestUrl,
auth ? {authorization: auth} : undefined
);
const response = await http.getJson<tc.IToolRelease[]>(MANIFEST_URL);
if (!response.result) {
throw new Error(`Unable to get manifest from ${manifestUrl}`);
throw new Error(`Unable to get manifest from ${MANIFEST_URL}`);
}
return response.result;
}
@@ -413,12 +193,7 @@ async function installPython(workingDirectory: string) {
core.info(data.toString().trim());
},
stderr: (data: Buffer) => {
const msg = data.toString().trim();
if (/^WARNING:/im.test(msg)) {
core.warning(msg);
} else {
core.error(msg);
}
core.error(data.toString().trim());
}
}
};
@@ -440,11 +215,7 @@ export async function installCpythonFromRelease(release: tc.IToolRelease) {
let pythonPath = '';
try {
const fileName = getDownloadFileName(downloadUrl);
pythonPath = await tc.downloadTool(
downloadUrl,
fileName,
authForUrl(downloadUrl)
);
pythonPath = await tc.downloadTool(downloadUrl, fileName, AUTH);
core.info('Extract downloaded archive');
let pythonExtractedFolder;
if (IS_WINDOWS) {
+21 -26
View File
@@ -1,20 +1,19 @@
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 finder from './find-python';
import * as finderPyPy from './find-pypy';
import * as finderGraalPy from './find-graalpy';
import * as path from 'path';
import * as os from 'os';
import {fileURLToPath} from 'url';
import fs from 'fs';
import {getCacheDistributor} from './cache-distributions/cache-factory.js';
import {getCacheDistributor} from './cache-distributions/cache-factory';
import {
isCacheFeatureAvailable,
logWarning,
IS_MAC,
getVersionInputFromFile,
getVersionsInputFromPlainFile
} from './utils.js';
} from './utils';
import {exec} from '@actions/exec';
function isPyPyVersion(versionSpec: string) {
return versionSpec.startsWith('pypy');
@@ -24,19 +23,17 @@ 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;
async function installPipPackages(pipInstall: string) {
core.info(`Installing pip packages: ${pipInstall}`);
try {
const installArgs = pipInstall.trim().split(/\s+/);
await exec('python', ['-m', 'pip', 'install', ...installArgs]);
core.info('Successfully installed pip packages');
} catch (error) {
core.setFailed(
`Failed to install pip packages from "${pipInstall}". Please verify that the package names, versions, or requirements files provided are correct and installable, that the specified packages and versions can be resolved from PyPI or the configured package index, and that your network connection is stable and allows access to the package index.`
);
}
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) {
@@ -118,7 +115,6 @@ async function run() {
core.startGroup('Installed versions');
for (const version of versions) {
if (isPyPyVersion(version)) {
warnIfMirrorUnsupported(version);
const installed = await finderPyPy.findPyPyVersion(
version,
arch,
@@ -131,7 +127,6 @@ 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,
@@ -164,16 +159,16 @@ async function run() {
if (cache && isCacheFeatureAvailable()) {
await cacheDependencies(cache, pythonVersion);
}
const pipInstall = core.getInput('pip-install');
if (pipInstall) {
await installPipPackages(pipInstall);
}
} else {
core.warning(
'The `python-version` input is not set. The version of Python currently in `PATH` will be used.'
);
}
const matchersPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'../..',
'.github'
);
const matchersPath = path.join(__dirname, '../..', '.github');
core.info(`##[add-matcher]${path.join(matchersPath, 'python.json')}`);
} catch (err) {
core.setFailed((err as Error).message);
+3 -3
View File
@@ -258,7 +258,7 @@ export function getVersionInputFromTomlFile(versionFile: string): string[] {
pyprojectFile = pyprojectFile.replace(/\r\n/g, '\n');
const pyprojectConfig = toml.parse(pyprojectFile);
let keys: string[] = [];
let keys = [];
if ('project' in pyprojectConfig) {
// standard project metadata (PEP 621)
@@ -267,7 +267,7 @@ export function getVersionInputFromTomlFile(versionFile: string): string[] {
// python poetry
keys = ['tool', 'poetry', 'dependencies', 'python'];
}
const versions: string[] = [];
const versions = [];
const version = extractValue(pyprojectConfig, keys);
if (version !== undefined) {
versions.push(version);
@@ -376,7 +376,7 @@ export function getVersionInputFromPipfileFile(versionFile: string): string[] {
} else {
keys.push('python_version');
}
const versions: string[] = [];
const versions = [];
const version = extractValue(pipfileConfig, keys);
if (version !== undefined) {
versions.push(version);
+2 -2
View File
@@ -3,7 +3,7 @@
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ES2022", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "NodeNext", /* Specify module code generation. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
@@ -60,5 +60,5 @@
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"exclude": ["node_modules", "**/*.test.ts", "jest.config.ts"]
"exclude": ["node_modules", "**/*.test.ts"]
}