Skip to content

Commit adeb286

Browse files
authored
Add support for .tools-versions (#531)
Closes: #504
1 parent fce199e commit adeb286

File tree

7 files changed

+248
-2
lines changed

7 files changed

+248
-2
lines changed

.github/workflows/test.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,22 @@ jobs:
187187
exit 1
188188
fi
189189
190+
test-tool-versions-file-version:
191+
runs-on: ubuntu-latest
192+
steps:
193+
- uses: actions/checkout@v5
194+
- name: Install from .tools-versions file
195+
id: setup-uv
196+
uses: ./
197+
with:
198+
version-file: "__tests__/fixtures/.tool-versions"
199+
- name: Correct version gets installed
200+
run: |
201+
if [ "$(uv --version)" != "uv 0.5.15" ]; then
202+
echo "Wrong uv version: $(uv --version)"
203+
exit 1
204+
fi
205+
190206
test-checksum:
191207
runs-on: ${{ matrix.inputs.os }}
192208
strategy:
@@ -635,6 +651,7 @@ jobs:
635651
- test-uv-file-version
636652
- test-version-file-version
637653
- test-version-file-hash-version
654+
- test-tool-versions-file-version
638655
- test-checksum
639656
- test-with-explicit-token
640657
- test-uvx

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ You can use the `version-file` input to specify a file that contains the version
100100
This can either be a `pyproject.toml` or `uv.toml` file which defines a `required-version` or
101101
uv defined as a dependency in `pyproject.toml` or `requirements.txt`.
102102

103+
[asdf](https://asdf-vm.com/) `.tool-versions` is also supported, but without the `ref` syntax.
104+
103105
```yaml
104106
- name: Install uv based on the version defined in pyproject.toml
105107
uses: astral-sh/setup-uv@v6

__tests__/fixtures/.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uv 0.5.15
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
jest.mock("node:fs");
2+
jest.mock("@actions/core", () => ({
3+
warning: jest.fn(),
4+
}));
5+
6+
import fs from "node:fs";
7+
import * as core from "@actions/core";
8+
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
9+
import { getUvVersionFromToolVersions } from "../../src/version/tool-versions-file";
10+
11+
const mockedFs = fs as jest.Mocked<typeof fs>;
12+
const mockedCore = core as jest.Mocked<typeof core>;
13+
14+
describe("getUvVersionFromToolVersions", () => {
15+
beforeEach(() => {
16+
jest.clearAllMocks();
17+
});
18+
19+
it("should return undefined for non-.tool-versions files", () => {
20+
const result = getUvVersionFromToolVersions("package.json");
21+
expect(result).toBeUndefined();
22+
expect(mockedFs.readFileSync).not.toHaveBeenCalled();
23+
});
24+
25+
it("should return version for valid uv entry", () => {
26+
const fileContent = "python 3.11.0\nuv 0.1.0\nnodejs 18.0.0";
27+
mockedFs.readFileSync.mockReturnValue(fileContent);
28+
29+
const result = getUvVersionFromToolVersions(".tool-versions");
30+
31+
expect(result).toBe("0.1.0");
32+
expect(mockedFs.readFileSync).toHaveBeenCalledWith(
33+
".tool-versions",
34+
"utf8",
35+
);
36+
});
37+
38+
it("should return version for uv entry with v prefix", () => {
39+
const fileContent = "uv v0.2.0";
40+
mockedFs.readFileSync.mockReturnValue(fileContent);
41+
42+
const result = getUvVersionFromToolVersions(".tool-versions");
43+
44+
expect(result).toBe("0.2.0");
45+
});
46+
47+
it("should handle whitespace around uv entry", () => {
48+
const fileContent = " uv 0.3.0 ";
49+
mockedFs.readFileSync.mockReturnValue(fileContent);
50+
51+
const result = getUvVersionFromToolVersions(".tool-versions");
52+
53+
expect(result).toBe("0.3.0");
54+
});
55+
56+
it("should skip commented lines", () => {
57+
const fileContent = "# uv 0.1.0\npython 3.11.0\nuv 0.2.0";
58+
mockedFs.readFileSync.mockReturnValue(fileContent);
59+
60+
const result = getUvVersionFromToolVersions(".tool-versions");
61+
62+
expect(result).toBe("0.2.0");
63+
});
64+
65+
it("should return first matching uv version", () => {
66+
const fileContent = "uv 0.1.0\npython 3.11.0\nuv 0.2.0";
67+
mockedFs.readFileSync.mockReturnValue(fileContent);
68+
69+
const result = getUvVersionFromToolVersions(".tool-versions");
70+
71+
expect(result).toBe("0.1.0");
72+
});
73+
74+
it("should return undefined when no uv entry found", () => {
75+
const fileContent = "python 3.11.0\nnodejs 18.0.0";
76+
mockedFs.readFileSync.mockReturnValue(fileContent);
77+
78+
const result = getUvVersionFromToolVersions(".tool-versions");
79+
80+
expect(result).toBeUndefined();
81+
});
82+
83+
it("should return undefined for empty file", () => {
84+
mockedFs.readFileSync.mockReturnValue("");
85+
86+
const result = getUvVersionFromToolVersions(".tool-versions");
87+
88+
expect(result).toBeUndefined();
89+
});
90+
91+
it("should warn and return undefined for ref syntax", () => {
92+
const fileContent = "uv ref:main";
93+
mockedFs.readFileSync.mockReturnValue(fileContent);
94+
95+
const result = getUvVersionFromToolVersions(".tool-versions");
96+
97+
expect(result).toBeUndefined();
98+
expect(mockedCore.warning).toHaveBeenCalledWith(
99+
"The ref syntax of .tool-versions is not supported. Please use a released version instead.",
100+
);
101+
});
102+
103+
it("should handle file path with .tool-versions extension", () => {
104+
const fileContent = "uv 0.1.0";
105+
mockedFs.readFileSync.mockReturnValue(fileContent);
106+
107+
const result = getUvVersionFromToolVersions("path/to/.tool-versions");
108+
109+
expect(result).toBe("0.1.0");
110+
expect(mockedFs.readFileSync).toHaveBeenCalledWith(
111+
"path/to/.tool-versions",
112+
"utf8",
113+
);
114+
});
115+
});

dist/setup/index.js

Lines changed: 77 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/version/resolve.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from "node:fs";
22
import * as core from "@actions/core";
33
import { getRequiredVersionFromConfigFile } from "./config-file";
44
import { getUvVersionFromRequirementsFile } from "./requirements-file";
5+
import { getUvVersionFromToolVersions } from "./tool-versions-file";
56

67
export function getUvVersionFromFile(filePath: string): string | undefined {
78
core.info(`Trying to find version for uv in: ${filePath}`);
@@ -11,7 +12,10 @@ export function getUvVersionFromFile(filePath: string): string | undefined {
1112
}
1213
let uvVersion: string | undefined;
1314
try {
14-
uvVersion = getRequiredVersionFromConfigFile(filePath);
15+
uvVersion = getUvVersionFromToolVersions(filePath);
16+
if (uvVersion === undefined) {
17+
uvVersion = getRequiredVersionFromConfigFile(filePath);
18+
}
1519
if (uvVersion === undefined) {
1620
uvVersion = getUvVersionFromRequirementsFile(filePath);
1721
}

src/version/tool-versions-file.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import fs from "node:fs";
2+
import * as core from "@actions/core";
3+
4+
export function getUvVersionFromToolVersions(
5+
filePath: string,
6+
): string | undefined {
7+
if (!filePath.endsWith(".tool-versions")) {
8+
return undefined;
9+
}
10+
const fileContents = fs.readFileSync(filePath, "utf8");
11+
const lines = fileContents.split("\n");
12+
13+
for (const line of lines) {
14+
// Skip commented lines
15+
if (line.trim().startsWith("#")) {
16+
continue;
17+
}
18+
const match = line.match(/^\s*uv\s*v?\s*(?<version>[^\s]+)\s*$/);
19+
if (match) {
20+
const matchedVersion = match.groups?.version.trim();
21+
if (matchedVersion?.startsWith("ref")) {
22+
core.warning(
23+
"The ref syntax of .tool-versions is not supported. Please use a released version instead.",
24+
);
25+
return undefined;
26+
}
27+
return matchedVersion;
28+
}
29+
}
30+
return undefined;
31+
}

0 commit comments

Comments
 (0)