From 2f06e9da25e11351919e97ebc9d3cd20dc7208ab Mon Sep 17 00:00:00 2001
From: Dmitry Shibanov <shibanov-1997@inbox.ru>
Date: Mon, 25 Jul 2022 16:54:04 +0200
Subject: [PATCH] Add check-latest functionality (#406)

---
 .github/workflows/test-pypy.yml            |  33 ++++++
 .github/workflows/test-python.yml          |  24 +++++
 .licenses/npm/@actions/http-client.dep.yml |  32 ------
 README.md                                  |  18 ++++
 __tests__/find-pypy.test.ts                | 117 +++++++++++++++++++--
 __tests__/finder.test.ts                   |  87 +++++++++++++--
 __tests__/install-pypy.test.ts             |  34 +++++-
 action.yml                                 |   3 +
 dist/setup/index.js                        |  66 +++++++++---
 src/find-pypy.ts                           |  34 +++++-
 src/find-python.ts                         |  29 ++++-
 src/install-pypy.ts                        |   8 +-
 src/install-python.ts                      |  29 +++--
 src/setup-python.ts                        |   8 +-
 14 files changed, 440 insertions(+), 82 deletions(-)
 delete mode 100644 .licenses/npm/@actions/http-client.dep.yml

diff --git a/.github/workflows/test-pypy.yml b/.github/workflows/test-pypy.yml
index 4800662e..de9ba6b7 100644
--- a/.github/workflows/test-pypy.yml
+++ b/.github/workflows/test-pypy.yml
@@ -91,3 +91,36 @@ jobs:
 
       - 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, 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'
+          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
\ No newline at end of file
diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml
index 17709ec5..921449fb 100644
--- a/.github/workflows/test-python.yml
+++ b/.github/workflows/test-python.yml
@@ -172,3 +172,27 @@ jobs:
 
     - 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, windows-latest, macos-latest]
+        python-version: ["3.8", "3.9", "3.10"]
+    steps:
+      - uses: actions/checkout@v3
+      - name: Setup Python and check latest
+        uses: ./
+        with:
+          python-version: ${{ matrix.python-version }}
+          check-latest: true
+      - name: Validate version
+        run: |
+          $pythonVersion = (python --version)
+          if ("$pythonVersion" -NotMatch "${{ matrix.python }}"){
+            Write-Host "The current version is $pythonVersion; expected version is ${{ matrix.python }}"
+            exit 1
+          }
+          $pythonVersion
+        shell: pwsh
\ No newline at end of file
diff --git a/.licenses/npm/@actions/http-client.dep.yml b/.licenses/npm/@actions/http-client.dep.yml
deleted file mode 100644
index 43316cbc..00000000
--- a/.licenses/npm/@actions/http-client.dep.yml
+++ /dev/null
@@ -1,32 +0,0 @@
----
-name: "@actions/http-client"
-version: 1.0.11
-type: npm
-summary: Actions Http Client
-homepage: https://github.com/actions/http-client#readme
-license: mit
-licenses:
-- sources: LICENSE
-  text: |
-    Actions Http Client for Node.js
-
-    Copyright (c) GitHub, Inc.
-
-    All rights reserved.
-
-    MIT License
-
-    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
-    associated documentation files (the "Software"), to deal in the Software without restriction,
-    including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
-    and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
-    subject to the following conditions:
-
-    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-    THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
-    LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
-    NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-    SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-notices: []
diff --git a/README.md b/README.md
index 6e178e2f..cb170045 100644
--- a/README.md
+++ b/README.md
@@ -259,6 +259,24 @@ pypy3.7-nightly or pypy-3.7-nightly # Python 3.7 and nightly PyPy
 
 Note: `pypy2` and `pypy3` have been removed in v3. Use the format above instead.
 
+# Check latest version
+
+The `check-latest` flag defaults to `false`. Use the default or set `check-latest` to `false` if you prefer stability and if you want to ensure a specific `Python/PyPy` version is always used.
+
+If `check-latest` is set to `true`, the action first checks if the cached version is the latest one. If the locally cached version is not the most up-to-date, a `Python/PyPy` version will then be downloaded. Set `check-latest` to `true` if you want the most up-to-date `Python/PyPy` version to always be used.
+
+> Setting `check-latest` to `true` has performance implications as downloading `Python/PyPy` versions is slower than using cached versions.
+
+```yaml
+steps:
+  - uses: actions/checkout@v3
+  - uses: actions/setup-python@v3
+    with:
+      python-version: '3.7'
+      check-latest: true
+  - run: python my_script.py
+```
+
 # Caching packages dependencies
 
 The action has built-in functionality for caching and restoring dependencies. It uses [actions/cache](https://github.com/actions/toolkit/tree/main/packages/cache) under the hood for caching dependencies but requires less configuration settings. Supported package managers are `pip`, `pipenv` and `poetry`. The `cache` input is optional, and caching is turned off by default.
diff --git a/__tests__/find-pypy.test.ts b/__tests__/find-pypy.test.ts
index 3da9a226..660f23d3 100644
--- a/__tests__/find-pypy.test.ts
+++ b/__tests__/find-pypy.test.ts
@@ -14,7 +14,6 @@ import * as finder from '../src/find-pypy';
 import {
   IPyPyManifestRelease,
   IS_WINDOWS,
-  validateVersion,
   getPyPyVersionFromPath
 } from '../src/utils';
 
@@ -82,6 +81,12 @@ describe('findPyPyToolCache', () => {
   const pypyPath = path.join('PyPy', actualPythonVersion, architecture);
   let tcFind: jest.SpyInstance;
   let spyReadExactPyPyVersion: jest.SpyInstance;
+  let infoSpy: jest.SpyInstance;
+  let warningSpy: jest.SpyInstance;
+  let debugSpy: jest.SpyInstance;
+  let addPathSpy: jest.SpyInstance;
+  let exportVariableSpy: jest.SpyInstance;
+  let setOutputSpy: jest.SpyInstance;
 
   beforeEach(() => {
     tcFind = jest.spyOn(tc, 'find');
@@ -94,6 +99,24 @@ describe('findPyPyToolCache', () => {
 
     spyReadExactPyPyVersion = jest.spyOn(utils, 'readExactPyPyVersionFile');
     spyReadExactPyPyVersion.mockImplementation(() => actualPyPyVersion);
+
+    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(() => {
@@ -136,6 +159,13 @@ describe('findPyPyToolCache', () => {
 });
 
 describe('findPyPyVersion', () => {
+  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;
@@ -154,6 +184,27 @@ describe('findPyPyVersion', () => {
   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');
@@ -222,7 +273,7 @@ describe('findPyPyVersion', () => {
 
   it('found PyPy in toolcache', async () => {
     await expect(
-      finder.findPyPyVersion('pypy-3.6-v7.3.x', architecture, true)
+      finder.findPyPyVersion('pypy-3.6-v7.3.x', architecture, true, false)
     ).resolves.toEqual({
       resolvedPythonVersion: '3.6.12',
       resolvedPyPyVersion: '7.3.3'
@@ -240,13 +291,13 @@ describe('findPyPyVersion', () => {
 
   it('throw on invalid input format', async () => {
     await expect(
-      finder.findPyPyVersion('pypy3.7-v7.3.x', architecture, true)
+      finder.findPyPyVersion('pypy3.7-v7.3.x', architecture, true, false)
     ).rejects.toThrow();
   });
 
   it('throw on invalid input format pypy3.7-7.3.x', async () => {
     await expect(
-      finder.findPyPyVersion('pypy3.7-v7.3.x', architecture, true)
+      finder.findPyPyVersion('pypy3.7-v7.3.x', architecture, true, false)
     ).rejects.toThrow();
   });
 
@@ -258,7 +309,7 @@ describe('findPyPyVersion', () => {
     spyChmodSync = jest.spyOn(fs, 'chmodSync');
     spyChmodSync.mockImplementation(() => undefined);
     await expect(
-      finder.findPyPyVersion('pypy-3.7-v7.3.x', architecture, true)
+      finder.findPyPyVersion('pypy-3.7-v7.3.x', architecture, true, false)
     ).resolves.toEqual({
       resolvedPythonVersion: '3.7.9',
       resolvedPyPyVersion: '7.3.3'
@@ -282,7 +333,7 @@ describe('findPyPyVersion', () => {
     spyChmodSync = jest.spyOn(fs, 'chmodSync');
     spyChmodSync.mockImplementation(() => undefined);
     await expect(
-      finder.findPyPyVersion('pypy-3.7-v7.3.x', architecture, false)
+      finder.findPyPyVersion('pypy-3.7-v7.3.x', architecture, false, false)
     ).resolves.toEqual({
       resolvedPythonVersion: '3.7.9',
       resolvedPyPyVersion: '7.3.3'
@@ -293,9 +344,61 @@ describe('findPyPyVersion', () => {
 
   it('throw if release is not found', async () => {
     await expect(
-      finder.findPyPyVersion('pypy-3.7-v7.5.x', architecture, true)
+      finder.findPyPyVersion('pypy-3.7-v7.5.x', architecture, true, false)
     ).rejects.toThrowError(
       `PyPy version 3.7 (v7.5.x) with arch ${architecture} not found`
     );
   });
+
+  it('check-latest enabled version found and used from toolcache', async () => {
+    await expect(
+      finder.findPyPyVersion('pypy-3.6-v7.3.x', architecture, false, true)
+    ).resolves.toEqual({
+      resolvedPythonVersion: '3.6.12',
+      resolvedPyPyVersion: '7.3.3'
+    });
+
+    expect(infoSpy).toHaveBeenCalledWith(
+      'Resolved as PyPy 7.3.3 with Python (3.6.12)'
+    );
+  });
+
+  it('check-latest enabled version found and install successfully', async () => {
+    spyCacheDir = jest.spyOn(tc, 'cacheDir');
+    spyCacheDir.mockImplementation(() =>
+      path.join(toolDir, 'PyPy', '3.7.7', architecture)
+    );
+    spyChmodSync = jest.spyOn(fs, 'chmodSync');
+    spyChmodSync.mockImplementation(() => undefined);
+    await expect(
+      finder.findPyPyVersion('pypy-3.7-v7.3.x', architecture, false, true)
+    ).resolves.toEqual({
+      resolvedPythonVersion: '3.7.9',
+      resolvedPyPyVersion: '7.3.3'
+    });
+    expect(infoSpy).toHaveBeenCalledWith(
+      'Resolved as PyPy 7.3.3 with Python (3.7.9)'
+    );
+  });
+
+  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 pypyPath = '';
+      if (semver.satisfies('3.8.8', semverRange)) {
+        pypyPath = path.join(toolDir, 'PyPy', '3.8.8', architecture);
+      }
+      return pypyPath;
+    });
+    await expect(
+      finder.findPyPyVersion('pypy-3.8-v7.3.x', architecture, false, true)
+    ).resolves.toEqual({
+      resolvedPythonVersion: '3.8.8',
+      resolvedPyPyVersion: '7.3.3'
+    });
+
+    expect(infoSpy).toHaveBeenCalledWith(
+      'Failed to resolve PyPy v7.3.x with Python (3.8) from manifest'
+    );
+  });
 });
diff --git a/__tests__/finder.test.ts b/__tests__/finder.test.ts
index 3bd279f1..d2fe775b 100644
--- a/__tests__/finder.test.ts
+++ b/__tests__/finder.test.ts
@@ -1,6 +1,7 @@
-import io = require('@actions/io');
-import fs = require('fs');
-import path = require('path');
+import * as io from '@actions/io';
+import os from 'os';
+import fs from 'fs';
+import path from 'path';
 
 const toolDir = path.join(
   __dirname,
@@ -26,11 +27,14 @@ import * as installer from '../src/install-python';
 const manifestData = require('./data/versions-manifest.json');
 
 describe('Finder tests', () => {
+  let writeSpy: jest.SpyInstance;
   let spyCoreAddPath: jest.SpyInstance;
   let spyCoreExportVariable: jest.SpyInstance;
   const env = process.env;
 
   beforeEach(() => {
+    writeSpy = jest.spyOn(process.stdout, 'write');
+    writeSpy.mockImplementation(() => {});
     jest.resetModules();
     process.env = {...env};
     spyCoreAddPath = jest.spyOn(core, 'addPath');
@@ -45,11 +49,14 @@ describe('Finder tests', () => {
   });
 
   it('Finds Python if it is installed', async () => {
+    const getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
+    getBooleanInputSpy.mockImplementation(input => false);
+
     const pythonDir: string = path.join(toolDir, 'Python', '3.0.0', 'x64');
     await io.mkdirP(pythonDir);
     fs.writeFileSync(`${pythonDir}.complete`, 'hello');
     // This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
-    await finder.useCpythonVersion('3.x', 'x64', true);
+    await finder.useCpythonVersion('3.x', 'x64', true, false);
     expect(spyCoreAddPath).toHaveBeenCalled();
     expect(spyCoreExportVariable).toHaveBeenCalledWith(
       'pythonLocation',
@@ -66,7 +73,7 @@ describe('Finder tests', () => {
     await io.mkdirP(pythonDir);
     fs.writeFileSync(`${pythonDir}.complete`, 'hello');
     // This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
-    await finder.useCpythonVersion('3.x', 'x64', false);
+    await finder.useCpythonVersion('3.x', 'x64', false, false);
     expect(spyCoreAddPath).not.toHaveBeenCalled();
     expect(spyCoreExportVariable).not.toHaveBeenCalled();
   });
@@ -75,6 +82,9 @@ describe('Finder tests', () => {
     const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo');
     findSpy.mockImplementation(() => <tc.IToolRelease[]>manifestData);
 
+    const getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
+    getBooleanInputSpy.mockImplementation(input => false);
+
     const installSpy: jest.SpyInstance = jest.spyOn(
       installer,
       'installCpythonFromRelease'
@@ -85,7 +95,7 @@ describe('Finder tests', () => {
       fs.writeFileSync(`${pythonDir}.complete`, 'hello');
     });
     // This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
-    await finder.useCpythonVersion('1.2.3', 'x64', true);
+    await finder.useCpythonVersion('1.2.3', 'x64', true, false);
     expect(spyCoreAddPath).toHaveBeenCalled();
     expect(spyCoreExportVariable).toHaveBeenCalledWith(
       'pythonLocation',
@@ -101,6 +111,9 @@ describe('Finder tests', () => {
     const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo');
     findSpy.mockImplementation(() => <tc.IToolRelease[]>manifestData);
 
+    const getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
+    getBooleanInputSpy.mockImplementation(input => false);
+
     const installSpy: jest.SpyInstance = jest.spyOn(
       installer,
       'installCpythonFromRelease'
@@ -116,7 +129,65 @@ describe('Finder tests', () => {
       fs.writeFileSync(`${pythonDir}.complete`, 'hello');
     });
     // This will throw if it doesn't find it in the manifest (because no such version exists)
-    await finder.useCpythonVersion('1.2.3-beta.2', 'x64', true);
+    await finder.useCpythonVersion('1.2.3-beta.2', 'x64', false, false);
+  });
+
+  it('Check-latest true, finds the latest version in the manifest', async () => {
+    const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo');
+    findSpy.mockImplementation(() => <tc.IToolRelease[]>manifestData);
+
+    const getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
+    getBooleanInputSpy.mockImplementation(input => true);
+
+    const cnSpy: jest.SpyInstance = jest.spyOn(process.stdout, 'write');
+    cnSpy.mockImplementation(line => {
+      // uncomment to debug
+      // process.stderr.write('write:' + line + '\n');
+    });
+
+    const addPathSpy: jest.SpyInstance = jest.spyOn(core, 'addPath');
+    addPathSpy.mockImplementation(() => null);
+
+    const infoSpy: jest.SpyInstance = jest.spyOn(core, 'info');
+    infoSpy.mockImplementation(() => {});
+
+    const debugSpy: jest.SpyInstance = jest.spyOn(core, 'debug');
+    debugSpy.mockImplementation(() => {});
+
+    const pythonDir: string = path.join(toolDir, 'Python', '1.2.2', 'x64');
+    const expPath: string = path.join(toolDir, 'Python', '1.2.3', 'x64');
+
+    const installSpy: jest.SpyInstance = jest.spyOn(
+      installer,
+      'installCpythonFromRelease'
+    );
+    installSpy.mockImplementation(async () => {
+      await io.mkdirP(expPath);
+      fs.writeFileSync(`${expPath}.complete`, 'hello');
+    });
+
+    const tcFindSpy: jest.SpyInstance = jest.spyOn(tc, 'find');
+    tcFindSpy
+      .mockImplementationOnce(() => '')
+      .mockImplementationOnce(() => expPath);
+
+    await io.mkdirP(pythonDir);
+    await io.rmRF(path.join(toolDir, 'Python', '1.2.3'));
+
+    fs.writeFileSync(`${pythonDir}.complete`, 'hello');
+    // This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
+    await finder.useCpythonVersion('1.2', 'x64', true, true);
+
+    expect(infoSpy).toHaveBeenCalledWith("Resolved as '1.2.3'");
+    expect(infoSpy).toHaveBeenCalledWith(
+      'Version 1.2.3 was not found in the local cache'
+    );
+    expect(infoSpy).toBeCalledWith(
+      'Version 1.2.3 is available for downloading'
+    );
+    expect(installSpy).toHaveBeenCalled();
+    expect(addPathSpy).toHaveBeenCalledWith(expPath);
+    await finder.useCpythonVersion('1.2.3-beta.2', 'x64', false, true);
     expect(spyCoreAddPath).toHaveBeenCalled();
     expect(spyCoreExportVariable).toHaveBeenCalledWith(
       'pythonLocation',
@@ -132,7 +203,7 @@ describe('Finder tests', () => {
     // This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
     let thrown = false;
     try {
-      await finder.useCpythonVersion('3.300000', 'x64', true);
+      await finder.useCpythonVersion('3.300000', 'x64', true, false);
     } catch {
       thrown = true;
     }
diff --git a/__tests__/install-pypy.test.ts b/__tests__/install-pypy.test.ts
index cffc90e8..ae7fb4a6 100644
--- a/__tests__/install-pypy.test.ts
+++ b/__tests__/install-pypy.test.ts
@@ -4,6 +4,7 @@ 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-pypy';
@@ -51,6 +52,22 @@ describe('findRelease', () => {
     download_url: `https://test.download.python.org/pypy/pypy3.6-v7.3.3-${extensionName}`
   };
 
+  let getBooleanInputSpy: jest.SpyInstance;
+  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("Python version is found, but PyPy version doesn't match", () => {
     const pythonVersion = '3.6';
     const pypyVersion = '7.3.7';
@@ -133,6 +150,10 @@ describe('findRelease', () => {
 
 describe('installPyPy', () => {
   let tcFind: jest.SpyInstance;
+  let getBooleanInputSpy: jest.SpyInstance;
+  let warningSpy: jest.SpyInstance;
+  let debugSpy: jest.SpyInstance;
+  let infoSpy: jest.SpyInstance;
   let spyExtractZip: jest.SpyInstance;
   let spyExtractTar: jest.SpyInstance;
   let spyFsReadDir: jest.SpyInstance;
@@ -158,6 +179,15 @@ describe('installPyPy', () => {
     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(() => ['PyPyTest']);
 
@@ -194,7 +224,7 @@ describe('installPyPy', () => {
 
   it('throw if release is not found', async () => {
     await expect(
-      installer.installPyPy('7.3.3', '3.6.17', architecture)
+      installer.installPyPy('7.3.3', '3.6.17', architecture, undefined)
     ).rejects.toThrowError(
       `PyPy version 3.6.17 (7.3.3) with arch ${architecture} not found`
     );
@@ -214,7 +244,7 @@ describe('installPyPy', () => {
     spyChmodSync.mockImplementation(() => undefined);
 
     await expect(
-      installer.installPyPy('7.3.x', '3.6.12', architecture)
+      installer.installPyPy('7.3.x', '3.6.12', architecture, undefined)
     ).resolves.toEqual({
       installDir: path.join(toolDir, 'PyPy', '3.6.12', architecture),
       resolvedPythonVersion: '3.6.12',
diff --git a/action.yml b/action.yml
index de2a4feb..f4aeb35b 100644
--- a/action.yml
+++ b/action.yml
@@ -12,6 +12,9 @@ inputs:
     required: false
   architecture:
     description: 'The target architecture (x86, x64) of the Python interpreter.'
+  check-latest:
+    description: 'Set this option if you want the action to check for the latest available version that satisfies the version spec.'
+    default: false
   token:
     description: Used to pull python distributions from actions/python-versions. Since there's a default, this is typically not supplied by the user.
     default: ${{ github.token }}
diff --git a/dist/setup/index.js b/dist/setup/index.js
index 3d373467..f9cb76f1 100644
--- a/dist/setup/index.js
+++ b/dist/setup/index.js
@@ -64685,19 +64685,34 @@ const utils_1 = __nccwpck_require__(1314);
 const semver = __importStar(__nccwpck_require__(1383));
 const core = __importStar(__nccwpck_require__(2186));
 const tc = __importStar(__nccwpck_require__(7784));
-function findPyPyVersion(versionSpec, architecture, updateEnvironment) {
+function findPyPyVersion(versionSpec, architecture, updateEnvironment, checkLatest) {
     return __awaiter(this, void 0, void 0, function* () {
         let resolvedPyPyVersion = '';
         let resolvedPythonVersion = '';
         let installDir;
+        let releases;
         const pypyVersionSpec = parsePyPyVersion(versionSpec);
+        if (checkLatest) {
+            releases = yield pypyInstall.getAvailablePyPyVersions();
+            if (releases && releases.length > 0) {
+                const releaseData = pypyInstall.findRelease(releases, pypyVersionSpec.pythonVersion, pypyVersionSpec.pypyVersion, architecture);
+                if (releaseData) {
+                    core.info(`Resolved as PyPy ${releaseData.resolvedPyPyVersion} with Python (${releaseData.resolvedPythonVersion})`);
+                    pypyVersionSpec.pythonVersion = releaseData.resolvedPythonVersion;
+                    pypyVersionSpec.pypyVersion = releaseData.resolvedPyPyVersion;
+                }
+                else {
+                    core.info(`Failed to resolve PyPy ${pypyVersionSpec.pypyVersion} with Python (${pypyVersionSpec.pythonVersion}) from manifest`);
+                }
+            }
+        }
         ({ installDir, resolvedPythonVersion, resolvedPyPyVersion } = findPyPyToolCache(pypyVersionSpec.pythonVersion, pypyVersionSpec.pypyVersion, architecture));
         if (!installDir) {
             ({
                 installDir,
                 resolvedPythonVersion,
                 resolvedPyPyVersion
-            } = yield pypyInstall.installPyPy(pypyVersionSpec.pypyVersion, pypyVersionSpec.pythonVersion, architecture));
+            } = yield pypyInstall.installPyPy(pypyVersionSpec.pypyVersion, pypyVersionSpec.pythonVersion, architecture, releases));
         }
         const pipDir = utils_1.IS_WINDOWS ? 'Scripts' : 'bin';
         const _binDir = path.join(installDir, pipDir);
@@ -64847,15 +64862,28 @@ function binDir(installDir) {
         return path.join(installDir, 'bin');
     }
 }
-function useCpythonVersion(version, architecture, updateEnvironment) {
+function useCpythonVersion(version, architecture, updateEnvironment, checkLatest) {
+    var _a;
     return __awaiter(this, void 0, void 0, function* () {
+        let manifest = null;
         const desugaredVersionSpec = desugarDevVersion(version);
-        const semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec);
+        let semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec);
         core.debug(`Semantic version spec of ${version} is ${semanticVersionSpec}`);
+        if (checkLatest) {
+            manifest = yield installer.getManifest();
+            const resolvedVersion = (_a = (yield installer.findReleaseFromManifest(semanticVersionSpec, architecture, manifest))) === null || _a === void 0 ? void 0 : _a.version;
+            if (resolvedVersion) {
+                semanticVersionSpec = resolvedVersion;
+                core.info(`Resolved as '${semanticVersionSpec}'`);
+            }
+            else {
+                core.info(`Failed to resolve version ${semanticVersionSpec} from manifest`);
+            }
+        }
         let installDir = tc.find('Python', semanticVersionSpec, architecture);
         if (!installDir) {
             core.info(`Version ${semanticVersionSpec} was not found in the local cache`);
-            const foundRelease = yield installer.findReleaseFromManifest(semanticVersionSpec, architecture);
+            const foundRelease = yield installer.findReleaseFromManifest(semanticVersionSpec, architecture, manifest);
             if (foundRelease && foundRelease.files && foundRelease.files.length > 0) {
                 core.info(`Version ${semanticVersionSpec} is available for downloading`);
                 yield installer.installCpythonFromRelease(foundRelease);
@@ -64974,7 +65002,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
     return (mod && mod.__esModule) ? mod : { "default": mod };
 };
 Object.defineProperty(exports, "__esModule", ({ value: true }));
-exports.findAssetForMacOrLinux = exports.findAssetForWindows = exports.isArchPresentForMacOrLinux = exports.isArchPresentForWindows = exports.pypyVersionToSemantic = exports.getPyPyBinaryPath = exports.findRelease = exports.installPyPy = void 0;
+exports.findAssetForMacOrLinux = exports.findAssetForWindows = exports.isArchPresentForMacOrLinux = exports.isArchPresentForWindows = exports.pypyVersionToSemantic = exports.getPyPyBinaryPath = exports.findRelease = exports.getAvailablePyPyVersions = exports.installPyPy = void 0;
 const path = __importStar(__nccwpck_require__(1017));
 const core = __importStar(__nccwpck_require__(2186));
 const tc = __importStar(__nccwpck_require__(7784));
@@ -64983,10 +65011,10 @@ const httpm = __importStar(__nccwpck_require__(9925));
 const exec = __importStar(__nccwpck_require__(1514));
 const fs_1 = __importDefault(__nccwpck_require__(7147));
 const utils_1 = __nccwpck_require__(1314);
-function installPyPy(pypyVersion, pythonVersion, architecture) {
+function installPyPy(pypyVersion, pythonVersion, architecture, releases) {
     return __awaiter(this, void 0, void 0, function* () {
         let downloadDir;
-        const releases = yield getAvailablePyPyVersions();
+        releases = releases !== null && releases !== void 0 ? releases : (yield getAvailablePyPyVersions());
         if (!releases || releases.length === 0) {
             throw new Error('No release was found in PyPy version.json');
         }
@@ -65032,6 +65060,7 @@ function getAvailablePyPyVersions() {
         return response.result;
     });
 }
+exports.getAvailablePyPyVersions = getAvailablePyPyVersions;
 function createPyPySymlink(pypyBinaryPath, pythonVersion) {
     return __awaiter(this, void 0, void 0, function* () {
         const version = semver.coerce(pythonVersion);
@@ -65154,7 +65183,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
     });
 };
 Object.defineProperty(exports, "__esModule", ({ value: true }));
-exports.installCpythonFromRelease = exports.findReleaseFromManifest = exports.MANIFEST_URL = void 0;
+exports.installCpythonFromRelease = exports.getManifest = exports.findReleaseFromManifest = exports.MANIFEST_URL = void 0;
 const path = __importStar(__nccwpck_require__(1017));
 const core = __importStar(__nccwpck_require__(2186));
 const tc = __importStar(__nccwpck_require__(7784));
@@ -65166,13 +65195,21 @@ const MANIFEST_REPO_OWNER = 'actions';
 const MANIFEST_REPO_NAME = 'python-versions';
 const MANIFEST_REPO_BRANCH = 'main';
 exports.MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`;
-function findReleaseFromManifest(semanticVersionSpec, architecture) {
+function findReleaseFromManifest(semanticVersionSpec, architecture, manifest) {
     return __awaiter(this, void 0, void 0, function* () {
-        const manifest = yield tc.getManifestFromRepo(MANIFEST_REPO_OWNER, MANIFEST_REPO_NAME, AUTH, MANIFEST_REPO_BRANCH);
-        return yield tc.findFromManifest(semanticVersionSpec, false, manifest, architecture);
+        if (!manifest) {
+            manifest = yield getManifest();
+        }
+        const foundRelease = yield tc.findFromManifest(semanticVersionSpec, false, manifest, architecture);
+        return foundRelease;
     });
 }
 exports.findReleaseFromManifest = findReleaseFromManifest;
+function getManifest() {
+    core.debug(`Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`);
+    return tc.getManifestFromRepo(MANIFEST_REPO_OWNER, MANIFEST_REPO_NAME, AUTH, MANIFEST_REPO_BRANCH);
+}
+exports.getManifest = getManifest;
 function installPython(workingDirectory) {
     return __awaiter(this, void 0, void 0, function* () {
         const options = {
@@ -65315,17 +65352,18 @@ function run() {
         core.debug(`Python is expected to be installed into RUNNER_TOOL_CACHE=${process.env['RUNNER_TOOL_CACHE']}`);
         try {
             const version = resolveVersionInput();
+            const checkLatest = core.getBooleanInput('check-latest');
             if (version) {
                 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);
+                    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);
+                    const installed = yield finder.useCpythonVersion(version, arch, updateEnvironment, checkLatest);
                     pythonVersion = installed.version;
                     core.info(`Successfully set up ${installed.impl} (${pythonVersion})`);
                 }
diff --git a/src/find-pypy.ts b/src/find-pypy.ts
index 3cc3fdd2..20b9821e 100644
--- a/src/find-pypy.ts
+++ b/src/find-pypy.ts
@@ -6,7 +6,8 @@ import {
   validateVersion,
   getPyPyVersionFromPath,
   readExactPyPyVersionFile,
-  validatePythonVersionFormatForPyPy
+  validatePythonVersionFormatForPyPy,
+  IPyPyManifestRelease
 } from './utils';
 
 import * as semver from 'semver';
@@ -21,14 +22,40 @@ interface IPyPyVersionSpec {
 export async function findPyPyVersion(
   versionSpec: string,
   architecture: string,
-  updateEnvironment: boolean
+  updateEnvironment: boolean,
+  checkLatest: boolean
 ): Promise<{resolvedPyPyVersion: string; resolvedPythonVersion: string}> {
   let resolvedPyPyVersion = '';
   let resolvedPythonVersion = '';
   let installDir: string | null;
+  let releases: IPyPyManifestRelease[] | undefined;
 
   const pypyVersionSpec = parsePyPyVersion(versionSpec);
 
+  if (checkLatest) {
+    releases = await pypyInstall.getAvailablePyPyVersions();
+    if (releases && releases.length > 0) {
+      const releaseData = pypyInstall.findRelease(
+        releases,
+        pypyVersionSpec.pythonVersion,
+        pypyVersionSpec.pypyVersion,
+        architecture
+      );
+
+      if (releaseData) {
+        core.info(
+          `Resolved as PyPy ${releaseData.resolvedPyPyVersion} with Python (${releaseData.resolvedPythonVersion})`
+        );
+        pypyVersionSpec.pythonVersion = releaseData.resolvedPythonVersion;
+        pypyVersionSpec.pypyVersion = releaseData.resolvedPyPyVersion;
+      } else {
+        core.info(
+          `Failed to resolve PyPy ${pypyVersionSpec.pypyVersion} with Python (${pypyVersionSpec.pythonVersion}) from manifest`
+        );
+      }
+    }
+  }
+
   ({installDir, resolvedPythonVersion, resolvedPyPyVersion} = findPyPyToolCache(
     pypyVersionSpec.pythonVersion,
     pypyVersionSpec.pypyVersion,
@@ -43,7 +70,8 @@ export async function findPyPyVersion(
     } = await pypyInstall.installPyPy(
       pypyVersionSpec.pypyVersion,
       pypyVersionSpec.pythonVersion,
-      architecture
+      architecture,
+      releases
     ));
   }
 
diff --git a/src/find-python.ts b/src/find-python.ts
index e1add953..4e54a94b 100644
--- a/src/find-python.ts
+++ b/src/find-python.ts
@@ -33,12 +33,34 @@ function binDir(installDir: string): string {
 export async function useCpythonVersion(
   version: string,
   architecture: string,
-  updateEnvironment: boolean
+  updateEnvironment: boolean,
+  checkLatest: boolean
 ): Promise<InstalledVersion> {
+  let manifest: tc.IToolRelease[] | null = null;
   const desugaredVersionSpec = desugarDevVersion(version);
-  const semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec);
+  let semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec);
   core.debug(`Semantic version spec of ${version} is ${semanticVersionSpec}`);
 
+  if (checkLatest) {
+    manifest = await installer.getManifest();
+    const resolvedVersion = (
+      await installer.findReleaseFromManifest(
+        semanticVersionSpec,
+        architecture,
+        manifest
+      )
+    )?.version;
+
+    if (resolvedVersion) {
+      semanticVersionSpec = resolvedVersion;
+      core.info(`Resolved as '${semanticVersionSpec}'`);
+    } else {
+      core.info(
+        `Failed to resolve version ${semanticVersionSpec} from manifest`
+      );
+    }
+  }
+
   let installDir: string | null = tc.find(
     'Python',
     semanticVersionSpec,
@@ -50,7 +72,8 @@ export async function useCpythonVersion(
     );
     const foundRelease = await installer.findReleaseFromManifest(
       semanticVersionSpec,
-      architecture
+      architecture,
+      manifest
     );
 
     if (foundRelease && foundRelease.files && foundRelease.files.length > 0) {
diff --git a/src/install-pypy.ts b/src/install-pypy.ts
index c3718b79..4c49e115 100644
--- a/src/install-pypy.ts
+++ b/src/install-pypy.ts
@@ -19,11 +19,13 @@ import {
 export async function installPyPy(
   pypyVersion: string,
   pythonVersion: string,
-  architecture: string
+  architecture: string,
+  releases: IPyPyManifestRelease[] | undefined
 ) {
   let downloadDir;
 
-  const releases = await getAvailablePyPyVersions();
+  releases = releases ?? (await getAvailablePyPyVersions());
+
   if (!releases || releases.length === 0) {
     throw new Error('No release was found in PyPy version.json');
   }
@@ -78,7 +80,7 @@ export async function installPyPy(
   return {installDir, resolvedPythonVersion, resolvedPyPyVersion};
 }
 
-async function getAvailablePyPyVersions() {
+export async function getAvailablePyPyVersions() {
   const url = 'https://downloads.python.org/pypy/versions.json';
   const http: httpm.HttpClient = new httpm.HttpClient('tool-cache');
 
diff --git a/src/install-python.ts b/src/install-python.ts
index 397da0cb..6e5c8518 100644
--- a/src/install-python.ts
+++ b/src/install-python.ts
@@ -14,20 +14,33 @@ export const MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_O
 
 export async function findReleaseFromManifest(
   semanticVersionSpec: string,
-  architecture: string
+  architecture: string,
+  manifest: tc.IToolRelease[] | null
 ): Promise<tc.IToolRelease | undefined> {
-  const manifest: tc.IToolRelease[] = await tc.getManifestFromRepo(
-    MANIFEST_REPO_OWNER,
-    MANIFEST_REPO_NAME,
-    AUTH,
-    MANIFEST_REPO_BRANCH
-  );
-  return await tc.findFromManifest(
+  if (!manifest) {
+    manifest = await getManifest();
+  }
+
+  const foundRelease = await tc.findFromManifest(
     semanticVersionSpec,
     false,
     manifest,
     architecture
   );
+
+  return foundRelease;
+}
+
+export function getManifest(): Promise<tc.IToolRelease[]> {
+  core.debug(
+    `Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`
+  );
+  return tc.getManifestFromRepo(
+    MANIFEST_REPO_OWNER,
+    MANIFEST_REPO_NAME,
+    AUTH,
+    MANIFEST_REPO_BRANCH
+  );
 }
 
 async function installPython(workingDirectory: string) {
diff --git a/src/setup-python.ts b/src/setup-python.ts
index 1ebcbac3..11ea4056 100644
--- a/src/setup-python.ts
+++ b/src/setup-python.ts
@@ -80,6 +80,8 @@ async function run() {
   );
   try {
     const version = resolveVersionInput();
+    const checkLatest = core.getBooleanInput('check-latest');
+
     if (version) {
       let pythonVersion: string;
       const arch: string = core.getInput('architecture') || os.arch();
@@ -88,7 +90,8 @@ async function run() {
         const installed = await finderPyPy.findPyPyVersion(
           version,
           arch,
-          updateEnvironment
+          updateEnvironment,
+          checkLatest
         );
         pythonVersion = `${installed.resolvedPyPyVersion}-${installed.resolvedPythonVersion}`;
         core.info(
@@ -98,7 +101,8 @@ async function run() {
         const installed = await finder.useCpythonVersion(
           version,
           arch,
-          updateEnvironment
+          updateEnvironment,
+          checkLatest
         );
         pythonVersion = installed.version;
         core.info(`Successfully set up ${installed.impl} (${pythonVersion})`);