Skip to content

Commit 69d9d7d

Browse files
Merge branch 'main' into v-dmshib/install-multiple-python-versions
2 parents fa589f7 + 1aafadc commit 69d9d7d

File tree

7 files changed

+212
-95
lines changed

7 files changed

+212
-95
lines changed

.github/workflows/release-new-action-version.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
runs-on: ubuntu-latest
2222
steps:
2323
- name: Update the ${{ env.TAG_NAME }} tag
24-
uses: actions/[email protected].0
24+
uses: actions/[email protected].1
2525
with:
2626
source-tag: ${{ env.TAG_NAME }}
2727
slack-webhook: ${{ secrets.SLACK_WEBHOOK }}

.github/workflows/workflow.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ${{ matrix.operating-system }}
1515
strategy:
1616
matrix:
17-
operating-system: [ubuntu-latest, windows-latest]
17+
operating-system: [ubuntu-20.04, windows-latest]
1818
steps:
1919
- name: Checkout
2020
uses: actions/checkout@v3
@@ -68,7 +68,7 @@ jobs:
6868
python-version: 3.8
6969
- name: Verify 3.8
7070
run: python __tests__/verify-python.py 3.8
71-
71+
7272
- name: Run with setup-python 3.7.5
7373
uses: ./
7474
with:

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ steps:
3333
python-version: 'pypy3.9'
3434
- run: python my_script.py
3535
```
36-
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 using `setup-python`.
36+
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.
3737

3838
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/).
3939

dist/setup/index.js

+80-37
Original file line numberDiff line numberDiff line change
@@ -66511,27 +66511,45 @@ function installPyPy(pypyVersion, pythonVersion, architecture, releases) {
6651166511
const { foundAsset, resolvedPythonVersion, resolvedPyPyVersion } = releaseData;
6651266512
let downloadUrl = `${foundAsset.download_url}`;
6651366513
core.info(`Downloading PyPy from "${downloadUrl}" ...`);
66514-
const pypyPath = yield tc.downloadTool(downloadUrl);
66515-
core.info('Extracting downloaded archive...');
66516-
if (utils_1.IS_WINDOWS) {
66517-
downloadDir = yield tc.extractZip(pypyPath);
66514+
try {
66515+
const pypyPath = yield tc.downloadTool(downloadUrl);
66516+
core.info('Extracting downloaded archive...');
66517+
if (utils_1.IS_WINDOWS) {
66518+
downloadDir = yield tc.extractZip(pypyPath);
66519+
}
66520+
else {
66521+
downloadDir = yield tc.extractTar(pypyPath, undefined, 'x');
66522+
}
66523+
// root folder in archive can have unpredictable name so just take the first folder
66524+
// downloadDir is unique folder under TEMP and can't contain any other folders
66525+
const archiveName = fs_1.default.readdirSync(downloadDir)[0];
66526+
const toolDir = path.join(downloadDir, archiveName);
66527+
let installDir = toolDir;
66528+
if (!utils_1.isNightlyKeyword(resolvedPyPyVersion)) {
66529+
installDir = yield tc.cacheDir(toolDir, 'PyPy', resolvedPythonVersion, architecture);
66530+
}
66531+
utils_1.writeExactPyPyVersionFile(installDir, resolvedPyPyVersion);
66532+
const binaryPath = getPyPyBinaryPath(installDir);
66533+
yield createPyPySymlink(binaryPath, resolvedPythonVersion);
66534+
yield installPip(binaryPath);
66535+
return { installDir, resolvedPythonVersion, resolvedPyPyVersion };
66536+
}
66537+
catch (err) {
66538+
if (err instanceof Error) {
66539+
// Rate limit?
66540+
if (err instanceof tc.HTTPError &&
66541+
(err.httpStatusCode === 403 || err.httpStatusCode === 429)) {
66542+
core.info(`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`);
66543+
}
66544+
else {
66545+
core.info(err.message);
66546+
}
66547+
if (err.stack !== undefined) {
66548+
core.debug(err.stack);
66549+
}
66550+
}
66551+
throw err;
6651866552
}
66519-
else {
66520-
downloadDir = yield tc.extractTar(pypyPath, undefined, 'x');
66521-
}
66522-
// root folder in archive can have unpredictable name so just take the first folder
66523-
// downloadDir is unique folder under TEMP and can't contain any other folders
66524-
const archiveName = fs_1.default.readdirSync(downloadDir)[0];
66525-
const toolDir = path.join(downloadDir, archiveName);
66526-
let installDir = toolDir;
66527-
if (!utils_1.isNightlyKeyword(resolvedPyPyVersion)) {
66528-
installDir = yield tc.cacheDir(toolDir, 'PyPy', resolvedPythonVersion, architecture);
66529-
}
66530-
utils_1.writeExactPyPyVersionFile(installDir, resolvedPyPyVersion);
66531-
const binaryPath = getPyPyBinaryPath(installDir);
66532-
yield createPyPySymlink(binaryPath, resolvedPythonVersion);
66533-
yield installPip(binaryPath);
66534-
return { installDir, resolvedPythonVersion, resolvedPyPyVersion };
6653566553
});
6653666554
}
6653766555
exports.installPyPy = installPyPy;
@@ -66577,7 +66595,7 @@ function findRelease(releases, pythonVersion, pypyVersion, architecture) {
6657766595
semver.satisfies(pypyVersionToSemantic(item.pypy_version), pypyVersion);
6657866596
const isArchPresent = item.files &&
6657966597
(utils_1.IS_WINDOWS
66580-
? isArchPresentForWindows(item)
66598+
? isArchPresentForWindows(item, architecture)
6658166599
: isArchPresentForMacOrLinux(item, architecture, process.platform));
6658266600
return isPythonVersionSatisfied && isPyPyVersionSatisfied && isArchPresent;
6658366601
});
@@ -66590,7 +66608,7 @@ function findRelease(releases, pythonVersion, pypyVersion, architecture) {
6659066608
});
6659166609
const foundRelease = sortedReleases[0];
6659266610
const foundAsset = utils_1.IS_WINDOWS
66593-
? findAssetForWindows(foundRelease)
66611+
? findAssetForWindows(foundRelease, architecture)
6659466612
: findAssetForMacOrLinux(foundRelease, architecture, process.platform);
6659566613
return {
6659666614
foundAsset,
@@ -66613,24 +66631,31 @@ function pypyVersionToSemantic(versionSpec) {
6661366631
return versionSpec.replace(prereleaseVersion, '$1-$2.$3');
6661466632
}
6661566633
exports.pypyVersionToSemantic = pypyVersionToSemantic;
66616-
function isArchPresentForWindows(item) {
66617-
return item.files.some((file) => utils_1.WINDOWS_ARCHS.includes(file.arch) &&
66618-
utils_1.WINDOWS_PLATFORMS.includes(file.platform));
66634+
function isArchPresentForWindows(item, architecture) {
66635+
architecture = replaceX32toX86(architecture);
66636+
return item.files.some((file) => utils_1.WINDOWS_PLATFORMS.includes(file.platform) && file.arch === architecture);
6661966637
}
6662066638
exports.isArchPresentForWindows = isArchPresentForWindows;
6662166639
function isArchPresentForMacOrLinux(item, architecture, platform) {
6662266640
return item.files.some((file) => file.arch === architecture && file.platform === platform);
6662366641
}
6662466642
exports.isArchPresentForMacOrLinux = isArchPresentForMacOrLinux;
66625-
function findAssetForWindows(releases) {
66626-
return releases.files.find((item) => utils_1.WINDOWS_ARCHS.includes(item.arch) &&
66627-
utils_1.WINDOWS_PLATFORMS.includes(item.platform));
66643+
function findAssetForWindows(releases, architecture) {
66644+
architecture = replaceX32toX86(architecture);
66645+
return releases.files.find((item) => utils_1.WINDOWS_PLATFORMS.includes(item.platform) && item.arch === architecture);
6662866646
}
6662966647
exports.findAssetForWindows = findAssetForWindows;
6663066648
function findAssetForMacOrLinux(releases, architecture, platform) {
6663166649
return releases.files.find((item) => item.arch === architecture && item.platform === platform);
6663266650
}
6663366651
exports.findAssetForMacOrLinux = findAssetForMacOrLinux;
66652+
function replaceX32toX86(architecture) {
66653+
// convert x32 to x86 because os.arch() returns x32 for 32-bit systems but PyPy releases json has x86 arch value.
66654+
if (architecture === 'x32') {
66655+
architecture = 'x86';
66656+
}
66657+
return architecture;
66658+
}
6663466659

6663566660

6663666661
/***/ }),
@@ -66723,17 +66748,35 @@ function installCpythonFromRelease(release) {
6672366748
return __awaiter(this, void 0, void 0, function* () {
6672466749
const downloadUrl = release.files[0].download_url;
6672566750
core.info(`Download from "${downloadUrl}"`);
66726-
const pythonPath = yield tc.downloadTool(downloadUrl, undefined, AUTH);
66727-
core.info('Extract downloaded archive');
66728-
let pythonExtractedFolder;
66729-
if (utils_1.IS_WINDOWS) {
66730-
pythonExtractedFolder = yield tc.extractZip(pythonPath);
66751+
let pythonPath = '';
66752+
try {
66753+
pythonPath = yield tc.downloadTool(downloadUrl, undefined, AUTH);
66754+
core.info('Extract downloaded archive');
66755+
let pythonExtractedFolder;
66756+
if (utils_1.IS_WINDOWS) {
66757+
pythonExtractedFolder = yield tc.extractZip(pythonPath);
66758+
}
66759+
else {
66760+
pythonExtractedFolder = yield tc.extractTar(pythonPath);
66761+
}
66762+
core.info('Execute installation script');
66763+
yield installPython(pythonExtractedFolder);
6673166764
}
66732-
else {
66733-
pythonExtractedFolder = yield tc.extractTar(pythonPath);
66765+
catch (err) {
66766+
if (err instanceof tc.HTTPError) {
66767+
// Rate limit?
66768+
if (err.httpStatusCode === 403 || err.httpStatusCode === 429) {
66769+
core.info(`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`);
66770+
}
66771+
else {
66772+
core.info(err.message);
66773+
}
66774+
if (err.stack) {
66775+
core.debug(err.stack);
66776+
}
66777+
}
66778+
throw err;
6673466779
}
66735-
core.info('Execute installation script');
66736-
yield installPython(pythonExtractedFolder);
6673766780
});
6673866781
}
6673966782
exports.installCpythonFromRelease = installCpythonFromRelease;

docs/advanced-usage.md

+34-6
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,20 @@ steps:
281281
- run: pip install -e . -r subdirectory/requirements-dev.txt
282282
```
283283

284+
**Caching projects that use setup.py:**
285+
286+
```yaml
287+
steps:
288+
- uses: actions/checkout@v3
289+
- uses: actions/setup-python@v4
290+
with:
291+
python-version: '3.11'
292+
cache: 'pip'
293+
cache-dependency-path: setup.py
294+
- run: pip install -e .
295+
# Or pip install -e '.[test]' to install test dependencies
296+
```
297+
284298
# Outputs and environment variables
285299

286300
## Outputs
@@ -471,15 +485,29 @@ One quick way to grant access is to change the user and group of `/Users/runner/
471485

472486
## Using `setup-python` on GHES
473487

474-
`setup-python` comes pre-installed on the appliance with GHES if Actions is enabled. When dynamically downloading Python distributions, `setup-python` downloads distributions from [`actions/python-versions`](https://github.com/actions/python-versions) on github.com (outside of the appliance). These calls to `actions/python-versions` are made via unauthenticated requests, which are limited to [60 requests per hour per IP](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting). If more requests are made within the time frame, then you will start to see rate-limit errors during downloading that looks like: `##[error]API rate limit exceeded for...`.
488+
### Avoiding rate limit issues
489+
490+
`setup-python` comes pre-installed on the appliance with GHES if Actions is enabled. When dynamically downloading Python distributions, `setup-python` downloads distributions from [`actions/python-versions`](https://github.com/actions/python-versions) on github.com (outside of the appliance). These calls to `actions/python-versions` are by default made via unauthenticated requests, which are limited to [60 requests per hour per IP](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting). If more requests are made within the time frame, then you will start to see rate-limit errors during downloading that look like this:
491+
492+
##[error]API rate limit exceeded for YOUR_IP. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)
493+
494+
To get a higher rate limit, you can [generate a personal access token (PAT) on github.com](https://github.com/settings/tokens/new) and pass it as the `token` input for the action. It is important to understand that this needs to be a token from github.com and _not_ from your GHES instance. If you or your colleagues do not yet have a github.com account, you might need to create one.
475495

476-
To get a higher rate limit, you can [generate a personal access token on github.com](https://github.com/settings/tokens/new) and pass it as the `token` input for the action:
496+
Here are the steps you need to follow to avoid the rate limit:
497+
498+
1. Create a PAT on any github.com account by using [this link](https://github.com/settings/tokens/new) after logging into github.com (not your Enterprise instance). This PAT does _not_ need any rights, so make sure all the boxes are unchecked.
499+
2. Store this PAT in the repository / organization where you run your workflow, e.g. as `GH_GITHUB_COM_TOKEN`. You can do this by navigating to your repository -> **Settings** -> **Secrets** -> **Actions** -> **New repository secret**.
500+
3. To use this functionality, you need to use any version newer than `v4.3`. Also, change _python-version_ as needed.
477501

478502
```yml
479-
uses: actions/setup-python@v4
480-
with:
481-
token: ${{ secrets.GH_DOTCOM_TOKEN }}
482-
python-version: 3.11
503+
- name: Set up Python
504+
uses: actions/setup-python@4
505+
with:
506+
python-version: 3.8
507+
token: ${{ secrets.GH_GITHUB_COM_TOKEN }}
483508
```
484509

510+
Requests should now be authenticated. To verify that you are getting the higher rate limit, you can call GitHub's [rate limit API](https://docs.github.com/en/rest/rate-limit) from within your workflow ([example](https://github.com/actions/setup-python/pull/443#issuecomment-1206776401)).
511+
512+
### No access to github.com
485513
If the runner is not able to access github.com, any Python versions requested during a workflow run must come from the runner's tool cache. See "[Setting up the tool cache on self-hosted runners without internet access](https://docs.github.com/en/[email protected]/admin/github-actions/managing-access-to-actions-from-githubcom/setting-up-the-tool-cache-on-self-hosted-runners-without-internet-access)" for more information.

0 commit comments

Comments
 (0)