diff --git a/.github/workflows/basic-validation.yml b/.github/workflows/basic-validation.yml new file mode 100644 index 000000000..f69f9a1d5 --- /dev/null +++ b/.github/workflows/basic-validation.yml @@ -0,0 +1,15 @@ +name: Basic validation + +on: + push: + branches: + - main + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' +jobs: + call-basic-validation: + name: Basic validation + uses: actions/reusable-workflows/.github/workflows/basic-validation.yml@main \ No newline at end of file diff --git a/.github/workflows/check-dist.yml b/.github/workflows/check-dist.yml index 6274fd288..1251c11d9 100644 --- a/.github/workflows/check-dist.yml +++ b/.github/workflows/check-dist.yml @@ -1,8 +1,3 @@ -# `dist/index.js` is a special file in Actions. -# When you reference an action with `uses:` in a workflow, -# `index.js` is the code that will run. -# For our project, we generate this file through a build process from other source files. -# We need to make sure the checked-in `index.js` actually matches what we expect it to be. name: Check dist/ on: @@ -17,36 +12,6 @@ on: workflow_dispatch: jobs: - check-dist: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Set Node.js 16.x - uses: actions/setup-node@v3 - with: - node-version: 16.x - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Rebuild the dist/ directory - run: npm run build - - - name: Compare the expected and actual dist/ directories - run: | - if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then - echo "Detected uncommitted changes after build. See status below:" - git diff - exit 1 - fi - id: diff - - # If index.js was different than expected, upload the expected version as an artifact - - uses: actions/upload-artifact@v3 - if: ${{ failure() && steps.diff.conclusion == 'failure' }} - with: - name: dist - path: dist/ + call-check-dist: + name: Check dist/ + uses: actions/reusable-workflows/.github/workflows/check-dist.yml@main \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3ea240d83..fd273502c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,47 +1,13 @@ -name: "Code scanning - action" +name: CodeQL analysis on: push: branches: [ 'main' ] pull_request: schedule: - - cron: '25 3 * * 5' + - cron: '0 3 * * 0' jobs: - CodeQL-Build: - - strategy: - fail-fast: false - - # CodeQL runs on ubuntu-latest and windows-latest - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + call-codeQL-analysis: + name: CodeQL analysis + uses: actions/reusable-workflows/.github/workflows/codeql-analysis.yml@main \ No newline at end of file diff --git a/.github/workflows/workflow.yml b/.github/workflows/e2e-tests.yml similarity index 86% rename from .github/workflows/workflow.yml rename to .github/workflows/e2e-tests.yml index 3575845e5..21dead7ee 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/e2e-tests.yml @@ -1,4 +1,5 @@ -name: Main workflow +name: e2e tests + on: push: branches: @@ -8,9 +9,10 @@ on: pull_request: paths-ignore: - '**.md' + jobs: - run: - name: Run + test-setup-python: + name: Test setup-python runs-on: ${{ matrix.operating-system }} strategy: matrix: @@ -19,21 +21,6 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - name: Set Node.js 16.x - uses: actions/setup-node@v3 - with: - node-version: 16.x - cache: npm - - - name: npm ci - run: npm ci - - - name: Lint - run: npm run format-check - - - name: npm test - run: npm test - - name: Run with setup-python 2.7 uses: ./ with: @@ -98,4 +85,4 @@ jobs: - name: Verify 3.10 run: python __tests__/verify-python.py 3.10 - name: Run python-path sample 3.10 - run: pipx run --python '${{ steps.cp310.outputs.python-path }}' nox --version + run: pipx run --python '${{ steps.cp310.outputs.python-path }}' nox --version \ No newline at end of file diff --git a/.github/workflows/licensed.yml b/.github/workflows/licensed.yml index 6f4cd9223..88d3eae61 100644 --- a/.github/workflows/licensed.yml +++ b/.github/workflows/licensed.yml @@ -9,21 +9,6 @@ on: - main jobs: - test: - runs-on: ubuntu-latest - name: Check licenses - steps: - - uses: actions/checkout@v3 - - name: Set Node.js 16.x - uses: actions/setup-node@v3 - with: - node-version: 16.x - cache: npm - - run: npm ci - - name: Install licensed - run: | - cd $RUNNER_TEMP - curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/3.4.4/licensed-3.4.4-linux-x64.tar.gz - sudo tar -xzf licensed.tar.gz - sudo mv licensed /usr/local/bin/licensed - - run: licensed status + call-licensed: + name: Licensed + uses: actions/reusable-workflows/.github/workflows/licensed.yml@main \ No newline at end of file diff --git a/.github/workflows/release-new-action-version.yml b/.github/workflows/release-new-action-version.yml index b8076d438..a28a50ce9 100644 --- a/.github/workflows/release-new-action-version.yml +++ b/.github/workflows/release-new-action-version.yml @@ -1,4 +1,5 @@ name: Release new action version + on: release: types: [released] @@ -24,4 +25,4 @@ jobs: uses: actions/publish-action@v0.2.1 with: source-tag: ${{ env.TAG_NAME }} - slack-webhook: ${{ secrets.SLACK_WEBHOOK }} + slack-webhook: ${{ secrets.SLACK_WEBHOOK }} \ No newline at end of file diff --git a/.github/workflows/test-pypy.yml b/.github/workflows/test-pypy.yml index de9ba6b79..5340c12f3 100644 --- a/.github/workflows/test-pypy.yml +++ b/.github/workflows/test-pypy.yml @@ -1,4 +1,5 @@ name: Validate PyPy e2e + on: push: branches: @@ -123,4 +124,46 @@ jobs: EXECUTABLE=${EXECUTABLE/-/} # remove the first '-' in "pypy-X.Y" -> "pypyX.Y" to match executable name EXECUTABLE=${EXECUTABLE%%-*} # remove any -* suffixe ${EXECUTABLE} --version + shell: bash + + setup-pypy-multiple-versions: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v3 + - name: Setup PyPy and check latest + uses: ./ + with: + python-version: | + pypy-3.7-v7.3.x + pypy3.8 + check-latest: true + - name: PyPy and Python version + run: python --version + + - name: Run simple code + run: python -c 'import math; print(math.factorial(5))' + + - name: Assert PyPy is running + run: | + import platform + assert platform.python_implementation().lower() == "pypy" + shell: python + + - name: Assert expected binaries (or symlinks) are present + run: | + EXECUTABLE="pypy-3.7-v7.3.x" + EXECUTABLE=${EXECUTABLE/-/} # remove the first '-' in "pypy-X.Y" -> "pypyX.Y" to match executable name + EXECUTABLE=${EXECUTABLE%%-*} # remove any -* suffixe + ${EXECUTABLE} --version + shell: bash + - name: Assert expected binaries (or symlinks) are present + run: | + EXECUTABLE='pypy3.8' + EXECUTABLE=${EXECUTABLE/pypy-/pypy} # remove the first '-' in "pypy-X.Y" -> "pypyX.Y" to match executable name + EXECUTABLE=${EXECUTABLE%%-*} # remove any -* suffixe + ${EXECUTABLE} --version shell: bash \ No newline at end of file diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 921449fb1..4f1ffd7f2 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -1,4 +1,5 @@ name: Validate Python e2e + on: push: branches: @@ -190,8 +191,35 @@ jobs: - name: Validate version run: | $pythonVersion = (python --version) - if ("$pythonVersion" -NotMatch "${{ matrix.python }}"){ - Write-Host "The current version is $pythonVersion; expected version is ${{ matrix.python }}" + if ("$pythonVersion" -NotMatch "${{ matrix.python-version }}"){ + Write-Host "The current version is $pythonVersion; expected version is ${{ matrix.python-version }}" + exit 1 + } + $pythonVersion + shell: pwsh + + setup-python-multiple-python-versions: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v3 + - name: Setup Python and check latest + uses: ./ + with: + python-version: | + 3.7 + 3.8 + 3.9 + 3.10 + check-latest: true + - name: Validate version + run: | + $pythonVersion = (python --version) + if ("$pythonVersion" -NotMatch "3.10"){ + Write-Host "The current version is $pythonVersion; expected version is 3.10" exit 1 } $pythonVersion diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts index 04ec7c51f..30fc61cb4 100644 --- a/__tests__/utils.test.ts +++ b/__tests__/utils.test.ts @@ -42,14 +42,13 @@ describe('validateVersion', () => { describe('isCacheFeatureAvailable', () => { it('isCacheFeatureAvailable disabled on GHES', () => { 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 { process.env['GITHUB_SERVER_URL'] = 'http://example.com'; - isCacheFeatureAvailable(); - } catch (error) { - expect(error).toHaveProperty( - '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.' - ); + expect(isCacheFeatureAvailable()).toBeFalsy(); + expect(infoMock).toHaveBeenCalledWith(message); } finally { delete process.env['GITHUB_SERVER_URL']; } diff --git a/dist/setup/index.js b/dist/setup/index.js index 6cf4871db..7d1b84946 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -66867,31 +66867,31 @@ function cacheDependencies(cache, pythonVersion) { }); } function resolveVersionInput() { - let version = core.getInput('python-version'); + let versions = core.getMultilineInput('python-version'); let versionFile = core.getInput('python-version-file'); - if (version && versionFile) { + if (versions.length && versionFile) { core.warning('Both python-version and python-version-file inputs are specified, only python-version will be used.'); } - if (version) { - return version; + if (versions.length) { + return versions; } if (versionFile) { if (!fs_1.default.existsSync(versionFile)) { throw new Error(`The specified python version file at: ${versionFile} doesn't exist.`); } - version = fs_1.default.readFileSync(versionFile, 'utf8'); + const version = fs_1.default.readFileSync(versionFile, 'utf8'); core.info(`Resolved ${versionFile} as ${version}`); - return version; + return [version]; } utils_1.logWarning("Neither 'python-version' nor 'python-version-file' inputs were supplied. Attempting to find '.python-version' file."); versionFile = '.python-version'; if (fs_1.default.existsSync(versionFile)) { - version = fs_1.default.readFileSync(versionFile, 'utf8'); + const version = fs_1.default.readFileSync(versionFile, 'utf8'); core.info(`Resolved ${versionFile} as ${version}`); - return version; + return [version]; } utils_1.logWarning(`${versionFile} doesn't exist.`); - return version; + return versions; } function run() { var _a; @@ -66904,22 +66904,26 @@ function run() { } core.debug(`Python is expected to be installed into ${process.env['RUNNER_TOOL_CACHE']}`); try { - const version = resolveVersionInput(); + const versions = resolveVersionInput(); const checkLatest = core.getBooleanInput('check-latest'); - if (version) { - let pythonVersion; + if (versions.length) { + let pythonVersion = ''; const arch = core.getInput('architecture') || os.arch(); const updateEnvironment = core.getBooleanInput('update-environment'); - if (isPyPyVersion(version)) { - const installed = yield finderPyPy.findPyPyVersion(version, arch, updateEnvironment, checkLatest); - pythonVersion = `${installed.resolvedPyPyVersion}-${installed.resolvedPythonVersion}`; - core.info(`Successfully set up PyPy ${installed.resolvedPyPyVersion} with Python (${installed.resolvedPythonVersion})`); - } - else { - const installed = yield finder.useCpythonVersion(version, arch, updateEnvironment, checkLatest); - pythonVersion = installed.version; - core.info(`Successfully set up ${installed.impl} (${pythonVersion})`); + core.startGroup('Installed versions'); + for (const version of versions) { + if (isPyPyVersion(version)) { + const installed = yield finderPyPy.findPyPyVersion(version, arch, updateEnvironment, checkLatest); + pythonVersion = `${installed.resolvedPyPyVersion}-${installed.resolvedPythonVersion}`; + core.info(`Successfully set up PyPy ${installed.resolvedPyPyVersion} with Python (${installed.resolvedPythonVersion})`); + } + else { + const installed = yield finder.useCpythonVersion(version, arch, updateEnvironment, checkLatest); + pythonVersion = installed.version; + core.info(`Successfully set up ${installed.impl} (${pythonVersion})`); + } } + core.endGroup(); const cache = core.getInput('cache'); if (cache && utils_1.isCacheFeatureAvailable()) { yield cacheDependencies(cache, pythonVersion); @@ -67057,16 +67061,15 @@ function isGhes() { } exports.isGhes = isGhes; function isCacheFeatureAvailable() { - if (!cache.isFeatureAvailable()) { - if (isGhes()) { - throw new Error('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.'); - } - else { - core.warning('The runner was not able to contact the cache service. Caching will be skipped'); - } + if (cache.isFeatureAvailable()) { + return true; + } + if (isGhes()) { + core.warning('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.'); return false; } - return true; + core.warning('The runner was not able to contact the cache service. Caching will be skipped'); + return false; } exports.isCacheFeatureAvailable = isCacheFeatureAvailable; function logWarning(message) { diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 643e73dd6..fa022d930 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -2,6 +2,7 @@ - [Using the python-version input](advanced-usage.md#using-the-python-version-input) - [Specifying a Python version](advanced-usage.md#specifying-a-python-version) - [Specifying a PyPy version](advanced-usage.md#specifying-a-pypy-version) + - [Specifying multiple Python and PyPy versions](advanced-usage.md#specifying-multiple-python/pypy-version) - [Matrix Testing](advanced-usage.md#matrix-testing) - [Using the python-version-file input](advanced-usage.md#using-the-python-version-file-input) - [Check latest version](advanced-usage.md#check-latest-version) @@ -132,6 +133,62 @@ jobs: ``` More details on PyPy syntax can be found in the [Available versions of PyPy](#pypy) section. +### Specifying multiple Python/PyPy version +The python-version input can get multiple python/pypy versions. The last specified version will be used as a default one. + +Download and set up multiple Python versions: + +```yaml +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: | + 3.8 + 3.9 + 3.10 + - run: python my_script.py +``` + +Download and set up multiple PyPy versions: + +```yaml +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: | + pypy-3.7-v7.3.x + pypy3.9-nightly + pypy3.8 + - run: python my_script.py +``` + +Download and set up multiple Python/PyPy versions: + +```yaml +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: | + 3.8 + 3.9 + pypy3.9-nightly + pypy3.8 + 3.10 + - run: python my_script.py +``` + ### Matrix Testing Using `setup-python` it's possible to use [matrix syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix) to install several versions of Python or PyPy: diff --git a/package.json b/package.json index 9f6c3d5d5..b8bfb1ec3 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "ncc build -o dist/setup src/setup-python.ts && ncc build -o dist/cache-save src/cache-save.ts", "format": "prettier --write \"{,!(node_modules)/**/}*.ts\"", "format-check": "prettier --check \"{,!(node_modules)/**/}*.ts\"", + "lint": "echo \"Fake command that does nothing. It is used in reusable workflows\"", "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": "jest --coverage" }, diff --git a/src/setup-python.ts b/src/setup-python.ts index d6e6bdaf0..0089b4016 100644 --- a/src/setup-python.ts +++ b/src/setup-python.ts @@ -22,18 +22,18 @@ async function cacheDependencies(cache: string, pythonVersion: string) { await cacheDistributor.restoreCache(); } -function resolveVersionInput(): string { - let version = core.getInput('python-version'); +function resolveVersionInput() { + let versions = core.getMultilineInput('python-version'); let versionFile = core.getInput('python-version-file'); - if (version && versionFile) { + if (versions.length && versionFile) { core.warning( 'Both python-version and python-version-file inputs are specified, only python-version will be used.' ); } - if (version) { - return version; + if (versions.length) { + return versions; } if (versionFile) { @@ -42,9 +42,9 @@ function resolveVersionInput(): string { `The specified python version file at: ${versionFile} doesn't exist.` ); } - version = fs.readFileSync(versionFile, 'utf8'); + const version = fs.readFileSync(versionFile, 'utf8'); core.info(`Resolved ${versionFile} as ${version}`); - return version; + return [version]; } logWarning( @@ -52,14 +52,14 @@ function resolveVersionInput(): string { ); versionFile = '.python-version'; if (fs.existsSync(versionFile)) { - version = fs.readFileSync(versionFile, 'utf8'); + const version = fs.readFileSync(versionFile, 'utf8'); core.info(`Resolved ${versionFile} as ${version}`); - return version; + return [version]; } logWarning(`${versionFile} doesn't exist.`); - return version; + return versions; } async function run() { @@ -75,35 +75,38 @@ async function run() { `Python is expected to be installed into ${process.env['RUNNER_TOOL_CACHE']}` ); try { - const version = resolveVersionInput(); + const versions = resolveVersionInput(); const checkLatest = core.getBooleanInput('check-latest'); - if (version) { - let pythonVersion: string; + if (versions.length) { + let pythonVersion = ''; const arch: string = core.getInput('architecture') || os.arch(); const updateEnvironment = core.getBooleanInput('update-environment'); - if (isPyPyVersion(version)) { - const installed = await finderPyPy.findPyPyVersion( - version, - arch, - updateEnvironment, - checkLatest - ); - pythonVersion = `${installed.resolvedPyPyVersion}-${installed.resolvedPythonVersion}`; - core.info( - `Successfully set up PyPy ${installed.resolvedPyPyVersion} with Python (${installed.resolvedPythonVersion})` - ); - } else { - const installed = await finder.useCpythonVersion( - version, - arch, - updateEnvironment, - checkLatest - ); - pythonVersion = installed.version; - core.info(`Successfully set up ${installed.impl} (${pythonVersion})`); + core.startGroup('Installed versions'); + for (const version of versions) { + if (isPyPyVersion(version)) { + const installed = await finderPyPy.findPyPyVersion( + version, + arch, + updateEnvironment, + checkLatest + ); + pythonVersion = `${installed.resolvedPyPyVersion}-${installed.resolvedPythonVersion}`; + core.info( + `Successfully set up PyPy ${installed.resolvedPyPyVersion} with Python (${installed.resolvedPythonVersion})` + ); + } else { + const installed = await finder.useCpythonVersion( + version, + arch, + updateEnvironment, + checkLatest + ); + pythonVersion = installed.version; + core.info(`Successfully set up ${installed.impl} (${pythonVersion})`); + } } - + core.endGroup(); const cache = core.getInput('cache'); if (cache && isCacheFeatureAvailable()) { await cacheDependencies(cache, pythonVersion); diff --git a/src/utils.ts b/src/utils.ts index 37059cb66..fe32eda94 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -105,21 +105,21 @@ export function isGhes(): boolean { } export function isCacheFeatureAvailable(): boolean { - if (!cache.isFeatureAvailable()) { - if (isGhes()) { - throw new Error( - '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.' - ); - } else { - core.warning( - 'The runner was not able to contact the cache service. Caching will be skipped' - ); - } + if (cache.isFeatureAvailable()) { + return true; + } + if (isGhes()) { + core.warning( + '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.' + ); return false; } - return true; + core.warning( + 'The runner was not able to contact the cache service. Caching will be skipped' + ); + return false; } export function logWarning(message: string): void {