Skip to content

Commit 7702017

Browse files
authored
feat: improve version detection (#41)
1 parent db0d1fa commit 7702017

File tree

20 files changed

+350
-417
lines changed

20 files changed

+350
-417
lines changed

.github/workflows/test.yaml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ on:
66
pull_request:
77
workflow_dispatch:
88

9+
env:
10+
# This version is used when running the tests to ensure that the correct version is installed.
11+
# It is the version that the test expect to find during automatic version detection. It is important
12+
# that this version is NOT the latest version of the CLI because some tests may still pass if the
13+
# automatic version detection fails and falls back to the latest version.
14+
BIOME_EXPECTED_VERSION: 1.5.0
15+
916
jobs:
1017
test-specific:
1118
name: Specific version
@@ -56,7 +63,7 @@ jobs:
5663
- name: Check equality
5764
shell: bash
5865
run: |
59-
if [ "Version: 1.5.1" == "${{ steps.version.outputs.version }}" ]; then
66+
if [ "Version: ${{ env.BIOME_EXPECTED_VERSION }}" == "${{ steps.version.outputs.version }}" ]; then
6067
exit 0
6168
else
6269
echo "Versions do not match"
@@ -84,7 +91,7 @@ jobs:
8491
- name: Check equality
8592
shell: bash
8693
run: |
87-
if [ "Version: 1.5.1" == "${{ steps.version.outputs.version }}" ]; then
94+
if [ "Version: ${{ env.BIOME_EXPECTED_VERSION }}" == "${{ steps.version.outputs.version }}" ]; then
8895
exit 0
8996
else
9097
echo "Versions do not match"
@@ -112,7 +119,7 @@ jobs:
112119
- name: Check equality
113120
shell: bash
114121
run: |
115-
if [ "Version: 1.5.1" == "${{ steps.version.outputs.version }}" ]; then
122+
if [ "Version: ${{ env.BIOME_EXPECTED_VERSION }}" == "${{ steps.version.outputs.version }}" ]; then
116123
exit 0
117124
else
118125
echo "Versions do not match"
@@ -140,7 +147,7 @@ jobs:
140147
- name: Check equality
141148
shell: bash
142149
run: |
143-
if [ "Version: 1.5.1" == "${{ steps.version.outputs.version }}" ]; then
150+
if [ "Version: ${{ env.BIOME_EXPECTED_VERSION }}" == "${{ steps.version.outputs.version }}" ]; then
144151
exit 0
145152
else
146153
echo "Versions do not match"
@@ -159,6 +166,8 @@ jobs:
159166
uses: actions/checkout@v4
160167
- name: Setup Biome CLI
161168
uses: ./
169+
with:
170+
working-dir: "test/fixtures/fallback"
162171
- name: Retrieve the version
163172
id: version
164173
shell: bash

README.md

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,8 @@ The following inputs are supported.
4141
4242
To automatically determine the version of Biome to install based on the project's dependencies, you can simply omit the `version` input.
4343

44-
The action will search for the version of the `@biomejs/biome` dependency in the lockfiles of popular package managers such as npm, yarn, pnpm, and bun. It will then install that specific version of the Biome CLI.
45-
46-
> [!IMPORTANT]
47-
> <img src="https://bun.sh/logo.svg" width="16"> [Bun](https://bun.sh) users must configure Bun to output a yarn lockfile because this action cannot yet read bun's binary lockfile format.
48-
> An easy way to do this is to add the following to your `bunfig.toml` file:
49-
> ```toml
50-
> [install.lockfile]
51-
> print = "yarn"
52-
53-
If no version of the Biome CLI is found in the lockfiles, the action will install the latest version of the Biome CLI.
44+
The action will look for the version of the `@biomejs/biome` dependency in the lockfiles of popular package managers such as npm, yarn, pnpm, and bun. If the version cannot be found in the lockfiles, the action will attempt to retrieve the version from the `package.json` file, and as a last
45+
resort, it will install the latest version of the Biome CLI.
5446

5547
```yaml
5648
- name: Setup Biome CLI
@@ -60,6 +52,9 @@ If no version of the Biome CLI is found in the lockfiles, the action will instal
6052
run: biome ci .
6153
```
6254

55+
> [!IMPORTANT]
56+
> We recommend that you *pin* the version of `@biomejs/biome` in your project's dependencies. If you provide a semver range, and automatic version detection falls back to reading the `package.json file`, the highest version within the range will be used. See the [versioning documentation](https://biomejs.dev/internals/versioning/) for more information.
57+
6358
### Latest version
6459

6560
Setup the latest version of the Biome CLI.

dist/index.mjs

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22447,7 +22447,7 @@ const getInput = (name) => {
2244722447
return (0,core.getInput)(name) === "" ? void 0 : (0,core.getInput)(name);
2244822448
};
2244922449

22450-
const getBiomeVersion = async () => {
22450+
const getBiomeVersion = async (octokit) => {
2245122451
let root = getInput("working-dir");
2245222452
if (!root) {
2245322453
root = process.cwd();
@@ -22461,7 +22461,7 @@ const getBiomeVersion = async () => {
2246122461
"The specified working directory does not exist. Using the current working directory instead."
2246222462
);
2246322463
}
22464-
return getInput("version") ?? await extractVersionFromNpmLockFile(root) ?? await extractVersionFromPnpmLockFile(root) ?? await extractVersionFromYarnLockFile(root) ?? "latest";
22464+
return getInput("version") ?? await extractVersionFromNpmLockFile(root) ?? await extractVersionFromPnpmLockFile(root) ?? await extractVersionFromYarnLockFile(root) ?? await extractVersionFromPackageManifest(root, octokit) ?? "latest";
2246522465
};
2246622466
const extractVersionFromNpmLockFile = async (root) => {
2246722467
try {
@@ -22496,15 +22496,59 @@ const extractVersionFromYarnLockFile = async (root) => {
2249622496
return void 0;
2249722497
}
2249822498
};
22499+
const extractVersionFromPackageManifest = async (root, octokit) => {
22500+
try {
22501+
const manifest = JSON.parse(
22502+
await (0,promises_namespaceObject.readFile)((0,external_node_path_namespaceObject.join)(root, "package.json"), "utf8")
22503+
);
22504+
const versionSpecifier = manifest.devDependencies?.["@biomejs/biome"] ?? manifest.dependencies?.["@biomejs/biome"];
22505+
if (!versionSpecifier) {
22506+
return void 0;
22507+
}
22508+
if ((0,semver.valid)(versionSpecifier)) {
22509+
return versionSpecifier;
22510+
}
22511+
if ((0,semver.validRange)(versionSpecifier)) {
22512+
(0,core.warning)(
22513+
`Please consider pinning the version of @biomejs/biome in your package.json file.
22514+
See https://biomejs.dev/internals/versioning/ for more information.`,
22515+
{ title: "Biome version range detected" }
22516+
);
22517+
const versions = await fetchBiomeVersions(octokit);
22518+
if (!versions) {
22519+
return void 0;
22520+
}
22521+
return (0,semver.maxSatisfying)(versions, versionSpecifier)?.version ?? void 0;
22522+
}
22523+
} catch {
22524+
return void 0;
22525+
}
22526+
};
22527+
const fetchBiomeVersions = async (octokit) => {
22528+
try {
22529+
const releases = await octokit.paginate(
22530+
"GET /repos/{owner}/{repo}/releases",
22531+
{
22532+
owner: "biomejs",
22533+
repo: "biome"
22534+
}
22535+
);
22536+
const versions = releases.filter((release) => release.tag_name.startsWith("cli/")).map((release) => (0,semver.coerce)(release.tag_name));
22537+
return (0,semver.rsort)(versions);
22538+
} catch {
22539+
return void 0;
22540+
}
22541+
};
2249922542

2250022543
(async () => {
22544+
const octokit = new rest_dist_node.Octokit({
22545+
auth: (await (0,dist_node.createActionAuth)()()).token
22546+
});
2250122547
await setup({
22502-
version: await getBiomeVersion(),
22548+
version: await getBiomeVersion(octokit),
2250322549
platform: process.platform,
2250422550
architecture: process.arch,
22505-
octokit: new rest_dist_node.Octokit({
22506-
auth: (await (0,dist_node.createActionAuth)()()).token
22507-
})
22551+
octokit
2250822552
});
2250922553
})();
2251022554

src/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { setup } from "./setup";
44
import { getBiomeVersion } from "./version";
55

66
(async () => {
7+
const octokit = new Octokit({
8+
auth: (await createActionAuth()()).token,
9+
});
10+
711
await setup({
8-
version: await getBiomeVersion(),
12+
version: await getBiomeVersion(octokit),
913
platform: process.platform as "linux" | "darwin" | "win32",
1014
architecture: process.arch as "x64" | "arm64",
11-
octokit: new Octokit({
12-
auth: (await createActionAuth()()).token,
13-
}),
15+
octokit: octokit,
1416
});
1517
})();

src/version.ts

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import { existsSync } from "node:fs";
22
import { join } from "node:path";
33
import { info, warning } from "@actions/core";
4+
import { Octokit } from "@octokit/rest";
45
import { readFile } from "fs/promises";
6+
import {
7+
SemVer,
8+
coerce,
9+
maxSatisfying,
10+
rsort,
11+
valid,
12+
validRange,
13+
} from "semver";
514
import { parse } from "yaml";
615
import { getInput } from "./helpers";
716

@@ -16,7 +25,7 @@ import { getInput } from "./helpers";
1625
*
1726
* @param projectRoot The root directory of the project. Defaults to the current working directory.
1827
*/
19-
export const getBiomeVersion = async (): Promise<string> => {
28+
export const getBiomeVersion = async (octokit: Octokit): Promise<string> => {
2029
let root = getInput("working-dir");
2130

2231
// If the working directory is not specified, we fallback to the current
@@ -42,6 +51,7 @@ export const getBiomeVersion = async (): Promise<string> => {
4251
(await extractVersionFromNpmLockFile(root)) ??
4352
(await extractVersionFromPnpmLockFile(root)) ??
4453
(await extractVersionFromYarnLockFile(root)) ??
54+
(await extractVersionFromPackageManifest(root, octokit)) ??
4555
"latest"
4656
);
4757
};
@@ -102,3 +112,89 @@ const extractVersionFromYarnLockFile = async (
102112
return undefined;
103113
}
104114
};
115+
116+
/**
117+
* Extracts the Biome CLI version from the project's package.json file.
118+
*
119+
* This function attempts to extract the version of the `@biomejs/biome`
120+
* package from the `package.json` file. If the package is not installed,
121+
* or the version cannot be extracted, this function will return undefined.
122+
*
123+
* If the version is specified as a range, this function will return the
124+
* highest available version that satisfies the range, if it exists, or
125+
* undefined otherwise.
126+
*/
127+
const extractVersionFromPackageManifest = async (
128+
root: string,
129+
octokit: Octokit,
130+
): Promise<string | undefined> => {
131+
try {
132+
const manifest = JSON.parse(
133+
await readFile(join(root, "package.json"), "utf8"),
134+
);
135+
136+
// The package should be installed as a devDependency, but we'll check
137+
// both dependencies and devDependencies just in case.
138+
const versionSpecifier =
139+
manifest.devDependencies?.["@biomejs/biome"] ??
140+
manifest.dependencies?.["@biomejs/biome"];
141+
142+
// Biome is not a dependency of the project.
143+
if (!versionSpecifier) {
144+
return undefined;
145+
}
146+
147+
// If the version is specific, we return it directly.
148+
if (valid(versionSpecifier)) {
149+
return versionSpecifier;
150+
}
151+
152+
// If the version is a range, return the highest available version.
153+
if (validRange(versionSpecifier)) {
154+
warning(
155+
`Please consider pinning the version of @biomejs/biome in your package.json file.
156+
See https://biomejs.dev/internals/versioning/ for more information.`,
157+
{ title: "Biome version range detected" },
158+
);
159+
160+
const versions = await fetchBiomeVersions(octokit);
161+
162+
if (!versions) {
163+
return undefined;
164+
}
165+
166+
return maxSatisfying(versions, versionSpecifier)?.version ?? undefined;
167+
}
168+
} catch {
169+
return undefined;
170+
}
171+
};
172+
173+
/**
174+
* Fetches the available versions of the Biome CLI from GitHub.
175+
*
176+
* This function will return the versions of the Biome CLI that are available
177+
* on GitHub. This includes all versions that have been released, including
178+
* pre-releases and draft releases.
179+
*/
180+
const fetchBiomeVersions = async (
181+
octokit: Octokit,
182+
): Promise<SemVer[] | undefined> => {
183+
try {
184+
const releases = await octokit.paginate(
185+
"GET /repos/{owner}/{repo}/releases",
186+
{
187+
owner: "biomejs",
188+
repo: "biome",
189+
},
190+
);
191+
192+
const versions = releases
193+
.filter((release) => release.tag_name.startsWith("cli/"))
194+
.map((release) => coerce(release.tag_name));
195+
196+
return rsort(versions as SemVer[]);
197+
} catch {
198+
return undefined;
199+
}
200+
};

0 commit comments

Comments
 (0)