Compare commits

..

12 Commits

Author SHA1 Message Date
0ae58361cd Add example of GraalPy to docs (#773) 2023-12-05 11:40:46 +01:00
b64ffcaf5b update actions/checkout to v4 (#761) 2023-11-08 11:16:06 +01:00
8d2896179a Examples now use checkout@v4 (#738) 2023-11-08 10:25:26 +01:00
7bc6abb01e advanced-usage.md: Encourage the use actions/checkout@v4 (#729) 2023-11-08 09:52:55 +01:00
e8111cec9d Bump @babel/traverse from 7.9.0 to 7.23.2 (#743) 2023-10-26 13:33:26 +02:00
a00ea43da6 add fix for graalpy ci (#741) 2023-10-16 11:57:32 +02:00
8635b1ccc5 Change deprecation comment to past tense (#723) 2023-10-16 11:19:29 +02:00
f6cc428f53 Use non-deprecated versions in examples (#724) 2023-10-16 11:01:43 +02:00
5f2af211d6 Add GraalPy support (#694)
* Add support for graalpy

* add graalpy test workflow

* format, lint and build

* symlink graalpy binaries names

* fix macos names for graalpy

* Don't attempt to update pip for graalpy

* Remove test schedule

* Extract common getBinaryDirectory function for PyPy and GraalPy

* Clean up and format

* Pass GitHub token to GraalPy queries

* Utilize pagination when querying GraalPy GitHub releases

* Build

* Fix lint errors

* Deal with possible multiple artifacts for a single releases

* Skip few GraalPy tests on windows - we don't have a windows release yet

* Fix GraalPy test on Mac OS

* Build

* Skip one more GraalPy test on windows

---------

Co-authored-by: Michael Simacek <michael.simacek@oracle.com>
2023-10-10 14:59:54 +02:00
3467d92d48 Trim python version to get rid of '\n' (#628) 2023-10-06 12:34:33 +02:00
65d7f2d534 Add range validation for toml files (#726) 2023-09-07 15:45:09 +02:00
f97b83114c Bump word-wrap from 1.2.3 to 1.2.4 (#702)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-04 09:55:40 +02:00
22 changed files with 10204 additions and 5023 deletions

View File

@ -23,7 +23,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', 'pypy-3.7-v7.x'] python-version: ['3.9', 'pypy-3.7-v7.x']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup Python - name: Setup Python
uses: ./ uses: ./
with: with:
@ -41,7 +41,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', 'pypy-3.9-v7.x'] python-version: ['3.9', 'pypy-3.9-v7.x']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup Python - name: Setup Python
id: cache-pipenv id: cache-pipenv
uses: ./ uses: ./
@ -77,7 +77,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', 'pypy-3.8'] python-version: ['3.9', 'pypy-3.8']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install poetry - name: Install poetry
run: pipx install poetry run: pipx install poetry
- name: Init pyproject.toml - name: Init pyproject.toml
@ -99,7 +99,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', 'pypy-3.7-v7.x'] python-version: ['3.9', 'pypy-3.7-v7.x']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup Python - name: Setup Python
uses: ./ uses: ./
with: with:
@ -118,7 +118,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', 'pypy-3.9-v7.x'] python-version: ['3.9', 'pypy-3.9-v7.x']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup Python - name: Setup Python
id: cache-pipenv id: cache-pipenv
uses: ./ uses: ./

View File

@ -19,7 +19,7 @@ jobs:
operating-system: [ubuntu-20.04, windows-latest] operating-system: [ubuntu-20.04, windows-latest]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Run with setup-python 3.5 - name: Run with setup-python 3.5
uses: ./ uses: ./
@ -93,10 +93,3 @@ jobs:
python-version: '<3.11' python-version: '<3.11'
- name: Verify <3.11 - name: Verify <3.11
run: python __tests__/verify-python.py 3.10 run: python __tests__/verify-python.py 3.10
- name: Run with setup-python >3.8
uses: ./
with:
python-version: '>3.8'
- name: Verify >3.8
run: python __tests__/verify-python.py 3.11

116
.github/workflows/test-graalpy.yml vendored Normal file
View File

@ -0,0 +1,116 @@
name: Validate GraalPy e2e
on:
push:
branches:
- main
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
jobs:
setup-graalpy:
name: Setup GraalPy ${{ matrix.graalpy }} ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-20.04, ubuntu-latest]
graalpy:
- 'graalpy-23.0'
- 'graalpy-22.3'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: setup-python ${{ matrix.graalpy }}
id: setup-python
uses: ./
with:
python-version: ${{ matrix.graalpy }}
- name: Check python-path
run: ./__tests__/check-python-path.sh '${{ steps.setup-python.outputs.python-path }}'
shell: bash
- name: GraalPy and Python version
run: python --version
- name: Run simple code
run: python -c 'import math; print(math.factorial(5))'
- name: Assert GraalPy is running
run: |
import platform
assert platform.python_implementation().lower() == "graalvm"
shell: python
- name: Assert expected binaries (or symlinks) are present
run: |
EXECUTABLE=${{ matrix.graalpy }}
EXECUTABLE=${EXECUTABLE/graalpy-/graalpy} # remove the first '-' in "graalpy-X.Y" -> "graalpyX.Y" to match executable name
EXECUTABLE=${EXECUTABLE%%-*} # remove any -* suffixe
${EXECUTABLE} --version
shell: bash
setup-graalpy-noenv:
name: Setup GraalPy ${{ matrix.graalpy }} ${{ matrix.os }} (noenv)
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-20.04, ubuntu-latest]
graalpy: ['graalpy23.0', 'graalpy22.3']
steps:
- name: Checkout
uses: actions/checkout@v4
- name: setup-python ${{ matrix.graalpy }}
id: setup-python
uses: ./
with:
python-version: ${{ matrix.graalpy }}
update-environment: false
- name: GraalPy and Python version
run: ${{ steps.setup-python.outputs.python-path }} --version
- name: Run simple code
run: ${{ steps.setup-python.outputs.python-path }} -c 'import math; print(math.factorial(5))'
check-latest:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- name: Setup GraalPy and check latest
uses: ./
id: graalpy
with:
python-version: 'graalpy-23.x'
check-latest: true
- name: GraalPy and Python version
run: python --version
- name: Run simple code
run: python -c 'import math; print(math.factorial(5))'
- name: Assert GraalPy is running
run: |
import platform
assert platform.python_implementation().lower() == "graalvm"
shell: python
- name: Assert expected binaries (or symlinks) are present
run: |
EXECUTABLE='${{ steps.graalpy.outputs.python-version }}'
EXECUTABLE="${EXECUTABLE%.*}"
${EXECUTABLE} --version
shell: bash

View File

@ -35,7 +35,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: setup-python ${{ matrix.pypy }} - name: setup-python ${{ matrix.pypy }}
id: setup-python id: setup-python
@ -78,7 +78,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: setup-python ${{ matrix.pypy }} - name: setup-python ${{ matrix.pypy }}
id: setup-python id: setup-python
@ -100,7 +100,7 @@ jobs:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup PyPy and check latest - name: Setup PyPy and check latest
uses: ./ uses: ./
with: with:
@ -133,7 +133,7 @@ jobs:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup PyPy and check latest - name: Setup PyPy and check latest
uses: ./ uses: ./
with: with:

View File

@ -33,7 +33,7 @@ jobs:
python: 3.8.15 python: 3.8.15
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: setup-python ${{ matrix.python }} - name: setup-python ${{ matrix.python }}
id: setup-python id: setup-python
@ -77,7 +77,7 @@ jobs:
python: 3.8.15 python: 3.8.15
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: build-version-file ${{ matrix.python }} - name: build-version-file ${{ matrix.python }}
run: echo ${{ matrix.python }} > .python-version run: echo ${{ matrix.python }} > .python-version
@ -124,7 +124,7 @@ jobs:
python: 3.8.15 python: 3.8.15
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: build-version-file ${{ matrix.python }} - name: build-version-file ${{ matrix.python }}
run: echo ${{ matrix.python }} > .python-version run: echo ${{ matrix.python }} > .python-version
@ -157,7 +157,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [macos-latest, windows-latest, ubuntu-20.04, ubuntu-22.04] os: [macos-latest, windows-latest, ubuntu-20.04, ubuntu-22.04]
python: [3.5.4, 3.6.7, 3.7.5, 3.8.15, 3.9.13] python: [3.5.4, 3.6.7, 3.7.5, 3.8.15, 3.9.13, '==3.10.10']
exclude: exclude:
- os: ubuntu-22.04 - os: ubuntu-22.04
python: 3.5.4 python: 3.5.4
@ -169,7 +169,7 @@ jobs:
python: 3.8.15 python: 3.8.15
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: build-version-file ${{ matrix.python }} - name: build-version-file ${{ matrix.python }}
run: | run: |
@ -190,7 +190,7 @@ jobs:
- name: Validate version - name: Validate version
run: | run: |
$pythonVersion = (python --version) $pythonVersion = (python --version)
if ("Python ${{ matrix.python }}" -ne "$pythonVersion"){ if ("Python ${{ matrix.python }}".replace("==", "") -ne "$pythonVersion"){
Write-Host "The current version is $pythonVersion; expected version is ${{ matrix.python }}" Write-Host "The current version is $pythonVersion; expected version is ${{ matrix.python }}"
exit 1 exit 1
} }
@ -219,7 +219,7 @@ jobs:
python: 3.8.15 python: 3.8.15
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: build-version-file ${{ matrix.python }} - name: build-version-file ${{ matrix.python }}
run: | run: |
@ -259,7 +259,7 @@ jobs:
os: [macos-latest, windows-latest, ubuntu-20.04] os: [macos-latest, windows-latest, ubuntu-20.04]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: setup-python 3.9.0-beta.4 - name: setup-python 3.9.0-beta.4
id: setup-python id: setup-python
@ -293,7 +293,7 @@ jobs:
os: [macos-latest, windows-latest, ubuntu-latest] os: [macos-latest, windows-latest, ubuntu-latest]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: setup-python 3.9-dev - name: setup-python 3.9-dev
id: setup-python id: setup-python
@ -321,7 +321,7 @@ jobs:
os: [macos-latest, windows-latest, ubuntu-latest] os: [macos-latest, windows-latest, ubuntu-latest]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: setup-python 3.12 - name: setup-python 3.12
id: setup-python id: setup-python
@ -351,7 +351,7 @@ jobs:
python: ['3.7', '3.8', '3.9', '3.10'] python: ['3.7', '3.8', '3.9', '3.10']
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: setup-python ${{ matrix.python }} - name: setup-python ${{ matrix.python }}
id: setup-python id: setup-python
@ -374,7 +374,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.8', '3.9', '3.10'] python-version: ['3.8', '3.9', '3.10']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup Python and check latest - name: Setup Python and check latest
uses: ./ uses: ./
with: with:
@ -397,7 +397,7 @@ jobs:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup Python and check latest - name: Setup Python and check latest
uses: ./ uses: ./
with: with:

View File

@ -1,9 +1,9 @@
--- ---
name: semver name: semver
version: 6.3.0 version: 6.3.1
type: npm type: npm
summary: The semantic version parser used by npm. summary: The semantic version parser used by npm.
homepage: https://github.com/npm/node-semver#readme homepage:
license: isc license: isc
licenses: licenses:
- sources: LICENSE - sources: LICENSE

View File

@ -18,7 +18,7 @@ See [action.yml](action.yml)
**Python** **Python**
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.10' python-version: '3.10'
@ -28,12 +28,23 @@ steps:
**PyPy** **PyPy**
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: 'pypy3.9' python-version: 'pypy3.9'
- run: python my_script.py - run: python my_script.py
``` ```
**GraalPy**
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: 'graalpy-22.3'
- run: python my_script.py
```
The `python-version` input is optional. If not supplied, the action will try to resolve the version from the default `.python-version` file. If the `.python-version` file doesn't exist Python or PyPy version from the PATH will be used. The default version of Python or PyPy in PATH varies between runners and can be changed unexpectedly so we recommend always setting Python version explicitly using the `python-version` or `python-version-file` inputs. The `python-version` input is optional. If not supplied, the action will try to resolve the version from the default `.python-version` file. If the `.python-version` file doesn't exist Python or PyPy version from the PATH will be used. The default version of Python or PyPy in PATH varies between runners and can be changed unexpectedly so we recommend always setting Python version explicitly using the `python-version` or `python-version-file` inputs.
The action will first check the local [tool cache](docs/advanced-usage.md#hosted-tool-cache) for a [semver](https://github.com/npm/node-semver#versions) match. If unable to find a specific version in the tool cache, the action will attempt to download a version of Python from [GitHub Releases](https://github.com/actions/python-versions/releases) and for PyPy from the official [PyPy's dist](https://downloads.python.org/pypy/). The action will first check the local [tool cache](docs/advanced-usage.md#hosted-tool-cache) for a [semver](https://github.com/npm/node-semver#versions) match. If unable to find a specific version in the tool cache, the action will attempt to download a version of Python from [GitHub Releases](https://github.com/actions/python-versions/releases) and for PyPy from the official [PyPy's dist](https://downloads.python.org/pypy/).
@ -62,7 +73,7 @@ The action defaults to searching for a dependency file (`requirements.txt` or `p
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.9' python-version: '3.9'
@ -82,7 +93,7 @@ See examples of using `cache` and `cache-dependency-path` for `pipenv` and `poet
- [Check latest version](docs/advanced-usage.md#check-latest-version) - [Check latest version](docs/advanced-usage.md#check-latest-version)
- [Caching packages](docs/advanced-usage.md#caching-packages) - [Caching packages](docs/advanced-usage.md#caching-packages)
- [Outputs and environment variables](docs/advanced-usage.md#outputs-and-environment-variables) - [Outputs and environment variables](docs/advanced-usage.md#outputs-and-environment-variables)
- [Available versions of Python and PyPy](docs/advanced-usage.md#available-versions-of-python-and-pypy) - [Available versions of Python and PyPy](advanced-usage.md#available-versions-of-python-pypy-and-graalpy)
- [Hosted tool cache](docs/advanced-usage.md#hosted-tool-cache) - [Hosted tool cache](docs/advanced-usage.md#hosted-tool-cache)
- [Using `setup-python` with a self-hosted runner](docs/advanced-usage.md#using-setup-python-with-a-self-hosted-runner) - [Using `setup-python` with a self-hosted runner](docs/advanced-usage.md#using-setup-python-with-a-self-hosted-runner)
- [Using `setup-python` on GHES](docs/advanced-usage.md#using-setup-python-on-ghes) - [Using `setup-python` on GHES](docs/advanced-usage.md#using-setup-python-on-ghes)

5798
__tests__/data/graalpy.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,378 @@
import fs from 'fs';
import {HttpClient} from '@actions/http-client';
import * as ifm from '@actions/http-client/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';
import * as finder from '../src/find-graalpy';
import {IGraalPyManifestRelease, IS_WINDOWS} from '../src/utils';
import manifestData from './data/graalpy.json';
const architecture = 'x64';
const toolDir = path.join(__dirname, 'runner', 'tools');
const tempDir = path.join(__dirname, 'runner', 'temp');
/* GraalPy doesn't have a windows release yet */
const describeSkipOnWindows = IS_WINDOWS ? describe.skip : describe;
describe('parseGraalPyVersion', () => {
it.each([
['graalpy-23', '23'],
['graalpy-23.0', '23.0'],
['graalpy23.0', '23.0']
])('%s -> %s', (input, expected) => {
expect(finder.parseGraalPyVersion(input)).toEqual(expected);
});
it.each(['', 'graalpy-', 'graalpy', 'p', 'notgraalpy-'])(
'throw on invalid input "%s"',
input => {
expect(() => finder.parseGraalPyVersion(input)).toThrow(
"Invalid 'version' property for GraalPy. GraalPy version should be specified as 'graalpy<python-version>' or 'graalpy-<python-version>'. See README for examples and documentation."
);
}
);
});
describe('findGraalPyToolCache', () => {
const actualGraalPyVersion = '23.0.0';
const graalpyPath = path.join('GraalPy', actualGraalPyVersion, architecture);
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 = jest.spyOn(tc, 'find');
tcFind.mockImplementation((toolname: string, pythonVersion: string) => {
const semverVersion = new semver.Range(pythonVersion);
return semver.satisfies(actualGraalPyVersion, semverVersion)
? graalpyPath
: '';
});
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => null);
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
addPathSpy = jest.spyOn(core, 'addPath');
addPathSpy.mockImplementation(() => null);
exportVariableSpy = jest.spyOn(core, 'exportVariable');
exportVariableSpy.mockImplementation(() => null);
setOutputSpy = jest.spyOn(core, 'setOutput');
setOutputSpy.mockImplementation(() => null);
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
jest.restoreAllMocks();
});
it('GraalPy exists on the path and versions are satisfied', () => {
expect(finder.findGraalPyToolCache('23.0.0', architecture)).toEqual({
installDir: graalpyPath,
resolvedGraalPyVersion: actualGraalPyVersion
});
});
it('GraalPy exists on the path and versions are satisfied with semver', () => {
expect(finder.findGraalPyToolCache('23.0', architecture)).toEqual({
installDir: graalpyPath,
resolvedGraalPyVersion: actualGraalPyVersion
});
});
it("GraalPy exists on the path, but version doesn't match", () => {
expect(finder.findGraalPyToolCache('22.3', architecture)).toEqual({
installDir: '',
resolvedGraalPyVersion: ''
});
});
});
describeSkipOnWindows('findGraalPyVersion', () => {
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 = jest.spyOn(core, 'getBooleanInput');
getBooleanInputSpy.mockImplementation(() => false);
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => {});
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
addPathSpy = jest.spyOn(core, 'addPath');
addPathSpy.mockImplementation(() => null);
exportVariableSpy = jest.spyOn(core, 'exportVariable');
exportVariableSpy.mockImplementation(() => null);
setOutputSpy = jest.spyOn(core, 'setOutput');
setOutputSpy.mockImplementation(() => null);
jest.resetModules();
process.env = {...env};
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 = jest.spyOn(tc, 'downloadTool');
spyDownloadTool.mockImplementation(() => path.join(tempDir, 'GraalPy'));
spyExtractZip = jest.spyOn(tc, 'extractZip');
spyExtractZip.mockImplementation(() => tempDir);
spyExtractTar = jest.spyOn(tc, 'extractTar');
spyExtractTar.mockImplementation(() => tempDir);
spyFsReadDir = jest.spyOn(fs, 'readdirSync');
spyFsReadDir.mockImplementation((directory: string) => ['GraalPyTest']);
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient.mockImplementation(
async (): Promise<ifm.ITypedResponse<IGraalPyManifestRelease[]>> => {
const result = JSON.stringify(manifestData);
return {
statusCode: 200,
headers: {},
result: JSON.parse(result) as IGraalPyManifestRelease[]
};
}
);
spyExec = jest.spyOn(exec, 'exec');
spyExec.mockImplementation(() => undefined);
spySymlinkSync = jest.spyOn(fs, 'symlinkSync');
spySymlinkSync.mockImplementation(() => undefined);
spyExistsSync = jest.spyOn(fs, 'existsSync');
spyExistsSync.mockReturnValue(true);
spyCoreAddPath = jest.spyOn(core, 'addPath');
spyCoreExportVariable = jest.spyOn(core, 'exportVariable');
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
jest.restoreAllMocks();
process.env = env;
});
it('found GraalPy in toolcache', async () => {
await expect(
finder.findGraalPyVersion(
'graalpy-23.0',
architecture,
true,
false,
false
)
).resolves.toEqual('23.0.0');
expect(spyCoreAddPath).toHaveBeenCalled();
expect(spyCoreExportVariable).toHaveBeenCalledWith(
'pythonLocation',
expect.anything()
);
expect(spyCoreExportVariable).toHaveBeenCalledWith(
'PKG_CONFIG_PATH',
expect.anything()
);
});
it('throw on invalid input format', async () => {
await expect(
finder.findGraalPyVersion('graalpy-x23', architecture, true, false, false)
).rejects.toThrow();
});
it('found and install successfully', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.0.0', architecture)
);
spyChmodSync = jest.spyOn(fs, 'chmodSync');
spyChmodSync.mockImplementation(() => undefined);
await expect(
finder.findGraalPyVersion(
'graalpy-23.0.0',
architecture,
true,
false,
false
)
).resolves.toEqual('23.0.0');
expect(spyCoreAddPath).toHaveBeenCalled();
expect(spyCoreExportVariable).toHaveBeenCalledWith(
'pythonLocation',
expect.anything()
);
expect(spyCoreExportVariable).toHaveBeenCalledWith(
'PKG_CONFIG_PATH',
expect.anything()
);
});
it('found and install successfully without environment update', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.0.0', architecture)
);
spyChmodSync = jest.spyOn(fs, 'chmodSync');
spyChmodSync.mockImplementation(() => undefined);
await expect(
finder.findGraalPyVersion(
'graalpy-23.0.0',
architecture,
false,
false,
false
)
).resolves.toEqual('23.0.0');
expect(spyCoreAddPath).not.toHaveBeenCalled();
expect(spyCoreExportVariable).not.toHaveBeenCalled();
});
it('throw if release is not found', async () => {
await expect(
finder.findGraalPyVersion(
'graalpy-19.0.0',
architecture,
true,
false,
false
)
).rejects.toThrow(
`GraalPy version 19.0.0 with arch ${architecture} not found`
);
});
it('check-latest enabled version found and used from toolcache', async () => {
await expect(
finder.findGraalPyVersion(
'graalpy-23.0.0',
architecture,
false,
true,
false
)
).resolves.toEqual('23.0.0');
expect(infoSpy).toHaveBeenCalledWith('Resolved as GraalPy 23.0.0');
});
it('check-latest enabled version found and install successfully', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.0.0', architecture)
);
spyChmodSync = jest.spyOn(fs, 'chmodSync');
spyChmodSync.mockImplementation(() => undefined);
await expect(
finder.findGraalPyVersion(
'graalpy-23.0.0',
architecture,
false,
true,
false
)
).resolves.toEqual('23.0.0');
expect(infoSpy).toHaveBeenCalledWith('Resolved as GraalPy 23.0.0');
});
it('check-latest enabled version is not found and used from toolcache', async () => {
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',
architecture,
false,
true,
false
)
).resolves.toEqual('22.3.4');
expect(infoSpy).toHaveBeenCalledWith(
'Failed to resolve GraalPy 22.3.4 from manifest'
);
});
it('found and install successfully, pre-release fallback', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.1', architecture)
);
spyChmodSync = jest.spyOn(fs, 'chmodSync');
spyChmodSync.mockImplementation(() => undefined);
await expect(
finder.findGraalPyVersion(
'graalpy23.1',
architecture,
false,
false,
false
)
).rejects.toThrow();
await expect(
finder.findGraalPyVersion('graalpy23.1', architecture, false, false, true)
).resolves.toEqual('23.1.0-a.1');
});
});

View File

@ -0,0 +1,256 @@
import fs from 'fs';
import {HttpClient} from '@actions/http-client';
import * as ifm from '@actions/http-client/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 installer from '../src/install-graalpy';
import {
IGraalPyManifestRelease,
IGraalPyManifestAsset,
IS_WINDOWS
} from '../src/utils';
import manifestData from './data/graalpy.json';
const architecture = 'x64';
const toolDir = path.join(__dirname, 'runner', 'tools');
const tempDir = path.join(__dirname, 'runner', 'temp');
/* GraalPy doesn't have a windows release yet */
const describeSkipOnWindows = IS_WINDOWS ? describe.skip : describe;
describe('graalpyVersionToSemantic', () => {
it.each([
['23.0.0a1', '23.0.0a1'],
['23.0.0', '23.0.0'],
['23.0.x', '23.0.x'],
['23.x', '23.x']
])('%s -> %s', (input, expected) => {
expect(installer.graalPyTagToVersion(input)).toEqual(expected);
});
});
describeSkipOnWindows('findRelease', () => {
const result = JSON.stringify(manifestData);
const releases = JSON.parse(result) as IGraalPyManifestRelease[];
const extension = 'tar.gz';
const arch = installer.toGraalPyArchitecture(architecture);
const platform = installer.toGraalPyPlatform(process.platform);
const extensionName = `${platform}-${arch}.${extension}`;
const files: IGraalPyManifestAsset = {
name: `graalpython-23.0.0-${extensionName}`,
browser_download_url: `https://github.com/oracle/graalpython/releases/download/graal-23.0.0/graalpython-23.0.0-${extensionName}`
};
const filesRC1: IGraalPyManifestAsset = {
name: `graalpython-23.1.0a1-${extensionName}`,
browser_download_url: `https://github.com/oracle/graalpython/releases/download/graal-23.1.0a1/graalpython-23.1.0a1-${extensionName}`
};
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
beforeEach(() => {
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => {});
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
});
it("GraalPy version doesn't match", () => {
const graalpyVersion = '12.0.0';
expect(
installer.findRelease(releases, graalpyVersion, architecture, false)
).toEqual(null);
});
it('GraalPy version matches', () => {
const graalpyVersion = '23.0.0';
expect(
installer.findRelease(releases, graalpyVersion, architecture, false)
).toMatchObject({
foundAsset: files,
resolvedGraalPyVersion: graalpyVersion
});
});
it('Preview version of GraalPy is found', () => {
const graalpyVersion = installer.graalPyTagToVersion('vm-23.1.0a1');
expect(
installer.findRelease(releases, graalpyVersion, architecture, false)
).toMatchObject({
foundAsset: {
name: `graalpython-23.1.0a1-${extensionName}`,
browser_download_url: `https://github.com/oracle/graalpython/releases/download/graal-23.1.0a1/graalpython-23.1.0a1-${extensionName}`
},
resolvedGraalPyVersion: '23.1.0-a.1'
});
});
it('Latest GraalPy is found', () => {
const graalpyVersion = 'x';
expect(
installer.findRelease(releases, graalpyVersion, architecture, false)
).toMatchObject({
foundAsset: files,
resolvedGraalPyVersion: '23.0.0'
});
});
it('GraalPy version matches semver (pre-release)', () => {
const graalpyVersion = '23.1.x';
expect(
installer.findRelease(releases, graalpyVersion, architecture, false)
).toBeNull();
expect(
installer.findRelease(releases, graalpyVersion, architecture, true)
).toMatchObject({
foundAsset: filesRC1,
resolvedGraalPyVersion: '23.1.0-a.1'
});
});
});
describeSkipOnWindows('installGraalPy', () => {
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 = jest.spyOn(tc, 'find');
tcFind.mockImplementation(() =>
path.join('GraalPy', '3.6.12', architecture)
);
spyDownloadTool = jest.spyOn(tc, 'downloadTool');
spyDownloadTool.mockImplementation(() => path.join(tempDir, 'GraalPy'));
spyExtractZip = jest.spyOn(tc, 'extractZip');
spyExtractZip.mockImplementation(() => tempDir);
spyExtractTar = jest.spyOn(tc, 'extractTar');
spyExtractTar.mockImplementation(() => tempDir);
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => {});
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
spyFsReadDir = jest.spyOn(fs, 'readdirSync');
spyFsReadDir.mockImplementation(() => ['GraalPyTest']);
spyFsWriteFile = jest.spyOn(fs, 'writeFileSync');
spyFsWriteFile.mockImplementation(() => undefined);
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient.mockImplementation(
async (): Promise<ifm.ITypedResponse<IGraalPyManifestRelease[]>> => {
const result = JSON.stringify(manifestData);
return {
statusCode: 200,
headers: {},
result: JSON.parse(result) as IGraalPyManifestRelease[]
};
}
);
spyExec = jest.spyOn(exec, 'exec');
spyExec.mockImplementation(() => undefined);
spySymlinkSync = jest.spyOn(fs, 'symlinkSync');
spySymlinkSync.mockImplementation(() => undefined);
spyExistsSync = jest.spyOn(fs, 'existsSync');
spyExistsSync.mockImplementation(() => false);
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
jest.restoreAllMocks();
});
it('throw if release is not found', async () => {
await expect(
installer.installGraalPy('7.3.3', architecture, false, undefined)
).rejects.toThrow(
`GraalPy version 7.3.3 with arch ${architecture} not found`
);
expect(spyHttpClient).toHaveBeenCalled();
expect(spyDownloadTool).not.toHaveBeenCalled();
expect(spyExec).not.toHaveBeenCalled();
});
it('found and install GraalPy', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '21.3.0', architecture)
);
spyChmodSync = jest.spyOn(fs, 'chmodSync');
spyChmodSync.mockImplementation(() => undefined);
await expect(
installer.installGraalPy('21.x', architecture, false, undefined)
).resolves.toEqual({
installDir: path.join(toolDir, 'GraalPy', '21.3.0', architecture),
resolvedGraalPyVersion: '21.3.0'
});
expect(spyHttpClient).toHaveBeenCalled();
expect(spyDownloadTool).toHaveBeenCalled();
expect(spyCacheDir).toHaveBeenCalled();
expect(spyExec).toHaveBeenCalled();
});
it('found and install GraalPy, pre-release fallback', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.1.0', architecture)
);
spyChmodSync = jest.spyOn(fs, 'chmodSync');
spyChmodSync.mockImplementation(() => undefined);
await expect(
installer.installGraalPy('23.1.x', architecture, false, undefined)
).rejects.toThrow();
await expect(
installer.installGraalPy('23.1.x', architecture, true, undefined)
).resolves.toEqual({
installDir: path.join(toolDir, 'GraalPy', '23.1.0', architecture),
resolvedGraalPyVersion: '23.1.0-a.1'
});
expect(spyHttpClient).toHaveBeenCalled();
expect(spyDownloadTool).toHaveBeenCalled();
expect(spyCacheDir).toHaveBeenCalled();
expect(spyExec).toHaveBeenCalled();
});
});

View File

@ -11,7 +11,8 @@ import {
isCacheFeatureAvailable, isCacheFeatureAvailable,
getVersionInputFromFile, getVersionInputFromFile,
getVersionInputFromPlainFile, getVersionInputFromPlainFile,
getVersionInputFromTomlFile getVersionInputFromTomlFile,
getNextPageUrl
} from '../src/utils'; } from '../src/utils';
jest.mock('@actions/cache'); jest.mock('@actions/cache');
@ -107,7 +108,7 @@ describe('Version from file test', () => {
await io.mkdirP(tempDir); await io.mkdirP(tempDir);
const pythonVersionFileName = 'pyproject.toml'; const pythonVersionFileName = 'pyproject.toml';
const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName); const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName);
const pythonVersion = '>=3.7'; const pythonVersion = '>=3.7.0';
const pythonVersionFileContent = `[project]\nrequires-python = "${pythonVersion}"`; const pythonVersionFileContent = `[project]\nrequires-python = "${pythonVersion}"`;
fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent); fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent);
expect(_fn(pythonVersionFilePath)).toEqual([pythonVersion]); expect(_fn(pythonVersionFilePath)).toEqual([pythonVersion]);
@ -119,7 +120,7 @@ describe('Version from file test', () => {
await io.mkdirP(tempDir); await io.mkdirP(tempDir);
const pythonVersionFileName = 'pyproject.toml'; const pythonVersionFileName = 'pyproject.toml';
const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName); const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName);
const pythonVersion = '>=3.7'; const pythonVersion = '>=3.7.0';
const pythonVersionFileContent = `[tool.poetry.dependencies]\npython = "${pythonVersion}"`; const pythonVersionFileContent = `[tool.poetry.dependencies]\npython = "${pythonVersion}"`;
fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent); fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent);
expect(_fn(pythonVersionFilePath)).toEqual([pythonVersion]); expect(_fn(pythonVersionFilePath)).toEqual([pythonVersion]);
@ -136,3 +137,25 @@ describe('Version from file test', () => {
} }
); );
}); });
describe('getNextPageUrl', () => {
it('GitHub API pagination next page is parsed correctly', () => {
function generateResponse(link: string) {
return {
statusCode: 200,
result: null,
headers: {
link: link
}
};
}
const page1Links =
'<https://api.github.com/repositories/129883600/releases?page=2>; rel="next", <https://api.github.com/repositories/129883600/releases?page=3>; rel="last"';
expect(getNextPageUrl(generateResponse(page1Links))).toStrictEqual(
'https://api.github.com/repositories/129883600/releases?page=2'
);
const page2Links =
'<https://api.github.com/repositories/129883600/releases?page=1>; rel="prev", <https://api.github.com/repositories/129883600/releases?page=1>; rel="first"';
expect(getNextPageUrl(generateResponse(page2Links))).toBeNull();
});
});

View File

@ -3300,8 +3300,11 @@ var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER ||
// Max safe segment length for coercion. // Max safe segment length for coercion.
var MAX_SAFE_COMPONENT_LENGTH = 16 var MAX_SAFE_COMPONENT_LENGTH = 16
var MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6
// The actual regexps go on exports.re // The actual regexps go on exports.re
var re = exports.re = [] var re = exports.re = []
var safeRe = exports.safeRe = []
var src = exports.src = [] var src = exports.src = []
var t = exports.tokens = {} var t = exports.tokens = {}
var R = 0 var R = 0
@ -3310,6 +3313,31 @@ function tok (n) {
t[n] = R++ t[n] = R++
} }
var LETTERDASHNUMBER = '[a-zA-Z0-9-]'
// Replace some greedy regex tokens to prevent regex dos issues. These regex are
// used internally via the safeRe object since all inputs in this library get
// normalized first to trim and collapse all extra whitespace. The original
// regexes are exported for userland consumption and lower level usage. A
// future breaking change could export the safer regex only with a note that
// all input should have extra whitespace removed.
var safeRegexReplacements = [
['\\s', 1],
['\\d', MAX_LENGTH],
[LETTERDASHNUMBER, MAX_SAFE_BUILD_LENGTH],
]
function makeSafeRe (value) {
for (var i = 0; i < safeRegexReplacements.length; i++) {
var token = safeRegexReplacements[i][0]
var max = safeRegexReplacements[i][1]
value = value
.split(token + '*').join(token + '{0,' + max + '}')
.split(token + '+').join(token + '{1,' + max + '}')
}
return value
}
// The following Regular Expressions can be used for tokenizing, // The following Regular Expressions can be used for tokenizing,
// validating, and parsing SemVer version strings. // validating, and parsing SemVer version strings.
@ -3319,14 +3347,14 @@ function tok (n) {
tok('NUMERICIDENTIFIER') tok('NUMERICIDENTIFIER')
src[t.NUMERICIDENTIFIER] = '0|[1-9]\\d*' src[t.NUMERICIDENTIFIER] = '0|[1-9]\\d*'
tok('NUMERICIDENTIFIERLOOSE') tok('NUMERICIDENTIFIERLOOSE')
src[t.NUMERICIDENTIFIERLOOSE] = '[0-9]+' src[t.NUMERICIDENTIFIERLOOSE] = '\\d+'
// ## Non-numeric Identifier // ## Non-numeric Identifier
// Zero or more digits, followed by a letter or hyphen, and then zero or // Zero or more digits, followed by a letter or hyphen, and then zero or
// more letters, digits, or hyphens. // more letters, digits, or hyphens.
tok('NONNUMERICIDENTIFIER') tok('NONNUMERICIDENTIFIER')
src[t.NONNUMERICIDENTIFIER] = '\\d*[a-zA-Z-][a-zA-Z0-9-]*' src[t.NONNUMERICIDENTIFIER] = '\\d*[a-zA-Z-]' + LETTERDASHNUMBER + '*'
// ## Main Version // ## Main Version
// Three dot-separated numeric identifiers. // Three dot-separated numeric identifiers.
@ -3368,7 +3396,7 @@ src[t.PRERELEASELOOSE] = '(?:-?(' + src[t.PRERELEASEIDENTIFIERLOOSE] +
// Any combination of digits, letters, or hyphens. // Any combination of digits, letters, or hyphens.
tok('BUILDIDENTIFIER') tok('BUILDIDENTIFIER')
src[t.BUILDIDENTIFIER] = '[0-9A-Za-z-]+' src[t.BUILDIDENTIFIER] = LETTERDASHNUMBER + '+'
// ## Build Metadata // ## Build Metadata
// Plus sign, followed by one or more period-separated build metadata // Plus sign, followed by one or more period-separated build metadata
@ -3448,6 +3476,7 @@ src[t.COERCE] = '(^|[^\\d])' +
'(?:$|[^\\d])' '(?:$|[^\\d])'
tok('COERCERTL') tok('COERCERTL')
re[t.COERCERTL] = new RegExp(src[t.COERCE], 'g') re[t.COERCERTL] = new RegExp(src[t.COERCE], 'g')
safeRe[t.COERCERTL] = new RegExp(makeSafeRe(src[t.COERCE]), 'g')
// Tilde ranges. // Tilde ranges.
// Meaning is "reasonably at or greater than" // Meaning is "reasonably at or greater than"
@ -3457,6 +3486,7 @@ src[t.LONETILDE] = '(?:~>?)'
tok('TILDETRIM') tok('TILDETRIM')
src[t.TILDETRIM] = '(\\s*)' + src[t.LONETILDE] + '\\s+' src[t.TILDETRIM] = '(\\s*)' + src[t.LONETILDE] + '\\s+'
re[t.TILDETRIM] = new RegExp(src[t.TILDETRIM], 'g') re[t.TILDETRIM] = new RegExp(src[t.TILDETRIM], 'g')
safeRe[t.TILDETRIM] = new RegExp(makeSafeRe(src[t.TILDETRIM]), 'g')
var tildeTrimReplace = '$1~' var tildeTrimReplace = '$1~'
tok('TILDE') tok('TILDE')
@ -3472,6 +3502,7 @@ src[t.LONECARET] = '(?:\\^)'
tok('CARETTRIM') tok('CARETTRIM')
src[t.CARETTRIM] = '(\\s*)' + src[t.LONECARET] + '\\s+' src[t.CARETTRIM] = '(\\s*)' + src[t.LONECARET] + '\\s+'
re[t.CARETTRIM] = new RegExp(src[t.CARETTRIM], 'g') re[t.CARETTRIM] = new RegExp(src[t.CARETTRIM], 'g')
safeRe[t.CARETTRIM] = new RegExp(makeSafeRe(src[t.CARETTRIM]), 'g')
var caretTrimReplace = '$1^' var caretTrimReplace = '$1^'
tok('CARET') tok('CARET')
@ -3493,6 +3524,7 @@ src[t.COMPARATORTRIM] = '(\\s*)' + src[t.GTLT] +
// this one has to use the /g flag // this one has to use the /g flag
re[t.COMPARATORTRIM] = new RegExp(src[t.COMPARATORTRIM], 'g') re[t.COMPARATORTRIM] = new RegExp(src[t.COMPARATORTRIM], 'g')
safeRe[t.COMPARATORTRIM] = new RegExp(makeSafeRe(src[t.COMPARATORTRIM]), 'g')
var comparatorTrimReplace = '$1$2$3' var comparatorTrimReplace = '$1$2$3'
// Something like `1.2.3 - 1.2.4` // Something like `1.2.3 - 1.2.4`
@ -3521,6 +3553,14 @@ for (var i = 0; i < R; i++) {
debug(i, src[i]) debug(i, src[i])
if (!re[i]) { if (!re[i]) {
re[i] = new RegExp(src[i]) re[i] = new RegExp(src[i])
// Replace all greedy whitespace to prevent regex dos issues. These regex are
// used internally via the safeRe object since all inputs in this library get
// normalized first to trim and collapse all extra whitespace. The original
// regexes are exported for userland consumption and lower level usage. A
// future breaking change could export the safer regex only with a note that
// all input should have extra whitespace removed.
safeRe[i] = new RegExp(makeSafeRe(src[i]))
} }
} }
@ -3545,7 +3585,7 @@ function parse (version, options) {
return null return null
} }
var r = options.loose ? re[t.LOOSE] : re[t.FULL] var r = options.loose ? safeRe[t.LOOSE] : safeRe[t.FULL]
if (!r.test(version)) { if (!r.test(version)) {
return null return null
} }
@ -3600,7 +3640,7 @@ function SemVer (version, options) {
this.options = options this.options = options
this.loose = !!options.loose this.loose = !!options.loose
var m = version.trim().match(options.loose ? re[t.LOOSE] : re[t.FULL]) var m = version.trim().match(options.loose ? safeRe[t.LOOSE] : safeRe[t.FULL])
if (!m) { if (!m) {
throw new TypeError('Invalid Version: ' + version) throw new TypeError('Invalid Version: ' + version)
@ -4045,6 +4085,7 @@ function Comparator (comp, options) {
return new Comparator(comp, options) return new Comparator(comp, options)
} }
comp = comp.trim().split(/\s+/).join(' ')
debug('comparator', comp, options) debug('comparator', comp, options)
this.options = options this.options = options
this.loose = !!options.loose this.loose = !!options.loose
@ -4061,7 +4102,7 @@ function Comparator (comp, options) {
var ANY = {} var ANY = {}
Comparator.prototype.parse = function (comp) { Comparator.prototype.parse = function (comp) {
var r = this.options.loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR] var r = this.options.loose ? safeRe[t.COMPARATORLOOSE] : safeRe[t.COMPARATOR]
var m = comp.match(r) var m = comp.match(r)
if (!m) { if (!m) {
@ -4185,9 +4226,16 @@ function Range (range, options) {
this.loose = !!options.loose this.loose = !!options.loose
this.includePrerelease = !!options.includePrerelease this.includePrerelease = !!options.includePrerelease
// First, split based on boolean or || // First reduce all whitespace as much as possible so we do not have to rely
// on potentially slow regexes like \s*. This is then stored and used for
// future error messages as well.
this.raw = range this.raw = range
this.set = range.split(/\s*\|\|\s*/).map(function (range) { .trim()
.split(/\s+/)
.join(' ')
// First, split based on boolean or ||
this.set = this.raw.split('||').map(function (range) {
return this.parseRange(range.trim()) return this.parseRange(range.trim())
}, this).filter(function (c) { }, this).filter(function (c) {
// throw out any that are not relevant for whatever reason // throw out any that are not relevant for whatever reason
@ -4195,7 +4243,7 @@ function Range (range, options) {
}) })
if (!this.set.length) { if (!this.set.length) {
throw new TypeError('Invalid SemVer Range: ' + range) throw new TypeError('Invalid SemVer Range: ' + this.raw)
} }
this.format() this.format()
@ -4214,20 +4262,19 @@ Range.prototype.toString = function () {
Range.prototype.parseRange = function (range) { Range.prototype.parseRange = function (range) {
var loose = this.options.loose var loose = this.options.loose
range = range.trim()
// `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4` // `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4`
var hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE] var hr = loose ? safeRe[t.HYPHENRANGELOOSE] : safeRe[t.HYPHENRANGE]
range = range.replace(hr, hyphenReplace) range = range.replace(hr, hyphenReplace)
debug('hyphen replace', range) debug('hyphen replace', range)
// `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5` // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5`
range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace) range = range.replace(safeRe[t.COMPARATORTRIM], comparatorTrimReplace)
debug('comparator trim', range, re[t.COMPARATORTRIM]) debug('comparator trim', range, safeRe[t.COMPARATORTRIM])
// `~ 1.2.3` => `~1.2.3` // `~ 1.2.3` => `~1.2.3`
range = range.replace(re[t.TILDETRIM], tildeTrimReplace) range = range.replace(safeRe[t.TILDETRIM], tildeTrimReplace)
// `^ 1.2.3` => `^1.2.3` // `^ 1.2.3` => `^1.2.3`
range = range.replace(re[t.CARETTRIM], caretTrimReplace) range = range.replace(safeRe[t.CARETTRIM], caretTrimReplace)
// normalize spaces // normalize spaces
range = range.split(/\s+/).join(' ') range = range.split(/\s+/).join(' ')
@ -4235,7 +4282,7 @@ Range.prototype.parseRange = function (range) {
// At this point, the range is completely trimmed and // At this point, the range is completely trimmed and
// ready to be split into comparators. // ready to be split into comparators.
var compRe = loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR] var compRe = loose ? safeRe[t.COMPARATORLOOSE] : safeRe[t.COMPARATOR]
var set = range.split(' ').map(function (comp) { var set = range.split(' ').map(function (comp) {
return parseComparator(comp, this.options) return parseComparator(comp, this.options)
}, this).join(' ').split(/\s+/) }, this).join(' ').split(/\s+/)
@ -4335,7 +4382,7 @@ function replaceTildes (comp, options) {
} }
function replaceTilde (comp, options) { function replaceTilde (comp, options) {
var r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE] var r = options.loose ? safeRe[t.TILDELOOSE] : safeRe[t.TILDE]
return comp.replace(r, function (_, M, m, p, pr) { return comp.replace(r, function (_, M, m, p, pr) {
debug('tilde', comp, _, M, m, p, pr) debug('tilde', comp, _, M, m, p, pr)
var ret var ret
@ -4376,7 +4423,7 @@ function replaceCarets (comp, options) {
function replaceCaret (comp, options) { function replaceCaret (comp, options) {
debug('caret', comp, options) debug('caret', comp, options)
var r = options.loose ? re[t.CARETLOOSE] : re[t.CARET] var r = options.loose ? safeRe[t.CARETLOOSE] : safeRe[t.CARET]
return comp.replace(r, function (_, M, m, p, pr) { return comp.replace(r, function (_, M, m, p, pr) {
debug('caret', comp, _, M, m, p, pr) debug('caret', comp, _, M, m, p, pr)
var ret var ret
@ -4435,7 +4482,7 @@ function replaceXRanges (comp, options) {
function replaceXRange (comp, options) { function replaceXRange (comp, options) {
comp = comp.trim() comp = comp.trim()
var r = options.loose ? re[t.XRANGELOOSE] : re[t.XRANGE] var r = options.loose ? safeRe[t.XRANGELOOSE] : safeRe[t.XRANGE]
return comp.replace(r, function (ret, gtlt, M, m, p, pr) { return comp.replace(r, function (ret, gtlt, M, m, p, pr) {
debug('xRange', comp, ret, gtlt, M, m, p, pr) debug('xRange', comp, ret, gtlt, M, m, p, pr)
var xM = isX(M) var xM = isX(M)
@ -4510,7 +4557,7 @@ function replaceXRange (comp, options) {
function replaceStars (comp, options) { function replaceStars (comp, options) {
debug('replaceStars', comp, options) debug('replaceStars', comp, options)
// Looseness is ignored here. star is always as loose as it gets! // Looseness is ignored here. star is always as loose as it gets!
return comp.trim().replace(re[t.STAR], '') return comp.trim().replace(safeRe[t.STAR], '')
} }
// This function is passed to string.replace(re[t.HYPHENRANGE]) // This function is passed to string.replace(re[t.HYPHENRANGE])
@ -4836,7 +4883,7 @@ function coerce (version, options) {
var match = null var match = null
if (!options.rtl) { if (!options.rtl) {
match = version.match(re[t.COERCE]) match = version.match(safeRe[t.COERCE])
} else { } else {
// Find the right-most coercible string that does not share // Find the right-most coercible string that does not share
// a terminus with a more left-ward coercible string. // a terminus with a more left-ward coercible string.
@ -4847,17 +4894,17 @@ function coerce (version, options) {
// Stop when we get a match that ends at the string end, since no // Stop when we get a match that ends at the string end, since no
// coercible string can be more right-ward without the same terminus. // coercible string can be more right-ward without the same terminus.
var next var next
while ((next = re[t.COERCERTL].exec(version)) && while ((next = safeRe[t.COERCERTL].exec(version)) &&
(!match || match.index + match[0].length !== version.length) (!match || match.index + match[0].length !== version.length)
) { ) {
if (!match || if (!match ||
next.index + next[0].length !== match.index + match[0].length) { next.index + next[0].length !== match.index + match[0].length) {
match = next match = next
} }
re[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length safeRe[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length
} }
// leave it in a clean state // leave it in a clean state
re[t.COERCERTL].lastIndex = -1 safeRe[t.COERCERTL].lastIndex = -1
} }
if (match === null) { if (match === null) {
@ -59042,7 +59089,11 @@ module.exports = v4;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k; if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) { }) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k; if (k2 === undefined) k2 = k;
o[k2] = m[k]; o[k2] = m[k];
@ -59151,7 +59202,11 @@ exports.CACHE_DEPENDENCY_BACKUP_PATH = '**/pyproject.toml';
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k; if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) { }) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k; if (k2 === undefined) k2 = k;
o[k2] = m[k]; o[k2] = m[k];

747
dist/setup/index.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,10 @@
- [Outputs](advanced-usage.md#outputs) - [Outputs](advanced-usage.md#outputs)
- [Environment variables](advanced-usage.md#environment-variables) - [Environment variables](advanced-usage.md#environment-variables)
- [Using update-environment flag](advanced-usage.md#using-update-environment-flag) - [Using update-environment flag](advanced-usage.md#using-update-environment-flag)
- [Available versions of Python and PyPy](advanced-usage.md#available-versions-of-python-and-pypy) - [Available versions of Python and PyPy](advanced-usage.md#available-versions-of-python-pypy-and-graalpy)
- [Python](advanced-usage.md#python) - [Python](advanced-usage.md#python)
- [PyPy](advanced-usage.md#pypy) - [PyPy](advanced-usage.md#pypy)
- [GraalPy](advanced-usage.md#graalpy)
- [Hosted tool cache](advanced-usage.md#hosted-tool-cache) - [Hosted tool cache](advanced-usage.md#hosted-tool-cache)
- [Using `setup-python` with a self-hosted runner](advanced-usage.md#using-setup-python-with-a-self-hosted-runner) - [Using `setup-python` with a self-hosted runner](advanced-usage.md#using-setup-python-with-a-self-hosted-runner)
- [Windows](advanced-usage.md#windows) - [Windows](advanced-usage.md#windows)
@ -30,7 +31,7 @@ If there is a specific version of Python that you need and you don't want to wor
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.7.5' python-version: '3.7.5'
@ -44,7 +45,7 @@ You can specify **only a major and minor version** if you are okay with the most
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.7' python-version: '3.7'
@ -58,7 +59,7 @@ You can specify the version with **prerelease tag** to download and set up an ac
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.12.0-alpha.1' python-version: '3.12.0-alpha.1'
@ -69,7 +70,7 @@ It's also possible to use **x.y-dev syntax** to download and set up the latest p
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.12-dev' python-version: '3.12-dev'
@ -82,7 +83,7 @@ You can also use several types of ranges that are specified in [semver](https://
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '>=3.9 <3.10' python-version: '>=3.9 <3.10'
@ -93,7 +94,7 @@ steps:
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.12.0-alpha - 3.12.0' python-version: '3.12.0-alpha - 3.12.0'
@ -104,7 +105,7 @@ steps:
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.x' python-version: '3.x'
@ -117,7 +118,7 @@ The version of PyPy should be specified in the format `pypy<python_version>[-v<p
The `-v<pypy_version>` parameter is optional and can be skipped. The latest PyPy version will be used in this case. The `-v<pypy_version>` parameter is optional and can be skipped. The latest PyPy version will be used in this case.
``` ```
pypy3.8 or pypy-3.8 # the latest available version of PyPy that supports Python 3.8 pypy3.9 or pypy-3.9 # the latest available version of PyPy that supports Python 3.9
pypy2.7 or pypy-2.7 # the latest available version of PyPy that supports Python 2.7 pypy2.7 or pypy-2.7 # the latest available version of PyPy that supports Python 2.7
pypy3.7-v7.3.3 or pypy-3.7-v7.3.3 # Python 3.7 and PyPy 7.3.3 pypy3.7-v7.3.3 or pypy-3.7-v7.3.3 # Python 3.7 and PyPy 7.3.3
pypy3.7-v7.x or pypy-3.7-v7.x # Python 3.7 and the latest available PyPy 7.x pypy3.7-v7.x or pypy-3.7-v7.x # Python 3.7 and the latest available PyPy 7.x
@ -137,7 +138,7 @@ jobs:
- 'pypy3.7' # the latest available version of PyPy that supports Python 3.7 - 'pypy3.7' # the latest available version of PyPy that supports Python 3.7
- 'pypy3.7-v7.3.3' # Python 3.7 and PyPy 7.3.3 - 'pypy3.7-v7.3.3' # Python 3.7 and PyPy 7.3.3
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@ -155,7 +156,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: | python-version: |
@ -172,7 +173,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: | python-version: |
@ -189,7 +190,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: | python-version: |
@ -211,10 +212,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [ '2.x', '3.x', 'pypy2.7', 'pypy3.7', 'pypy3.8' ] python-version: [ '2.x', '3.x', 'pypy2.7', 'pypy3.8', 'pypy3.9' ]
name: Python ${{ matrix.python-version }} sample name: Python ${{ matrix.python-version }} sample
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
@ -232,14 +233,14 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ['2.7', '3.7', '3.8', '3.9', '3.10', 'pypy2.7', 'pypy3.8'] python-version: ['2.7', '3.7', '3.8', '3.9', '3.10', 'pypy2.7', 'pypy3.9']
exclude: exclude:
- os: macos-latest - os: macos-latest
python-version: '3.8' python-version: '3.8'
- os: windows-latest - os: windows-latest
python-version: '3.6' python-version: '3.6'
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
@ -256,7 +257,7 @@ jobs:
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version-file: '.python-version' # Read python version from a file .python-version python-version-file: '.python-version' # Read python version from a file .python-version
@ -265,7 +266,7 @@ steps:
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version-file: 'pyproject.toml' # Read python version from a file pyproject.toml python-version-file: 'pyproject.toml' # Read python version from a file pyproject.toml
@ -280,7 +281,7 @@ If `check-latest` is set to `true`, the action first checks if the cached versio
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.7' python-version: '3.7'
@ -295,7 +296,7 @@ steps:
**Caching pipenv dependencies:** **Caching pipenv dependencies:**
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.9' python-version: '3.9'
@ -308,7 +309,7 @@ steps:
**Caching poetry dependencies:** **Caching poetry dependencies:**
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install poetry - name: Install poetry
run: pipx install poetry run: pipx install poetry
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
@ -322,7 +323,7 @@ steps:
**Using a list of file paths to cache dependencies** **Using a list of file paths to cache dependencies**
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.9' python-version: '3.9'
@ -337,7 +338,7 @@ steps:
**Using wildcard patterns to cache dependencies** **Using wildcard patterns to cache dependencies**
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.9' python-version: '3.9'
@ -349,7 +350,7 @@ steps:
**Using a list of wildcard patterns to cache dependencies** **Using a list of wildcard patterns to cache dependencies**
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.10' python-version: '3.10'
@ -364,7 +365,7 @@ steps:
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.11' python-version: '3.11'
@ -387,7 +388,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
id: cp310 id: cp310
with: with:
@ -404,7 +405,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
id: cp310 id: cp310
with: with:
@ -420,7 +421,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
id: cp310 id: cp310
with: with:
@ -451,7 +452,7 @@ Such a requirement on side-effect could be because you don't want your composite
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
id: cp310 id: cp310
with: with:
@ -459,7 +460,7 @@ Such a requirement on side-effect could be because you don't want your composite
update-environment: false update-environment: false
- run: ${{ steps.cp310.outputs.python-path }} my_script.py - run: ${{ steps.cp310.outputs.python-path }} my_script.py
``` ```
## Available versions of Python and PyPy ## Available versions of Python, PyPy and GraalPy
### Python ### Python
`setup-python` is able to configure **Python** from two sources: `setup-python` is able to configure **Python** from two sources:
@ -490,6 +491,11 @@ Such a requirement on side-effect could be because you don't want your composite
- PyPy < 7.3.3 are not available to install on-flight. - PyPy < 7.3.3 are not available to install on-flight.
- If some versions are not available, you can open an issue in https://foss.heptapod.net/pypy/pypy/ - If some versions are not available, you can open an issue in https://foss.heptapod.net/pypy/pypy/
### GraalPy
`setup-python` is able to download GraalPy versions from the [official GraalPy repository](https://github.com/oracle/graalpython).
- All available versions that we can download are listed in [releases](https://github.com/oracle/graalpython/releases).
## Hosted tool cache ## Hosted tool cache
GitHub hosted runners have a tool cache that comes with a few versions of Python + PyPy already installed. This tool cache helps speed up runs and tool setup by not requiring any new downloads. There is an environment variable called `RUNNER_TOOL_CACHE` on each runner that describes the location of the tool cache with Python and PyPy installed. `setup-python` works by taking a specific version of Python or PyPy from this tool cache and adding it to PATH. GitHub hosted runners have a tool cache that comes with a few versions of Python + PyPy already installed. This tool cache helps speed up runs and tool setup by not requiring any new downloads. There is an environment variable called `RUNNER_TOOL_CACHE` on each runner that describes the location of the tool cache with Python and PyPy installed. `setup-python` works by taking a specific version of Python or PyPy from this tool cache and adding it to PATH.
@ -611,7 +617,7 @@ jobs:
python_version: ["3.11", "3.12"] python_version: ["3.11", "3.12"]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: "${{ matrix.python_version }}" python-version: "${{ matrix.python_version }}"

7134
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -36,21 +36,21 @@
"semver": "^7.5.2" "semver": "^7.5.2"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^27.0.2", "@types/jest": "^29.5.6",
"@types/node": "^16.11.25", "@types/node": "^16.11.25",
"@types/semver": "^7.1.0", "@types/semver": "^7.1.0",
"@typescript-eslint/eslint-plugin": "^5.54.0", "@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.54.0", "@typescript-eslint/parser": "^5.54.0",
"@vercel/ncc": "^0.33.4", "@vercel/ncc": "^0.38.0",
"eslint": "^8.35.0", "eslint": "^8.35.0",
"eslint-config-prettier": "^8.6.0", "eslint-config-prettier": "^8.6.0",
"eslint-plugin-jest": "^27.2.1", "eslint-plugin-jest": "^27.2.1",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"husky": "^7.0.2", "husky": "^7.0.2",
"jest": "^27.2.5", "jest": "^29.7.0",
"jest-circus": "^27.2.5", "jest-circus": "^29.7.0",
"prettier": "^2.8.4", "prettier": "^2.8.4",
"ts-jest": "^27.0.5", "ts-jest": "^29.1.1",
"typescript": "^4.2.3" "typescript": "^4.2.3"
}, },
"husky": { "husky": {

146
src/find-graalpy.ts Normal file
View File

@ -0,0 +1,146 @@
import * as path from 'path';
import * as graalpyInstall from './install-graalpy';
import {
IS_WINDOWS,
validateVersion,
IGraalPyManifestRelease,
getBinaryDirectory
} from './utils';
import * as semver from 'semver';
import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
export async function findGraalPyVersion(
versionSpec: string,
architecture: string,
updateEnvironment: boolean,
checkLatest: boolean,
allowPreReleases: boolean
): Promise<string> {
let resolvedGraalPyVersion = '';
let installDir: string | null;
let releases: IGraalPyManifestRelease[] | undefined;
let graalpyVersionSpec = parseGraalPyVersion(versionSpec);
if (checkLatest) {
releases = await graalpyInstall.getAvailableGraalPyVersions();
if (releases && releases.length > 0) {
const releaseData = graalpyInstall.findRelease(
releases,
graalpyVersionSpec,
architecture,
false
);
if (releaseData) {
core.info(`Resolved as GraalPy ${releaseData.resolvedGraalPyVersion}`);
graalpyVersionSpec = releaseData.resolvedGraalPyVersion;
} else {
core.info(
`Failed to resolve GraalPy ${graalpyVersionSpec} from manifest`
);
}
}
}
({installDir, resolvedGraalPyVersion} = findGraalPyToolCache(
graalpyVersionSpec,
architecture
));
if (!installDir) {
({installDir, resolvedGraalPyVersion} = await graalpyInstall.installGraalPy(
graalpyVersionSpec,
architecture,
allowPreReleases,
releases
));
}
const pipDir = IS_WINDOWS ? 'Scripts' : 'bin';
const _binDir = path.join(installDir, pipDir);
const binaryExtension = IS_WINDOWS ? '.exe' : '';
const pythonPath = path.join(
IS_WINDOWS ? installDir : _binDir,
`python${binaryExtension}`
);
const pythonLocation = getBinaryDirectory(installDir);
if (updateEnvironment) {
core.exportVariable('pythonLocation', installDir);
// https://cmake.org/cmake/help/latest/module/FindPython.html#module:FindPython
core.exportVariable('Python_ROOT_DIR', installDir);
// https://cmake.org/cmake/help/latest/module/FindPython2.html#module:FindPython2
core.exportVariable('Python2_ROOT_DIR', installDir);
// https://cmake.org/cmake/help/latest/module/FindPython3.html#module:FindPython3
core.exportVariable('Python3_ROOT_DIR', installDir);
core.exportVariable('PKG_CONFIG_PATH', pythonLocation + '/lib/pkgconfig');
core.addPath(pythonLocation);
core.addPath(_binDir);
}
core.setOutput('python-version', 'graalpy' + resolvedGraalPyVersion);
core.setOutput('python-path', pythonPath);
return resolvedGraalPyVersion;
}
export function findGraalPyToolCache(
graalpyVersion: string,
architecture: string
) {
let resolvedGraalPyVersion = '';
let installDir: string | null = tc.find(
'GraalPy',
graalpyVersion,
architecture
);
if (installDir) {
// 'tc.find' finds tool based on Python version but we also need to check
// whether GraalPy version satisfies requested version.
resolvedGraalPyVersion = path.basename(path.dirname(installDir));
const isGraalPyVersionSatisfies = semver.satisfies(
resolvedGraalPyVersion,
graalpyVersion
);
if (!isGraalPyVersionSatisfies) {
installDir = null;
resolvedGraalPyVersion = '';
}
}
if (!installDir) {
core.info(
`GraalPy version ${graalpyVersion} was not found in the local cache`
);
}
return {installDir, resolvedGraalPyVersion};
}
export function parseGraalPyVersion(versionSpec: string): string {
const versions = versionSpec.split('-').filter(item => !!item);
if (/^(graalpy)(.+)/.test(versions[0])) {
const version = versions[0].replace('graalpy', '');
versions.splice(0, 1, 'graalpy', version);
}
if (versions.length < 2 || versions[0] != 'graalpy') {
throw new Error(
"Invalid 'version' property for GraalPy. GraalPy version should be specified as 'graalpy<python-version>' or 'graalpy-<python-version>'. See README for examples and documentation."
);
}
const pythonVersion = versions[1];
if (!validateVersion(pythonVersion)) {
throw new Error(
"Invalid 'version' property for GraalPy. GraalPy versions should satisfy SemVer notation. See README for examples and documentation."
);
}
return pythonVersion;
}

View File

@ -7,7 +7,8 @@ import {
getPyPyVersionFromPath, getPyPyVersionFromPath,
readExactPyPyVersionFile, readExactPyPyVersionFile,
validatePythonVersionFormatForPyPy, validatePythonVersionFormatForPyPy,
IPyPyManifestRelease IPyPyManifestRelease,
getBinaryDirectory
} from './utils'; } from './utils';
import * as semver from 'semver'; import * as semver from 'semver';
@ -82,7 +83,7 @@ export async function findPyPyVersion(
IS_WINDOWS ? installDir : _binDir, IS_WINDOWS ? installDir : _binDir,
`python${binaryExtension}` `python${binaryExtension}`
); );
const pythonLocation = pypyInstall.getPyPyBinaryPath(installDir); const pythonLocation = getBinaryDirectory(installDir);
if (updateEnvironment) { if (updateEnvironment) {
core.exportVariable('pythonLocation', installDir); core.exportVariable('pythonLocation', installDir);
// https://cmake.org/cmake/help/latest/module/FindPython.html#module:FindPython // https://cmake.org/cmake/help/latest/module/FindPython.html#module:FindPython

262
src/install-graalpy.ts Normal file
View File

@ -0,0 +1,262 @@
import * as os from 'os';
import * as path from 'path';
import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
import * as semver from 'semver';
import * as httpm from '@actions/http-client';
import * as ifm from '@actions/http-client/interfaces';
import * as exec from '@actions/exec';
import fs from 'fs';
import {
IS_WINDOWS,
IGraalPyManifestRelease,
createSymlinkInFolder,
isNightlyKeyword,
getBinaryDirectory,
getNextPageUrl
} from './utils';
const TOKEN = core.getInput('token');
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;
export async function installGraalPy(
graalpyVersion: string,
architecture: string,
allowPreReleases: boolean,
releases: IGraalPyManifestRelease[] | undefined
) {
let downloadDir;
releases = releases ?? (await getAvailableGraalPyVersions());
if (!releases || !releases.length) {
throw new Error('No release was found in GraalPy version.json');
}
let releaseData = findRelease(releases, graalpyVersion, architecture, false);
if (allowPreReleases && (!releaseData || !releaseData.foundAsset)) {
// check for pre-release
core.info(
[
`Stable GraalPy version ${graalpyVersion} with arch ${architecture} not found`,
`Trying pre-release versions`
].join(os.EOL)
);
releaseData = findRelease(releases, graalpyVersion, architecture, true);
}
if (!releaseData || !releaseData.foundAsset) {
throw new Error(
`GraalPy version ${graalpyVersion} with arch ${architecture} not found`
);
}
const {foundAsset, resolvedGraalPyVersion} = releaseData;
const downloadUrl = `${foundAsset.browser_download_url}`;
core.info(`Downloading GraalPy from "${downloadUrl}" ...`);
try {
const graalpyPath = await tc.downloadTool(downloadUrl, undefined, AUTH);
core.info('Extracting downloaded archive...');
downloadDir = await tc.extractTar(graalpyPath);
// root folder in archive can have unpredictable name so just take the first folder
// downloadDir is unique folder under TEMP and can't contain any other folders
const archiveName = fs.readdirSync(downloadDir)[0];
const toolDir = path.join(downloadDir, archiveName);
let installDir = toolDir;
if (!isNightlyKeyword(resolvedGraalPyVersion)) {
installDir = await tc.cacheDir(
toolDir,
'GraalPy',
resolvedGraalPyVersion,
architecture
);
}
const binaryPath = getBinaryDirectory(installDir);
await createGraalPySymlink(binaryPath, resolvedGraalPyVersion);
await installPip(binaryPath);
return {installDir, resolvedGraalPyVersion};
} catch (err) {
if (err instanceof Error) {
// Rate limit?
if (
err instanceof tc.HTTPError &&
(err.httpStatusCode === 403 || err.httpStatusCode === 429)
) {
core.info(
`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`
);
} else {
core.info(err.message);
}
if (err.stack !== undefined) {
core.debug(err.stack);
}
}
throw err;
}
}
export async function getAvailableGraalPyVersions() {
const http: httpm.HttpClient = new httpm.HttpClient('tool-cache');
const headers: ifm.IHeaders = {};
if (AUTH) {
headers.authorization = AUTH;
}
let url: string | null =
'https://api.github.com/repos/oracle/graalpython/releases';
const result: IGraalPyManifestRelease[] = [];
do {
const response: ifm.ITypedResponse<IGraalPyManifestRelease[]> =
await http.getJson(url, headers);
if (!response.result) {
throw new Error(
`Unable to retrieve the list of available GraalPy versions from '${url}'`
);
}
result.push(...response.result);
url = getNextPageUrl(response);
} while (url);
return result;
}
async function createGraalPySymlink(
graalpyBinaryPath: string,
graalpyVersion: string
) {
const version = semver.coerce(graalpyVersion)!;
const pythonBinaryPostfix = semver.major(version);
const pythonMinor = semver.minor(version);
const graalpyMajorMinorBinaryPostfix = `${pythonBinaryPostfix}.${pythonMinor}`;
const binaryExtension = IS_WINDOWS ? '.exe' : '';
core.info('Creating symlinks...');
createSymlinkInFolder(
graalpyBinaryPath,
`graalpy${binaryExtension}`,
`python${pythonBinaryPostfix}${binaryExtension}`,
true
);
createSymlinkInFolder(
graalpyBinaryPath,
`graalpy${binaryExtension}`,
`python${binaryExtension}`,
true
);
createSymlinkInFolder(
graalpyBinaryPath,
`graalpy${binaryExtension}`,
`graalpy${graalpyMajorMinorBinaryPostfix}${binaryExtension}`,
true
);
}
async function installPip(pythonLocation: string) {
core.info(
"Installing pip (GraalPy doesn't update pip because it uses a patched version of pip)"
);
const pythonBinary = path.join(pythonLocation, 'python');
await exec.exec(`${pythonBinary} -m ensurepip --default-pip`);
}
export function graalPyTagToVersion(tag: string) {
const versionPattern = /.*-(\d+\.\d+\.\d+(?:\.\d+)?)((?:a|b|rc))?(\d*)?/;
const match = tag.match(versionPattern);
if (match && match[2]) {
return `${match[1]}-${match[2]}.${match[3]}`;
} else if (match) {
return match[1];
} else {
return tag.replace(/.*-/, '');
}
}
export function findRelease(
releases: IGraalPyManifestRelease[],
graalpyVersion: string,
architecture: string,
includePrerelease: boolean
) {
const options = {includePrerelease: includePrerelease};
const filterReleases = releases.filter(item => {
const isVersionSatisfied = semver.satisfies(
graalPyTagToVersion(item.tag_name),
graalpyVersion,
options
);
return (
isVersionSatisfied && !!findAsset(item, architecture, process.platform)
);
});
if (!filterReleases.length) {
return null;
}
const sortedReleases = filterReleases.sort((previous, current) =>
semver.compare(
semver.coerce(graalPyTagToVersion(current.tag_name))!,
semver.coerce(graalPyTagToVersion(previous.tag_name))!
)
);
const foundRelease = sortedReleases[0];
const foundAsset = findAsset(foundRelease, architecture, process.platform);
return {
foundAsset,
resolvedGraalPyVersion: graalPyTagToVersion(foundRelease.tag_name)
};
}
export function toGraalPyPlatform(platform: string) {
switch (platform) {
case 'win32':
return 'windows';
case 'darwin':
return 'macos';
}
return platform;
}
export function toGraalPyArchitecture(architecture: string) {
switch (architecture) {
case 'x64':
return 'amd64';
case 'arm64':
return 'aarch64';
}
return architecture;
}
export function findAsset(
item: IGraalPyManifestRelease,
architecture: string,
platform: string
) {
const graalpyArch = toGraalPyArchitecture(architecture);
const graalpyPlatform = toGraalPyPlatform(platform);
const found = item.assets.filter(
file =>
file.name.startsWith('graalpy') &&
file.name.endsWith(`-${graalpyPlatform}-${graalpyArch}.tar.gz`)
);
/*
In the future there could be more variants of GraalPy for a single release. Pick the shortest name, that one is the most likely to be the primary variant.
*/
found.sort((f1, f2) => f1.name.length - f2.name.length);
return found[0];
}

View File

@ -13,7 +13,8 @@ import {
IPyPyManifestRelease, IPyPyManifestRelease,
createSymlinkInFolder, createSymlinkInFolder,
isNightlyKeyword, isNightlyKeyword,
writeExactPyPyVersionFile writeExactPyPyVersionFile,
getBinaryDirectory
} from './utils'; } from './utils';
export async function installPyPy( export async function installPyPy(
@ -94,7 +95,7 @@ export async function installPyPy(
writeExactPyPyVersionFile(installDir, resolvedPyPyVersion); writeExactPyPyVersionFile(installDir, resolvedPyPyVersion);
const binaryPath = getPyPyBinaryPath(installDir); const binaryPath = getBinaryDirectory(installDir);
await createPyPySymlink(binaryPath, resolvedPythonVersion); await createPyPySymlink(binaryPath, resolvedPythonVersion);
await installPip(binaryPath); await installPip(binaryPath);
@ -237,15 +238,6 @@ export function findRelease(
}; };
} }
/** Get PyPy binary location from the tool of installation directory
* - On Linux and macOS, the Python interpreter is in 'bin'.
* - On Windows, it is in the installation root.
*/
export function getPyPyBinaryPath(installDir: string) {
const _binDir = path.join(installDir, 'bin');
return IS_WINDOWS ? installDir : _binDir;
}
export function pypyVersionToSemantic(versionSpec: string) { export function pypyVersionToSemantic(versionSpec: string) {
const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc))(\d*)/g; const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc))(\d*)/g;
return versionSpec.replace(prereleaseVersion, '$1-$2.$3'); return versionSpec.replace(prereleaseVersion, '$1-$2.$3');

View File

@ -1,6 +1,7 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as finder from './find-python'; import * as finder from './find-python';
import * as finderPyPy from './find-pypy'; import * as finderPyPy from './find-pypy';
import * as finderGraalPy from './find-graalpy';
import * as path from 'path'; import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
import fs from 'fs'; import fs from 'fs';
@ -17,6 +18,10 @@ function isPyPyVersion(versionSpec: string) {
return versionSpec.startsWith('pypy'); return versionSpec.startsWith('pypy');
} }
function isGraalPyVersion(versionSpec: string) {
return versionSpec.startsWith('graalpy');
}
async function cacheDependencies(cache: string, pythonVersion: string) { async function cacheDependencies(cache: string, pythonVersion: string) {
const cacheDependencyPath = const cacheDependencyPath =
core.getInput('cache-dependency-path') || undefined; core.getInput('cache-dependency-path') || undefined;
@ -106,10 +111,20 @@ async function run() {
core.info( core.info(
`Successfully set up PyPy ${installed.resolvedPyPyVersion} with Python (${installed.resolvedPythonVersion})` `Successfully set up PyPy ${installed.resolvedPyPyVersion} with Python (${installed.resolvedPythonVersion})`
); );
} else if (isGraalPyVersion(version)) {
const installed = await finderGraalPy.findGraalPyVersion(
version,
arch,
updateEnvironment,
checkLatest,
allowPreReleases
);
pythonVersion = `${installed}`;
core.info(`Successfully set up GraalPy ${installed}`);
} else { } else {
if (version.startsWith('2')) { if (version.startsWith('2')) {
core.warning( core.warning(
'The support for python 2.7 will be removed on June 19. Related issue: https://github.com/actions/setup-python/issues/672' 'The support for python 2.7 was removed on June 19, 2023. Related issue: https://github.com/actions/setup-python/issues/672'
); );
} }
const installed = await finder.useCpythonVersion( const installed = await finder.useCpythonVersion(

View File

@ -6,6 +6,7 @@ import * as path from 'path';
import * as semver from 'semver'; import * as semver from 'semver';
import * as toml from '@iarna/toml'; import * as toml from '@iarna/toml';
import * as exec from '@actions/exec'; import * as exec from '@actions/exec';
import * as ifm from '@actions/http-client/interfaces';
export const IS_WINDOWS = process.platform === 'win32'; export const IS_WINDOWS = process.platform === 'win32';
export const IS_LINUX = process.platform === 'linux'; export const IS_LINUX = process.platform === 'linux';
@ -29,6 +30,16 @@ export interface IPyPyManifestRelease {
files: IPyPyManifestAsset[]; files: IPyPyManifestAsset[];
} }
export interface IGraalPyManifestAsset {
name: string;
browser_download_url: string;
}
export interface IGraalPyManifestRelease {
tag_name: string;
assets: IGraalPyManifestAsset[];
}
/** create Symlinks for downloaded PyPy /** create Symlinks for downloaded PyPy
* It should be executed only for downloaded versions in runtime, because * It should be executed only for downloaded versions in runtime, because
* toolcache versions have this setup. * toolcache versions have this setup.
@ -90,7 +101,7 @@ export function writeExactPyPyVersionFile(
} }
/** /**
* Python version should be specified explicitly like "x.y" (2.7, 3.6, 3.7) * Python version should be specified explicitly like "x.y" (3.10, 3.11, etc)
* "3.x" or "3" are not supported * "3.x" or "3" are not supported
* because it could cause ambiguity when both PyPy version and Python version are not precise * because it could cause ambiguity when both PyPy version and Python version are not precise
*/ */
@ -229,7 +240,21 @@ export function getVersionInputFromTomlFile(versionFile: string): string[] {
} }
core.info(`Extracted ${versions} from ${versionFile}`); core.info(`Extracted ${versions} from ${versionFile}`);
return Array.from(versions, version => version.split(',').join(' ')); const rawVersions = Array.from(versions, version =>
version.split(',').join(' ')
);
const validatedVersions = rawVersions
.map(item => semver.validRange(item, true))
.filter((versionRange, index) => {
if (!versionRange) {
core.debug(
`The version ${rawVersions[index]} is not valid SemVer range`
);
}
return !!versionRange;
}) as string[];
return validatedVersions;
} }
/** /**
@ -237,7 +262,7 @@ export function getVersionInputFromTomlFile(versionFile: string): string[] {
*/ */
export function getVersionInputFromPlainFile(versionFile: string): string[] { export function getVersionInputFromPlainFile(versionFile: string): string[] {
core.debug(`Trying to resolve version form ${versionFile}`); core.debug(`Trying to resolve version form ${versionFile}`);
const version = fs.readFileSync(versionFile, 'utf8'); const version = fs.readFileSync(versionFile, 'utf8').trim();
core.info(`Resolved ${versionFile} as ${version}`); core.info(`Resolved ${versionFile} as ${version}`);
return [version]; return [version];
} }
@ -252,3 +277,34 @@ export function getVersionInputFromFile(versionFile: string): string[] {
return getVersionInputFromPlainFile(versionFile); return getVersionInputFromPlainFile(versionFile);
} }
} }
/**
* Get the directory containing interpreter binary from installation directory of PyPy or GraalPy
* - On Linux and macOS, the Python interpreter is in 'bin'.
* - On Windows, it is in the installation root.
*/
export function getBinaryDirectory(installDir: string) {
return IS_WINDOWS ? installDir : path.join(installDir, 'bin');
}
/**
* Extract next page URL from a HTTP response "link" header. Such headers are used in GitHub APIs.
*/
export function getNextPageUrl<T>(response: ifm.ITypedResponse<T>) {
const responseHeaders = <ifm.IHeaders>response.headers;
const linkHeader = responseHeaders.link;
if (typeof linkHeader === 'string') {
for (const link of linkHeader.split(/\s*,\s*/)) {
const match = link.match(/<([^>]+)>(.*)/);
if (match) {
const url = match[1];
for (const param of match[2].split(/\s*;\s*/)) {
if (param.match(/rel="?next"?/)) {
return url;
}
}
}
}
}
return null;
}