Add manifest-file input (#352)

This commit is contained in:
Kevin Stillhammer
2026-04-11 19:14:05 +02:00
committed by GitHub
parent 535554df96
commit 9b8caf6c41
16 changed files with 2129 additions and 4213 deletions
+26
View File
@@ -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
+31 -11
View File
@@ -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"}]}
+509
View File
@@ -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();
});
});
});
+196
View File
@@ -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
View File
@@ -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:
Generated Vendored
+891 -4090
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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(
+243
View File
@@ -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;
}
+39
View File
@@ -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
View File
@@ -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 {
+6
View File
@@ -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/";
+21
View File
@@ -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,
});
+1
View File
@@ -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");