mirror of
https://github.com/astral-sh/ruff-action.git
synced 2026-05-12 20:50:14 +02:00
Add manifest-file input (#352)
This commit is contained in:
committed by
GitHub
parent
535554df96
commit
9b8caf6c41
@@ -431,6 +431,31 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
|
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
|
||||||
|
|
||||||
|
test-custom-manifest-file:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Create test src
|
||||||
|
run: |
|
||||||
|
mkdir -p "${{ runner.temp }}/ruff-manifest-test"
|
||||||
|
printf 'print("hello")\n' > "${{ runner.temp }}/ruff-manifest-test/hello.py"
|
||||||
|
- name: Install from custom manifest file
|
||||||
|
id: ruff-action
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
src: ${{ runner.temp }}/ruff-manifest-test
|
||||||
|
manifest-file: "https://raw.githubusercontent.com/astral-sh/ruff-action/${{ github.ref }}/__tests__/download/custom-manifest.ndjson"
|
||||||
|
- name: Correct version gets installed
|
||||||
|
run: |
|
||||||
|
if [ "$RUFF_VERSION" != "0.15.10" ]; then
|
||||||
|
echo "Wrong ruff version: $RUFF_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
|
||||||
|
|
||||||
all-tests-passed:
|
all-tests-passed:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
@@ -456,6 +481,7 @@ jobs:
|
|||||||
- test-failure
|
- test-failure
|
||||||
- test-multiple-src
|
- test-multiple-src
|
||||||
- test-parent-directory-pyproject
|
- test-parent-directory-pyproject
|
||||||
|
- test-custom-manifest-file
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- name: All tests passed
|
- name: All tests passed
|
||||||
|
|||||||
@@ -20,20 +20,25 @@ anything `ruff` can (ex, fix).
|
|||||||
- [Install a specific version](#install-a-specific-version)
|
- [Install a specific version](#install-a-specific-version)
|
||||||
- [Install a version by supplying a semver range or pep440 specifier](#install-a-version-by-supplying-a-semver-range-or-pep440-specifier)
|
- [Install a version by supplying a semver range or pep440 specifier](#install-a-version-by-supplying-a-semver-range-or-pep440-specifier)
|
||||||
- [Install a version from a specified version file](#install-a-version-from-a-specified-version-file)
|
- [Install a version from a specified version file](#install-a-version-from-a-specified-version-file)
|
||||||
|
- [Install using a custom manifest URL](#install-using-a-custom-manifest-url)
|
||||||
- [Validate checksum](#validate-checksum)
|
- [Validate checksum](#validate-checksum)
|
||||||
- [GitHub authentication token](#github-authentication-token)
|
- [GitHub authentication token](#github-authentication-token)
|
||||||
- [Outputs](#outputs)
|
- [Outputs](#outputs)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
| Input | Description | Default |
|
| Input | Description | Default |
|
||||||
|----------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
|
|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
|
||||||
| `version` | The version of Ruff to install. See [Install specific versions](#install-specific-versions) | `latest` |
|
| `version` | The version of Ruff to install. See [Install specific versions](#install-specific-versions) | `latest` |
|
||||||
| `version-file` | The file to read the version from. See [Install a version from a specified version file](#install-a-version-from-a-specified-version-file) | None |
|
| `version-file` | The file to read the version from. See [Install a version from a specified version file](#install-a-version-from-a-specified-version-file) | None |
|
||||||
| `args` | The arguments to pass to the `ruff` command. See [Configuring Ruff] | `check` |
|
| `manifest-file` | URL to a custom Ruff manifest in the `astral-sh/versions` format. | None |
|
||||||
| `src` | The directory or single files to run `ruff` on. | [github.workspace] |
|
| `args` | The arguments to pass to the `ruff` command. See [Configuring Ruff] | `check` |
|
||||||
| `checksum` | The sha256 checksum of the downloaded executable. | None |
|
| `src` | The directory or single files to run `ruff` on. | [github.workspace] |
|
||||||
| `github-token` | The GitHub token to use for authentication. | `GITHUB_TOKEN` |
|
| `checksum` | The sha256 checksum of the downloaded artifact. | None |
|
||||||
|
| `github-token` | The GitHub token to use when downloading Ruff release artifacts from GitHub. | `GITHUB_TOKEN` |
|
||||||
|
|
||||||
|
By default, Ruff version metadata is resolved from the
|
||||||
|
[`astral-sh/versions` Ruff manifest](https://github.com/astral-sh/versions/blob/main/v1/ruff.ndjson).
|
||||||
|
|
||||||
### Basic
|
### Basic
|
||||||
|
|
||||||
@@ -155,6 +160,19 @@ Currently `pyproject.toml` and `requirements.txt` are supported.
|
|||||||
version-file: "my-path/to/pyproject.toml-or-requirements.txt"
|
version-file: "my-path/to/pyproject.toml-or-requirements.txt"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Install using a custom manifest URL
|
||||||
|
|
||||||
|
You can override the default `astral-sh/versions` manifest with `manifest-file`.
|
||||||
|
This affects both version resolution and artifact selection.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Install Ruff from a custom manifest
|
||||||
|
uses: astral-sh/ruff-action@v3
|
||||||
|
with:
|
||||||
|
version: "latest"
|
||||||
|
manifest-file: "https://example.com/ruff.ndjson"
|
||||||
|
```
|
||||||
|
|
||||||
### Validate checksum
|
### Validate checksum
|
||||||
|
|
||||||
You can specify a checksum to validate the downloaded executable. Checksums up to the default version
|
You can specify a checksum to validate the downloaded executable. Checksums up to the default version
|
||||||
@@ -171,9 +189,11 @@ are automatically verified by this action. The sha256 hashes can be found on the
|
|||||||
|
|
||||||
### GitHub authentication token
|
### GitHub authentication token
|
||||||
|
|
||||||
This action uses the GitHub API to fetch the ruff release artifacts. To avoid hitting the GitHub API
|
By default, this action resolves available uv versions from
|
||||||
rate limit too quickly, an authentication token can be provided via the `github-token` input. By
|
[`astral-sh/versions`](https://github.com/astral-sh/versions) and downloads release artifacts from `https://releases.astral.sh`. If this fails this action falls back to downloading from the GitHub releases page of the ruff repository.
|
||||||
default, the `GITHUB_TOKEN` secret is used, which is automatically provided by GitHub Actions.
|
|
||||||
|
You can provide a token via `github-token` to authenticate those downloads. By default, the
|
||||||
|
`GITHUB_TOKEN` secret is used, which is automatically provided by GitHub Actions.
|
||||||
|
|
||||||
If the default
|
If the default
|
||||||
[permissions for the GitHub token](https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token)
|
[permissions for the GitHub token](https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":"0.15.10","artifacts":[{"platform":"x86_64-unknown-linux-gnu","variant":"default","url":"https://github.com/astral-sh/ruff/releases/download/0.15.10/ruff-x86_64-unknown-linux-gnu.tar.gz","archive_format":"tar.gz","sha256":"e3e9e5c791542f00d95edc74a506e1ac24efc0af9574de01ab338187bf1ff9f6"}]}
|
||||||
@@ -0,0 +1,509 @@
|
|||||||
|
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
||||||
|
import * as semver from "semver";
|
||||||
|
|
||||||
|
const mockInfo = jest.fn();
|
||||||
|
const mockWarning = jest.fn();
|
||||||
|
|
||||||
|
jest.unstable_mockModule("@actions/core", () => ({
|
||||||
|
debug: jest.fn(),
|
||||||
|
info: mockInfo,
|
||||||
|
warning: mockWarning,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockDownloadTool = jest.fn();
|
||||||
|
const mockExtractTar = jest.fn();
|
||||||
|
const mockExtractZip = jest.fn();
|
||||||
|
const mockCacheDir = jest.fn();
|
||||||
|
|
||||||
|
jest.unstable_mockModule("@actions/tool-cache", () => ({
|
||||||
|
cacheDir: mockCacheDir,
|
||||||
|
downloadTool: mockDownloadTool,
|
||||||
|
evaluateVersions: (versions: string[], range: string) =>
|
||||||
|
semver.maxSatisfying(versions, range) ?? "",
|
||||||
|
extractTar: mockExtractTar,
|
||||||
|
extractZip: mockExtractZip,
|
||||||
|
find: () => "",
|
||||||
|
findAllVersions: () => [],
|
||||||
|
isExplicitVersion: (version: string) => semver.valid(version) !== null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockGetLatestVersion = jest.fn();
|
||||||
|
const mockGetAllVersions = jest.fn();
|
||||||
|
const mockGetArtifact = jest.fn();
|
||||||
|
|
||||||
|
jest.unstable_mockModule("../../src/download/manifest", () => ({
|
||||||
|
getAllVersions: mockGetAllVersions,
|
||||||
|
getArtifact: mockGetArtifact,
|
||||||
|
getLatestVersion: mockGetLatestVersion,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockValidateChecksum = jest.fn();
|
||||||
|
|
||||||
|
jest.unstable_mockModule("../../src/download/checksum/checksum", () => ({
|
||||||
|
validateChecksum: mockValidateChecksum,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockCopyFile = jest.fn();
|
||||||
|
const mockReaddir = jest.fn();
|
||||||
|
|
||||||
|
jest.unstable_mockModule("node:fs", () => ({
|
||||||
|
promises: {
|
||||||
|
copyFile: mockCopyFile,
|
||||||
|
readdir: mockReaddir,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { downloadVersion, resolveVersion, rewriteToMirror } = await import(
|
||||||
|
"../../src/download/download-version"
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("download-version", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockInfo.mockReset();
|
||||||
|
mockWarning.mockReset();
|
||||||
|
mockDownloadTool.mockReset();
|
||||||
|
mockExtractTar.mockReset();
|
||||||
|
mockExtractZip.mockReset();
|
||||||
|
mockCacheDir.mockReset();
|
||||||
|
mockGetLatestVersion.mockReset();
|
||||||
|
mockGetAllVersions.mockReset();
|
||||||
|
mockGetArtifact.mockReset();
|
||||||
|
mockValidateChecksum.mockReset();
|
||||||
|
mockCopyFile.mockReset();
|
||||||
|
mockReaddir.mockReset();
|
||||||
|
|
||||||
|
mockDownloadTool.mockResolvedValue("/tmp/downloaded");
|
||||||
|
mockExtractTar.mockResolvedValue("/tmp/extracted");
|
||||||
|
mockExtractZip.mockResolvedValue("/tmp/extracted");
|
||||||
|
mockCacheDir.mockResolvedValue("/tmp/cached");
|
||||||
|
mockReaddir.mockResolvedValue(["ruff"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveVersion", () => {
|
||||||
|
it("uses the default manifest to resolve latest", async () => {
|
||||||
|
mockGetLatestVersion.mockResolvedValue("0.15.8");
|
||||||
|
|
||||||
|
const version = await resolveVersion("latest", undefined);
|
||||||
|
|
||||||
|
expect(version).toBe("0.15.8");
|
||||||
|
expect(mockGetLatestVersion).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockGetLatestVersion).toHaveBeenCalledWith(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the default manifest to resolve available versions", async () => {
|
||||||
|
mockGetAllVersions.mockResolvedValue(["0.15.8", "0.15.7"]);
|
||||||
|
|
||||||
|
const version = await resolveVersion("0.15.x", undefined);
|
||||||
|
|
||||||
|
expect(version).toBe("0.15.8");
|
||||||
|
expect(mockGetAllVersions).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockGetAllVersions).toHaveBeenCalledWith(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses manifest-file when provided", async () => {
|
||||||
|
mockGetAllVersions.mockResolvedValue(["0.15.8", "0.15.7"]);
|
||||||
|
|
||||||
|
const version = await resolveVersion(
|
||||||
|
"0.15.x",
|
||||||
|
"https://example.com/custom.ndjson",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(version).toBe("0.15.8");
|
||||||
|
expect(mockGetAllVersions).toHaveBeenCalledWith(
|
||||||
|
"https://example.com/custom.ndjson",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("downloadVersion", () => {
|
||||||
|
it("fails when manifest lookup fails", async () => {
|
||||||
|
mockGetArtifact.mockRejectedValue(new Error("manifest unavailable"));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.15.8",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
),
|
||||||
|
).rejects.toThrow("manifest unavailable");
|
||||||
|
|
||||||
|
expect(mockDownloadTool).not.toHaveBeenCalled();
|
||||||
|
expect(mockValidateChecksum).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails when no matching artifact exists in the default manifest", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.15.8",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
),
|
||||||
|
).rejects.toThrow(
|
||||||
|
"Could not find artifact for version 0.15.8, arch x86_64, platform unknown-linux-gnu in https://raw.githubusercontent.com/astral-sh/versions/main/v1/ruff.ndjson .",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockDownloadTool).not.toHaveBeenCalled();
|
||||||
|
expect(mockValidateChecksum).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses built-in checksums for default manifest downloads", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "manifest-checksum-that-should-be-ignored",
|
||||||
|
downloadUrl: "https://example.com/ruff.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.15.8",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockValidateChecksum).toHaveBeenCalledWith(
|
||||||
|
undefined,
|
||||||
|
"/tmp/downloaded",
|
||||||
|
"x86_64",
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"0.15.8",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rewrites GitHub Releases URLs to the Astral mirror", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.15.8",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockDownloadTool).toHaveBeenCalledWith(
|
||||||
|
"https://releases.astral.sh/github/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not rewrite non-GitHub URLs", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl: "https://example.com/ruff.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.15.8",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockDownloadTool).toHaveBeenCalledWith(
|
||||||
|
"https://example.com/ruff.tar.gz",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to GitHub Releases when the mirror download fails", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
mockDownloadTool
|
||||||
|
.mockRejectedValueOnce(new Error("mirror unavailable"))
|
||||||
|
.mockResolvedValueOnce("/tmp/downloaded");
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.15.8",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockDownloadTool).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockDownloadTool).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
"https://releases.astral.sh/github/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(mockDownloadTool).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
expect(mockWarning).toHaveBeenCalledWith(
|
||||||
|
"Failed to download from mirror, falling back to GitHub Releases: mirror unavailable",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the canonical old GitHub Releases URL", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/ruff/releases/download/0.4.7/ruff-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
mockDownloadTool
|
||||||
|
.mockRejectedValueOnce(new Error("mirror unavailable"))
|
||||||
|
.mockResolvedValueOnce("/tmp/downloaded");
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.4.7",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockDownloadTool).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
"https://github.com/astral-sh/ruff/releases/download/v0.4.7/ruff-0.4.7-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not fall back when checksum validation fails", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
});
|
||||||
|
mockValidateChecksum.mockRejectedValue(new Error("bad checksum"));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.15.8",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
),
|
||||||
|
).rejects.toThrow("bad checksum");
|
||||||
|
|
||||||
|
expect(mockDownloadTool).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockWarning).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not fall back when extraction fails", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
});
|
||||||
|
mockExtractTar.mockRejectedValue(new Error("extract failed"));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.15.8",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
),
|
||||||
|
).rejects.toThrow("extract failed");
|
||||||
|
|
||||||
|
expect(mockDownloadTool).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockWarning).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not fall back for non-GitHub URLs", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl: "https://example.com/ruff.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
mockDownloadTool.mockRejectedValue(new Error("download failed"));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.15.8",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
),
|
||||||
|
).rejects.toThrow("download failed");
|
||||||
|
|
||||||
|
expect(mockDownloadTool).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses manifest-file checksum metadata when checksum input is unset", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "manifest-checksum",
|
||||||
|
downloadUrl: "https://example.com/custom-ruff.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.15.8",
|
||||||
|
"",
|
||||||
|
"token",
|
||||||
|
"https://example.com/custom.ndjson",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockValidateChecksum).toHaveBeenCalledWith(
|
||||||
|
"manifest-checksum",
|
||||||
|
"/tmp/downloaded",
|
||||||
|
"x86_64",
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"0.15.8",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers checksum input over manifest-file checksum metadata", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "manifest-checksum",
|
||||||
|
downloadUrl: "https://example.com/custom-ruff.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.15.8",
|
||||||
|
"user-checksum",
|
||||||
|
"token",
|
||||||
|
"https://example.com/custom.ndjson",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockValidateChecksum).toHaveBeenCalledWith(
|
||||||
|
"user-checksum",
|
||||||
|
"/tmp/downloaded",
|
||||||
|
"x86_64",
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"0.15.8",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves tar extraction behavior for newer versions", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl: "https://example.com/ruff.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.15.8",
|
||||||
|
"user-checksum",
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockExtractTar).toHaveBeenCalledWith("/tmp/downloaded");
|
||||||
|
expect(mockCacheDir).toHaveBeenCalledWith(
|
||||||
|
"/tmp/extracted/ruff-x86_64-unknown-linux-gnu",
|
||||||
|
"ruff",
|
||||||
|
"0.15.8",
|
||||||
|
"x86_64",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves tar extraction behavior for older versions", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl: "https://example.com/ruff.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.4.10",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockCacheDir).toHaveBeenCalledWith(
|
||||||
|
"/tmp/extracted",
|
||||||
|
"ruff",
|
||||||
|
"0.4.10",
|
||||||
|
"x86_64",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves zip extraction behavior on Windows", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "zip",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl: "https://example.com/ruff.zip",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"pc-windows-msvc",
|
||||||
|
"x86_64",
|
||||||
|
"0.15.8",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockCopyFile).toHaveBeenCalledWith(
|
||||||
|
"/tmp/downloaded",
|
||||||
|
"/tmp/downloaded.zip",
|
||||||
|
);
|
||||||
|
expect(mockExtractZip).toHaveBeenCalledWith("/tmp/downloaded.zip");
|
||||||
|
expect(mockCacheDir).toHaveBeenCalledWith(
|
||||||
|
"/tmp/extracted",
|
||||||
|
"ruff",
|
||||||
|
"0.15.8",
|
||||||
|
"x86_64",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rewriteToMirror", () => {
|
||||||
|
it("rewrites a GitHub Releases URL to the Astral mirror", () => {
|
||||||
|
expect(
|
||||||
|
rewriteToMirror(
|
||||||
|
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
),
|
||||||
|
).toBe(
|
||||||
|
"https://releases.astral.sh/github/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined for non-GitHub URLs", () => {
|
||||||
|
expect(
|
||||||
|
rewriteToMirror("https://example.com/ruff.tar.gz"),
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined for a different GitHub repo", () => {
|
||||||
|
expect(
|
||||||
|
rewriteToMirror(
|
||||||
|
"https://github.com/other/repo/releases/download/v1.0/file.tar.gz",
|
||||||
|
),
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
||||||
|
|
||||||
|
const mockFetch = jest.fn();
|
||||||
|
|
||||||
|
jest.unstable_mockModule("@actions/core", () => ({
|
||||||
|
debug: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule("../../src/utils/fetch", () => ({
|
||||||
|
fetch: mockFetch,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {
|
||||||
|
clearManifestCache,
|
||||||
|
fetchManifest,
|
||||||
|
getAllVersions,
|
||||||
|
getArtifact,
|
||||||
|
getLatestVersion,
|
||||||
|
parseManifest,
|
||||||
|
} = await import("../../src/download/manifest");
|
||||||
|
|
||||||
|
const sampleManifestResponse = `{"version":"0.15.8","artifacts":[{"platform":"aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-aarch64-apple-darwin.tar.gz","archive_format":"tar.gz","sha256":"fcf0a9ea6599c6ae28a4c854ac6da76f2c889354d7c36ce136ef071f7ab9721f"},{"platform":"x86_64-pc-windows-msvc","variant":"default","url":"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-pc-windows-msvc.zip","archive_format":"zip","sha256":"eb02fd95d8e0eed462b4a67ecdd320d865b38c560bffcda9a0b87ec944bdf036"}]}
|
||||||
|
{"version":"0.15.7","artifacts":[{"platform":"aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/ruff/releases/download/0.15.7/ruff-aarch64-apple-darwin.tar.gz","archive_format":"tar.gz","sha256":"606b3c6949d971709f2526fa0d9f0fd23ccf60e09f117999b406b424af18a6a6"}]}`;
|
||||||
|
|
||||||
|
const multiVariantManifestResponse = `{"version":"0.15.8","artifacts":[{"platform":"aarch64-apple-darwin","variant":"python-managed","url":"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-aarch64-apple-darwin-managed.tar.gz","archive_format":"tar.gz","sha256":"managed-checksum"},{"platform":"aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-aarch64-apple-darwin.zip","archive_format":"zip","sha256":"default-checksum"}]}`;
|
||||||
|
|
||||||
|
const oldVersionManifestResponse = `{"version":"v0.4.7","artifacts":[{"platform":"0.4.7-aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/ruff/releases/download/v0.4.7/ruff-0.4.7-aarch64-apple-darwin.tar.gz","archive_format":"tar.gz","sha256":"old-checksum"}]}`;
|
||||||
|
|
||||||
|
function createMockResponse(
|
||||||
|
ok: boolean,
|
||||||
|
status: number,
|
||||||
|
statusText: string,
|
||||||
|
data: string,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
ok,
|
||||||
|
status,
|
||||||
|
statusText,
|
||||||
|
text: async () => data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("manifest", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
clearManifestCache();
|
||||||
|
mockFetch.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetchManifest", () => {
|
||||||
|
it("fetches and parses manifest data", async () => {
|
||||||
|
mockFetch.mockResolvedValue(
|
||||||
|
createMockResponse(true, 200, "OK", sampleManifestResponse),
|
||||||
|
);
|
||||||
|
|
||||||
|
const versions = await fetchManifest();
|
||||||
|
|
||||||
|
expect(versions).toHaveLength(2);
|
||||||
|
expect(versions[0]?.version).toBe("0.15.8");
|
||||||
|
expect(versions[1]?.version).toBe("0.15.7");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws on a failed fetch", async () => {
|
||||||
|
mockFetch.mockResolvedValue(
|
||||||
|
createMockResponse(false, 500, "Internal Server Error", ""),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(fetchManifest()).rejects.toThrow(
|
||||||
|
"Failed to fetch manifest data: 500 Internal Server Error",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("caches results per URL", async () => {
|
||||||
|
mockFetch.mockResolvedValue(
|
||||||
|
createMockResponse(true, 200, "OK", sampleManifestResponse),
|
||||||
|
);
|
||||||
|
|
||||||
|
await fetchManifest("https://example.com/custom.ndjson");
|
||||||
|
await fetchManifest("https://example.com/custom.ndjson");
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAllVersions", () => {
|
||||||
|
it("returns all version strings", async () => {
|
||||||
|
mockFetch.mockResolvedValue(
|
||||||
|
createMockResponse(true, 200, "OK", sampleManifestResponse),
|
||||||
|
);
|
||||||
|
|
||||||
|
const versions = await getAllVersions(
|
||||||
|
"https://example.com/custom.ndjson",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(versions).toEqual(["0.15.8", "0.15.7"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getLatestVersion", () => {
|
||||||
|
it("returns the first version string", async () => {
|
||||||
|
mockFetch.mockResolvedValue(
|
||||||
|
createMockResponse(true, 200, "OK", sampleManifestResponse),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
getLatestVersion("https://example.com/custom.ndjson"),
|
||||||
|
).resolves.toBe("0.15.8");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getArtifact", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockFetch.mockResolvedValue(
|
||||||
|
createMockResponse(true, 200, "OK", sampleManifestResponse),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds an artifact by version and platform", async () => {
|
||||||
|
const artifact = await getArtifact("0.15.8", "aarch64", "apple-darwin");
|
||||||
|
|
||||||
|
expect(artifact).toEqual({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum:
|
||||||
|
"fcf0a9ea6599c6ae28a4c854ac6da76f2c889354d7c36ce136ef071f7ab9721f",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-aarch64-apple-darwin.tar.gz",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds a windows artifact", async () => {
|
||||||
|
const artifact = await getArtifact("0.15.8", "x86_64", "pc-windows-msvc");
|
||||||
|
|
||||||
|
expect(artifact).toEqual({
|
||||||
|
archiveFormat: "zip",
|
||||||
|
checksum:
|
||||||
|
"eb02fd95d8e0eed462b4a67ecdd320d865b38c560bffcda9a0b87ec944bdf036",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-pc-windows-msvc.zip",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers the default variant when multiple artifacts share a platform", async () => {
|
||||||
|
mockFetch.mockResolvedValue(
|
||||||
|
createMockResponse(true, 200, "OK", multiVariantManifestResponse),
|
||||||
|
);
|
||||||
|
|
||||||
|
const artifact = await getArtifact("0.15.8", "aarch64", "apple-darwin");
|
||||||
|
|
||||||
|
expect(artifact).toEqual({
|
||||||
|
archiveFormat: "zip",
|
||||||
|
checksum: "default-checksum",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-aarch64-apple-darwin.zip",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds an old artifact when the manifest version has a v prefix", async () => {
|
||||||
|
mockFetch.mockResolvedValue(
|
||||||
|
createMockResponse(true, 200, "OK", oldVersionManifestResponse),
|
||||||
|
);
|
||||||
|
|
||||||
|
const artifact = await getArtifact("0.4.7", "aarch64", "apple-darwin");
|
||||||
|
|
||||||
|
expect(artifact).toEqual({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "old-checksum",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/ruff/releases/download/v0.4.7/ruff-0.4.7-aarch64-apple-darwin.tar.gz",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined for an unknown version", async () => {
|
||||||
|
const artifact = await getArtifact("0.0.1", "aarch64", "apple-darwin");
|
||||||
|
|
||||||
|
expect(artifact).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined for an unknown platform", async () => {
|
||||||
|
const artifact = await getArtifact(
|
||||||
|
"0.15.8",
|
||||||
|
"aarch64",
|
||||||
|
"unknown-linux-musl",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(artifact).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseManifest", () => {
|
||||||
|
it("throws for malformed manifest data", () => {
|
||||||
|
expect(() => parseManifest('{"version":"0.1.0"', "test-source")).toThrow(
|
||||||
|
"Failed to parse manifest data from test-source",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+4
-2
@@ -20,10 +20,12 @@ inputs:
|
|||||||
checksum:
|
checksum:
|
||||||
description: "The checksum of the ruff version to install"
|
description: "The checksum of the ruff version to install"
|
||||||
required: false
|
required: false
|
||||||
|
manifest-file:
|
||||||
|
description: "URL to a custom manifest file in the astral-sh/versions format."
|
||||||
|
required: false
|
||||||
github-token:
|
github-token:
|
||||||
description:
|
description:
|
||||||
"Used to increase the rate limit when retrieving versions and downloading
|
"Used for authenticated downloads of Ruff release artifacts from GitHub."
|
||||||
ruff."
|
|
||||||
required: false
|
required: false
|
||||||
default: ${{ github.token }}
|
default: ${{ github.token }}
|
||||||
outputs:
|
outputs:
|
||||||
|
|||||||
+891
-4090
File diff suppressed because it is too large
Load Diff
Generated
+2
-1
@@ -16,7 +16,8 @@
|
|||||||
"@octokit/plugin-paginate-rest": "^13.1.1",
|
"@octokit/plugin-paginate-rest": "^13.1.1",
|
||||||
"@octokit/plugin-rest-endpoint-methods": "^16.0.0",
|
"@octokit/plugin-rest-endpoint-methods": "^16.0.0",
|
||||||
"@renovatebot/pep440": "^4.2.1",
|
"@renovatebot/pep440": "^4.2.1",
|
||||||
"smol-toml": "^1.6.0"
|
"smol-toml": "^1.6.0",
|
||||||
|
"undici": "^6.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.7",
|
"@biomejs/biome": "^2.4.7",
|
||||||
|
|||||||
+2
-1
@@ -35,7 +35,8 @@
|
|||||||
"@octokit/plugin-paginate-rest": "^13.1.1",
|
"@octokit/plugin-paginate-rest": "^13.1.1",
|
||||||
"@octokit/plugin-rest-endpoint-methods": "^16.0.0",
|
"@octokit/plugin-rest-endpoint-methods": "^16.0.0",
|
||||||
"@renovatebot/pep440": "^4.2.1",
|
"@renovatebot/pep440": "^4.2.1",
|
||||||
"smol-toml": "^1.6.0"
|
"smol-toml": "^1.6.0",
|
||||||
|
"undici": "^6.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.7",
|
"@biomejs/biome": "^2.4.7",
|
||||||
|
|||||||
+144
-104
@@ -2,16 +2,17 @@ import { promises as fs } from "node:fs";
|
|||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import * as tc from "@actions/tool-cache";
|
import * as tc from "@actions/tool-cache";
|
||||||
import { Octokit } from "@octokit/core";
|
|
||||||
import { paginateRest } from "@octokit/plugin-paginate-rest";
|
|
||||||
import { restEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
|
|
||||||
import * as pep440 from "@renovatebot/pep440";
|
import * as pep440 from "@renovatebot/pep440";
|
||||||
import * as semver from "semver";
|
import * as semver from "semver";
|
||||||
import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants";
|
import {
|
||||||
|
ASTRAL_MIRROR_PREFIX,
|
||||||
|
GITHUB_RELEASES_PREFIX,
|
||||||
|
TOOL_CACHE_NAME,
|
||||||
|
VERSIONS_MANIFEST_URL,
|
||||||
|
} from "../utils/constants";
|
||||||
import type { Architecture, Platform } from "../utils/platforms";
|
import type { Architecture, Platform } from "../utils/platforms";
|
||||||
import { validateChecksum } from "./checksum/checksum";
|
import { validateChecksum } from "./checksum/checksum";
|
||||||
|
import { getAllVersions, getArtifact, getLatestVersion } from "./manifest";
|
||||||
const PaginatingOctokit = Octokit.plugin(paginateRest, restEndpointMethods);
|
|
||||||
|
|
||||||
export function tryGetFromToolCache(
|
export function tryGetFromToolCache(
|
||||||
arch: Architecture,
|
arch: Architecture,
|
||||||
@@ -32,31 +33,46 @@ export async function downloadVersion(
|
|||||||
platform: Platform,
|
platform: Platform,
|
||||||
arch: Architecture,
|
arch: Architecture,
|
||||||
version: string,
|
version: string,
|
||||||
checkSum: string | undefined,
|
checksum: string | undefined,
|
||||||
githubToken: string,
|
githubToken: string,
|
||||||
|
manifestUrl?: string,
|
||||||
): Promise<{ version: string; cachedToolDir: string }> {
|
): Promise<{ version: string; cachedToolDir: string }> {
|
||||||
const artifact = `ruff-${arch}-${platform}`;
|
const artifact = await getArtifact(version, arch, platform, manifestUrl);
|
||||||
let extension = ".tar.gz";
|
|
||||||
if (platform === "pc-windows-msvc") {
|
|
||||||
extension = ".zip";
|
|
||||||
}
|
|
||||||
const downloadUrl = constructDownloadUrl(version, platform, arch);
|
|
||||||
core.debug(`Downloading ruff from "${downloadUrl}" ...`);
|
|
||||||
|
|
||||||
const downloadPath = await tc.downloadTool(
|
if (!artifact) {
|
||||||
downloadUrl,
|
throw new Error(
|
||||||
undefined,
|
getMissingArtifactMessage(version, arch, platform, manifestUrl),
|
||||||
githubToken,
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the default astral-sh/versions source, checksum validation relies on
|
||||||
|
// user input or the built-in KNOWN_CHECKSUMS table, not manifest sha256 values.
|
||||||
|
const resolvedChecksum =
|
||||||
|
manifestUrl === undefined
|
||||||
|
? checksum
|
||||||
|
: resolveChecksum(checksum, artifact.checksum);
|
||||||
|
|
||||||
|
const downloadPath = await downloadArtifact(
|
||||||
|
artifact.downloadUrl,
|
||||||
|
platform,
|
||||||
|
arch,
|
||||||
|
version,
|
||||||
|
getDownloadToken(artifact.downloadUrl, githubToken),
|
||||||
|
);
|
||||||
|
await validateChecksum(
|
||||||
|
resolvedChecksum,
|
||||||
|
downloadPath,
|
||||||
|
arch,
|
||||||
|
platform,
|
||||||
|
version,
|
||||||
);
|
);
|
||||||
core.debug(`Downloaded ruff to "${downloadPath}"`);
|
|
||||||
await validateChecksum(checkSum, downloadPath, arch, platform, version);
|
|
||||||
|
|
||||||
const extractedDir = await extractDownloadedArtifact(
|
const extractedDir = await extractDownloadedArtifact(
|
||||||
version,
|
version,
|
||||||
downloadPath,
|
downloadPath,
|
||||||
extension,
|
getExtension(platform),
|
||||||
platform,
|
platform,
|
||||||
artifact,
|
`ruff-${arch}-${platform}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const cachedToolDir = await tc.cacheDir(
|
const cachedToolDir = await tc.cacheDir(
|
||||||
@@ -65,25 +81,60 @@ export async function downloadVersion(
|
|||||||
version,
|
version,
|
||||||
arch,
|
arch,
|
||||||
);
|
);
|
||||||
return { cachedToolDir, version: version };
|
return { cachedToolDir, version };
|
||||||
}
|
}
|
||||||
|
|
||||||
function constructDownloadUrl(
|
export function rewriteToMirror(url: string): string | undefined {
|
||||||
version: string,
|
if (!url.startsWith(GITHUB_RELEASES_PREFIX)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ASTRAL_MIRROR_PREFIX + url.slice(GITHUB_RELEASES_PREFIX.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadArtifact(
|
||||||
|
downloadUrl: string,
|
||||||
platform: Platform,
|
platform: Platform,
|
||||||
arch: Architecture,
|
arch: Architecture,
|
||||||
): string {
|
version: string,
|
||||||
const artifactVersionSuffix =
|
githubToken: string | undefined,
|
||||||
semver.lte(version, "v0.4.10") && semver.gte(version, "v0.1.8")
|
): Promise<string> {
|
||||||
? `-${version}`
|
const mirrorUrl = rewriteToMirror(downloadUrl);
|
||||||
: "";
|
const resolvedDownloadUrl = mirrorUrl ?? downloadUrl;
|
||||||
const artifact = `ruff${artifactVersionSuffix}-${arch}-${platform}`;
|
|
||||||
let extension = ".tar.gz";
|
try {
|
||||||
if (platform === "pc-windows-msvc") {
|
return await downloadFile(
|
||||||
extension = ".zip";
|
resolvedDownloadUrl,
|
||||||
|
mirrorUrl !== undefined ? undefined : githubToken,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (mirrorUrl === undefined) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
core.warning(
|
||||||
|
`Failed to download from mirror, falling back to GitHub Releases: ${(err as Error).message}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return await downloadFile(
|
||||||
|
constructDownloadUrl(version, platform, arch),
|
||||||
|
githubToken,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const versionPrefix = semver.lte(version, "v0.4.10") ? "v" : "";
|
}
|
||||||
return `https://github.com/${OWNER}/${REPO}/releases/download/${versionPrefix}${version}/${artifact}${extension}`;
|
|
||||||
|
async function downloadFile(
|
||||||
|
downloadUrl: string,
|
||||||
|
githubToken: string | undefined,
|
||||||
|
): Promise<string> {
|
||||||
|
core.info(`Downloading ruff from "${downloadUrl}" ...`);
|
||||||
|
const downloadPath = await tc.downloadTool(
|
||||||
|
downloadUrl,
|
||||||
|
undefined,
|
||||||
|
githubToken,
|
||||||
|
);
|
||||||
|
core.debug(`Downloaded ruff to "${downloadPath}"`);
|
||||||
|
return downloadPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractDownloadedArtifact(
|
async function extractDownloadedArtifact(
|
||||||
@@ -98,7 +149,7 @@ async function extractDownloadedArtifact(
|
|||||||
const fullPathWithExtension = `${downloadPath}${extension}`;
|
const fullPathWithExtension = `${downloadPath}${extension}`;
|
||||||
await fs.copyFile(downloadPath, fullPathWithExtension);
|
await fs.copyFile(downloadPath, fullPathWithExtension);
|
||||||
ruffDir = await tc.extractZip(fullPathWithExtension);
|
ruffDir = await tc.extractZip(fullPathWithExtension);
|
||||||
// On windows extracting the zip does not create an intermediate directory
|
// On windows extracting the zip does not create an intermediate directory.
|
||||||
} else {
|
} else {
|
||||||
ruffDir = await tc.extractTar(downloadPath);
|
ruffDir = await tc.extractTar(downloadPath);
|
||||||
if (semver.gte(version, "v0.5.0")) {
|
if (semver.gte(version, "v0.5.0")) {
|
||||||
@@ -113,18 +164,21 @@ async function extractDownloadedArtifact(
|
|||||||
|
|
||||||
export async function resolveVersion(
|
export async function resolveVersion(
|
||||||
versionInput: string,
|
versionInput: string,
|
||||||
githubToken: string,
|
manifestUrl?: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
core.debug(`Resolving ${versionInput}...`);
|
core.debug(`Resolving ${versionInput}...`);
|
||||||
|
|
||||||
const version =
|
const version =
|
||||||
versionInput === "latest"
|
versionInput === "latest"
|
||||||
? await getLatestVersion(githubToken)
|
? await getLatestVersion(manifestUrl)
|
||||||
: versionInput;
|
: versionInput;
|
||||||
|
|
||||||
if (tc.isExplicitVersion(version)) {
|
if (tc.isExplicitVersion(version)) {
|
||||||
core.debug(`Version ${version} is an explicit version.`);
|
core.debug(`Version ${version} is an explicit version.`);
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
const availableVersions = await getAvailableVersions(githubToken);
|
|
||||||
|
const availableVersions = await getAvailableVersions(manifestUrl);
|
||||||
const resolvedVersion = maxSatisfying(availableVersions, version);
|
const resolvedVersion = maxSatisfying(availableVersions, version);
|
||||||
if (resolvedVersion === undefined) {
|
if (resolvedVersion === undefined) {
|
||||||
throw new Error(`No version found for ${version}`);
|
throw new Error(`No version found for ${version}`);
|
||||||
@@ -133,77 +187,63 @@ export async function resolveVersion(
|
|||||||
return resolvedVersion;
|
return resolvedVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAvailableVersions(githubToken: string): Promise<string[]> {
|
async function getAvailableVersions(manifestUrl?: string): Promise<string[]> {
|
||||||
try {
|
return await getAllVersions(manifestUrl);
|
||||||
const octokit = new PaginatingOctokit({
|
|
||||||
auth: githubToken,
|
|
||||||
});
|
|
||||||
return await getReleaseTagNames(octokit);
|
|
||||||
} catch (err) {
|
|
||||||
if ((err as Error).message.includes("Bad credentials")) {
|
|
||||||
core.info(
|
|
||||||
"No (valid) GitHub token provided. Falling back to anonymous. Requests might be rate limited.",
|
|
||||||
);
|
|
||||||
const octokit = new PaginatingOctokit();
|
|
||||||
return await getReleaseTagNames(octokit);
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getReleaseTagNames(
|
function getMissingArtifactMessage(
|
||||||
octokit: InstanceType<typeof PaginatingOctokit>,
|
version: string,
|
||||||
): Promise<string[]> {
|
arch: Architecture,
|
||||||
const response = await octokit.paginate(octokit.rest.repos.listReleases, {
|
platform: Platform,
|
||||||
owner: OWNER,
|
manifestUrl?: string,
|
||||||
repo: REPO,
|
): string {
|
||||||
});
|
if (manifestUrl === undefined) {
|
||||||
const releaseTagNames = response.map((release) => release.tag_name);
|
return `Could not find artifact for version ${version}, arch ${arch}, platform ${platform} in ${VERSIONS_MANIFEST_URL} .`;
|
||||||
if (releaseTagNames.length === 0) {
|
|
||||||
throw Error(
|
|
||||||
"Github API request failed while getting releases. Check the GitHub status page for outages. Try again later.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return response.map((release) => release.tag_name);
|
|
||||||
|
return `manifest-file does not contain version ${version}, arch ${arch}, platform ${platform}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLatestVersion(githubToken: string) {
|
function resolveChecksum(
|
||||||
const octokit = new PaginatingOctokit({
|
checksum: string | undefined,
|
||||||
auth: githubToken,
|
manifestChecksum: string,
|
||||||
});
|
): string {
|
||||||
|
return checksum !== undefined && checksum !== ""
|
||||||
let latestRelease: { tag_name: string } | undefined;
|
? checksum
|
||||||
try {
|
: manifestChecksum;
|
||||||
latestRelease = await getLatestRelease(octokit);
|
|
||||||
} catch (err) {
|
|
||||||
if ((err as Error).message.includes("Bad credentials")) {
|
|
||||||
core.info(
|
|
||||||
"No (valid) GitHub token provided. Falling back to anonymous. Requests might be rate limited.",
|
|
||||||
);
|
|
||||||
const octokit = new PaginatingOctokit();
|
|
||||||
latestRelease = await getLatestRelease(octokit);
|
|
||||||
} else {
|
|
||||||
core.error(
|
|
||||||
"Github API request failed while getting latest release. Check the GitHub status page for outages. Try again later.",
|
|
||||||
);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!latestRelease) {
|
|
||||||
throw new Error("Could not determine latest release.");
|
|
||||||
}
|
|
||||||
return latestRelease.tag_name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLatestRelease(
|
function getDownloadToken(
|
||||||
octokit: InstanceType<typeof PaginatingOctokit>,
|
downloadUrl: string,
|
||||||
) {
|
githubToken: string,
|
||||||
const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({
|
): string | undefined {
|
||||||
owner: OWNER,
|
return downloadUrl.startsWith(GITHUB_RELEASES_PREFIX)
|
||||||
repo: REPO,
|
? githubToken
|
||||||
});
|
: undefined;
|
||||||
return latestRelease;
|
}
|
||||||
|
|
||||||
|
function constructDownloadUrl(
|
||||||
|
version: string,
|
||||||
|
platform: Platform,
|
||||||
|
arch: Architecture,
|
||||||
|
): string {
|
||||||
|
const normalizedVersion = stripVersionPrefix(version);
|
||||||
|
const artifactVersionSuffix =
|
||||||
|
semver.lte(version, "v0.4.10") && semver.gte(version, "v0.1.8")
|
||||||
|
? `-${normalizedVersion}`
|
||||||
|
: "";
|
||||||
|
const artifact = `ruff${artifactVersionSuffix}-${arch}-${platform}`;
|
||||||
|
const versionPrefix = semver.lte(version, "v0.4.10") ? "v" : "";
|
||||||
|
|
||||||
|
return `${GITHUB_RELEASES_PREFIX}${versionPrefix}${normalizedVersion}/${artifact}${getExtension(platform)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripVersionPrefix(version: string): string {
|
||||||
|
return version.startsWith("v") ? version.slice(1) : version;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExtension(platform: Platform): string {
|
||||||
|
return platform === "pc-windows-msvc" ? ".zip" : ".tar.gz";
|
||||||
}
|
}
|
||||||
|
|
||||||
function maxSatisfying(
|
function maxSatisfying(
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
import * as core from "@actions/core";
|
||||||
|
import { VERSIONS_MANIFEST_URL } from "../utils/constants";
|
||||||
|
import { fetch } from "../utils/fetch";
|
||||||
|
import { selectDefaultVariant } from "./variant-selection";
|
||||||
|
|
||||||
|
export interface ManifestArtifact {
|
||||||
|
platform: string;
|
||||||
|
variant?: string;
|
||||||
|
url: string;
|
||||||
|
archive_format: string;
|
||||||
|
sha256: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ManifestVersion {
|
||||||
|
version: string;
|
||||||
|
artifacts: ManifestArtifact[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArtifactResult {
|
||||||
|
archiveFormat: string;
|
||||||
|
checksum: string;
|
||||||
|
downloadUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedManifestData = new Map<string, ManifestVersion[]>();
|
||||||
|
|
||||||
|
export async function fetchManifest(
|
||||||
|
manifestUrl: string = VERSIONS_MANIFEST_URL,
|
||||||
|
): Promise<ManifestVersion[]> {
|
||||||
|
const cachedVersions = cachedManifestData.get(manifestUrl);
|
||||||
|
if (cachedVersions !== undefined) {
|
||||||
|
core.debug(`Using cached manifest data from ${manifestUrl}`);
|
||||||
|
return cachedVersions;
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info(`Fetching manifest data from ${manifestUrl} ...`);
|
||||||
|
const response = await fetch(manifestUrl, {});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch manifest data: ${response.status} ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await response.text();
|
||||||
|
const versions = parseManifest(body, manifestUrl);
|
||||||
|
cachedManifestData.set(manifestUrl, versions);
|
||||||
|
return versions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseManifest(
|
||||||
|
data: string,
|
||||||
|
sourceDescription: string,
|
||||||
|
): ManifestVersion[] {
|
||||||
|
const trimmed = data.trim();
|
||||||
|
if (trimmed === "") {
|
||||||
|
throw new Error(`Manifest at ${sourceDescription} is empty.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith("[")) {
|
||||||
|
throw new Error(
|
||||||
|
`Legacy JSON array manifests are no longer supported in ${sourceDescription}. Use the astral-sh/versions manifest format instead.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions: ManifestVersion[] = [];
|
||||||
|
|
||||||
|
for (const [index, line] of data.split("\n").entries()) {
|
||||||
|
const record = line.trim();
|
||||||
|
if (record === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(record);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse manifest data from ${sourceDescription} at line ${index + 1}: ${(error as Error).message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isManifestVersion(parsed)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid manifest record in ${sourceDescription} at line ${index + 1}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
versions.push(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versions.length === 0) {
|
||||||
|
throw new Error(`No manifest data found in ${sourceDescription}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLatestVersion(
|
||||||
|
manifestUrl: string = VERSIONS_MANIFEST_URL,
|
||||||
|
): Promise<string> {
|
||||||
|
const latestVersion = (await fetchManifest(manifestUrl))[0]?.version;
|
||||||
|
|
||||||
|
if (latestVersion === undefined) {
|
||||||
|
throw new Error("No versions found in manifest data");
|
||||||
|
}
|
||||||
|
|
||||||
|
core.debug(`Latest version from manifest: ${latestVersion}`);
|
||||||
|
return latestVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllVersions(
|
||||||
|
manifestUrl: string = VERSIONS_MANIFEST_URL,
|
||||||
|
): Promise<string[]> {
|
||||||
|
core.info(
|
||||||
|
`Getting available versions from ${manifestSource(manifestUrl)} ...`,
|
||||||
|
);
|
||||||
|
const versions = await fetchManifest(manifestUrl);
|
||||||
|
return versions.map((versionData) => versionData.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getArtifact(
|
||||||
|
version: string,
|
||||||
|
arch: string,
|
||||||
|
platform: string,
|
||||||
|
manifestUrl: string = VERSIONS_MANIFEST_URL,
|
||||||
|
): Promise<ArtifactResult | undefined> {
|
||||||
|
const versions = await fetchManifest(manifestUrl);
|
||||||
|
const versionData = versions.find((candidate) =>
|
||||||
|
matchesManifestVersion(candidate.version, version),
|
||||||
|
);
|
||||||
|
if (!versionData) {
|
||||||
|
core.debug(`Version ${version} not found in manifest ${manifestUrl}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPlatforms = getTargetPlatforms(
|
||||||
|
versionData.version,
|
||||||
|
arch,
|
||||||
|
platform,
|
||||||
|
);
|
||||||
|
const matchingArtifacts = versionData.artifacts.filter((candidate) =>
|
||||||
|
targetPlatforms.includes(candidate.platform),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingArtifacts.length === 0) {
|
||||||
|
core.debug(
|
||||||
|
`Artifact for ${targetPlatforms.join(" or ")} not found in version ${version}. Available platforms: ${versionData.artifacts
|
||||||
|
.map((candidate) => candidate.platform)
|
||||||
|
.join(", ")}`,
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const artifact = selectDefaultVariant(
|
||||||
|
matchingArtifacts,
|
||||||
|
`Multiple artifacts found for ${targetPlatforms.join(" or ")} in version ${version}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
archiveFormat: artifact.archive_format,
|
||||||
|
checksum: artifact.sha256,
|
||||||
|
downloadUrl: artifact.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearManifestCache(manifestUrl?: string): void {
|
||||||
|
if (manifestUrl === undefined) {
|
||||||
|
cachedManifestData.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedManifestData.delete(manifestUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function manifestSource(manifestUrl: string): string {
|
||||||
|
if (manifestUrl === VERSIONS_MANIFEST_URL) {
|
||||||
|
return VERSIONS_MANIFEST_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `manifest-file ${manifestUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesManifestVersion(
|
||||||
|
manifestVersion: string,
|
||||||
|
requestedVersion: string,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
manifestVersion === requestedVersion ||
|
||||||
|
manifestVersion === withVersionPrefix(requestedVersion)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetPlatforms(
|
||||||
|
manifestVersion: string,
|
||||||
|
arch: string,
|
||||||
|
platform: string,
|
||||||
|
): string[] {
|
||||||
|
const targetPlatform = `${arch}-${platform}`;
|
||||||
|
const versionPrefixedTargetPlatform = `${stripVersionPrefix(manifestVersion)}-${targetPlatform}`;
|
||||||
|
|
||||||
|
return [targetPlatform, versionPrefixedTargetPlatform];
|
||||||
|
}
|
||||||
|
|
||||||
|
function withVersionPrefix(version: string): string {
|
||||||
|
return version.startsWith("v") ? version : `v${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripVersionPrefix(version: string): string {
|
||||||
|
return version.startsWith("v") ? version.slice(1) : version;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isManifestVersion(value: unknown): value is ManifestVersion {
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value.version !== "string" || !Array.isArray(value.artifacts)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.artifacts.every(isManifestArtifact);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isManifestArtifact(value: unknown): value is ManifestArtifact {
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantIsValid =
|
||||||
|
typeof value.variant === "string" || value.variant === undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
typeof value.archive_format === "string" &&
|
||||||
|
typeof value.platform === "string" &&
|
||||||
|
typeof value.sha256 === "string" &&
|
||||||
|
typeof value.url === "string" &&
|
||||||
|
variantIsValid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
interface VariantAwareEntry {
|
||||||
|
variant?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectDefaultVariant<T extends VariantAwareEntry>(
|
||||||
|
entries: T[],
|
||||||
|
duplicateEntryDescription: string,
|
||||||
|
): T {
|
||||||
|
const firstEntry = entries[0];
|
||||||
|
if (firstEntry === undefined) {
|
||||||
|
throw new Error("selectDefaultVariant requires at least one candidate.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.length === 1) {
|
||||||
|
return firstEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultEntries = entries.filter((entry) =>
|
||||||
|
isDefaultVariant(entry.variant),
|
||||||
|
);
|
||||||
|
if (defaultEntries.length === 1) {
|
||||||
|
return defaultEntries[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`${duplicateEntryDescription} with variants ${formatVariants(entries)}. ruff-action currently requires a single default variant for duplicate platform entries.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDefaultVariant(variant: string | undefined): boolean {
|
||||||
|
return variant === undefined || variant === "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatVariants<T extends VariantAwareEntry>(entries: T[]): string {
|
||||||
|
return entries
|
||||||
|
.map((entry) => entry.variant ?? "default")
|
||||||
|
.sort((left, right) => left.localeCompare(right))
|
||||||
|
.join(", ");
|
||||||
|
}
|
||||||
+13
-4
@@ -11,6 +11,7 @@ import {
|
|||||||
args,
|
args,
|
||||||
checkSum,
|
checkSum,
|
||||||
githubToken,
|
githubToken,
|
||||||
|
manifestFile,
|
||||||
src,
|
src,
|
||||||
version,
|
version,
|
||||||
versionFile as versionFileInput,
|
versionFile as versionFileInput,
|
||||||
@@ -62,6 +63,7 @@ async function setupRuff(
|
|||||||
githubToken: string,
|
githubToken: string,
|
||||||
): Promise<{ ruffDir: string; version: string }> {
|
): Promise<{ ruffDir: string; version: string }> {
|
||||||
const resolvedVersion = await determineVersion();
|
const resolvedVersion = await determineVersion();
|
||||||
|
const manifestUrl = manifestFile || undefined;
|
||||||
if (semver.lt(resolvedVersion, "v0.0.247")) {
|
if (semver.lt(resolvedVersion, "v0.0.247")) {
|
||||||
throw Error(
|
throw Error(
|
||||||
"This action does not support ruff versions older than 0.0.247",
|
"This action does not support ruff versions older than 0.0.247",
|
||||||
@@ -82,6 +84,7 @@ async function setupRuff(
|
|||||||
resolvedVersion,
|
resolvedVersion,
|
||||||
checkSum,
|
checkSum,
|
||||||
githubToken,
|
githubToken,
|
||||||
|
manifestUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -95,7 +98,7 @@ async function determineVersion(): Promise<string> {
|
|||||||
throw Error("It is not allowed to specify both version and version-file");
|
throw Error("It is not allowed to specify both version and version-file");
|
||||||
}
|
}
|
||||||
if (version !== "") {
|
if (version !== "") {
|
||||||
return await resolveVersion(version, githubToken);
|
return await resolveVersion(version, manifestFile || undefined);
|
||||||
}
|
}
|
||||||
if (versionFileInput !== "") {
|
if (versionFileInput !== "") {
|
||||||
const versionFromPyproject =
|
const versionFromPyproject =
|
||||||
@@ -105,7 +108,10 @@ async function determineVersion(): Promise<string> {
|
|||||||
`Could not parse version from ${versionFileInput}. Using latest version.`,
|
`Could not parse version from ${versionFileInput}. Using latest version.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return await resolveVersion(versionFromPyproject || "latest", githubToken);
|
return await resolveVersion(
|
||||||
|
versionFromPyproject || "latest",
|
||||||
|
manifestFile || undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const pyProjectPath = findPyprojectToml(
|
const pyProjectPath = findPyprojectToml(
|
||||||
src,
|
src,
|
||||||
@@ -113,7 +119,7 @@ async function determineVersion(): Promise<string> {
|
|||||||
);
|
);
|
||||||
if (!pyProjectPath) {
|
if (!pyProjectPath) {
|
||||||
core.info(`Could not find pyproject.toml. Using latest version.`);
|
core.info(`Could not find pyproject.toml. Using latest version.`);
|
||||||
return await resolveVersion("latest", githubToken);
|
return await resolveVersion("latest", manifestFile || undefined);
|
||||||
}
|
}
|
||||||
const versionFromPyproject =
|
const versionFromPyproject =
|
||||||
getRuffVersionFromRequirementsFile(pyProjectPath);
|
getRuffVersionFromRequirementsFile(pyProjectPath);
|
||||||
@@ -122,7 +128,10 @@ async function determineVersion(): Promise<string> {
|
|||||||
`Could not parse version from ${pyProjectPath}. Using latest version.`,
|
`Could not parse version from ${pyProjectPath}. Using latest version.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return await resolveVersion(versionFromPyproject || "latest", githubToken);
|
return await resolveVersion(
|
||||||
|
versionFromPyproject || "latest",
|
||||||
|
manifestFile || undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRuffToPath(cachedPath: string): void {
|
function addRuffToPath(cachedPath: string): void {
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
export const REPO = "ruff";
|
export const REPO = "ruff";
|
||||||
export const OWNER = "astral-sh";
|
export const OWNER = "astral-sh";
|
||||||
export const TOOL_CACHE_NAME = "ruff";
|
export const TOOL_CACHE_NAME = "ruff";
|
||||||
|
export const VERSIONS_MANIFEST_URL =
|
||||||
|
"https://raw.githubusercontent.com/astral-sh/versions/main/v1/ruff.ndjson";
|
||||||
|
export const GITHUB_RELEASES_PREFIX =
|
||||||
|
"https://github.com/astral-sh/ruff/releases/download/";
|
||||||
|
export const ASTRAL_MIRROR_PREFIX =
|
||||||
|
"https://releases.astral.sh/github/ruff/releases/download/";
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ProxyAgent, type RequestInit, fetch as undiciFetch } from "undici";
|
||||||
|
|
||||||
|
export function getProxyAgent() {
|
||||||
|
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy;
|
||||||
|
if (httpProxy) {
|
||||||
|
return new ProxyAgent(httpProxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
|
||||||
|
if (httpsProxy) {
|
||||||
|
return new ProxyAgent(httpsProxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetch = async (url: string, opts: RequestInit) =>
|
||||||
|
await undiciFetch(url, {
|
||||||
|
dispatcher: getProxyAgent(),
|
||||||
|
...opts,
|
||||||
|
});
|
||||||
@@ -6,3 +6,4 @@ export const githubToken = core.getInput("github-token");
|
|||||||
export const args = core.getInput("args");
|
export const args = core.getInput("args");
|
||||||
export const src = core.getInput("src");
|
export const src = core.getInput("src");
|
||||||
export const versionFile = core.getInput("version-file");
|
export const versionFile = core.getInput("version-file");
|
||||||
|
export const manifestFile = core.getInput("manifest-file");
|
||||||
|
|||||||
Reference in New Issue
Block a user