feat: implement wildcard resolution into the action (#93)

* feat: remove retry attempts

* [autofix.ci] apply automated fixes

* feat: get download url from github's api

* [autofix.ci] apply automated fixes

* fix: add token property to action definition & fix satisfies params

* [autofix.ci] apply automated fixes

* fix: getPlatform, getArchitecture + eversion

* [autofix.ci] apply automated fixes

* fix: duplicate v

* [autofix.ci] apply automated fixes

* fix: check if valid semver and add bun-v

* [autofix.ci] apply automated fixes

* refactor: wrap validation

* [autofix.ci] apply automated fixes

* ci(format): use bun

bun install is rqeuired for patches

* ci(format): use bun

bun install is rqeuired for patches

* [autofix.ci] apply automated fixes

* feat: bring back support for sha downloads

* [autofix.ci] apply automated fixes

* fix: add bearer prefix for token

* [autofix.ci] apply automated fixes

* fix: proper error when artifact is not found

* [autofix.ci] apply automated fixes

* conflicts

* autofix build

* fix

* fix

* fix

* fix

* fix

* autofix build

* fix

* [autofix.ci] apply automated fixes

* fix

* [autofix.ci] apply automated fixes

* fix

* [autofix.ci] apply automated fixes

* fix

* fix

* [autofix.ci] apply automated fixes

* fix

* fix

* [autofix.ci] apply automated fixes

* fix: drop sha support for now

* [autofix.ci] apply automated fixes

* fix: filter tags

* [autofix.ci] apply automated fixes

* docs: token

* docs: token

* docs: token

* refactor: cleanup

* [autofix.ci] apply automated fixes

* refactor: cleanup

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Jozef Steinhübl
2026-01-05 00:03:34 +01:00
committed by GitHub
parent b7a1c7ccf2
commit 8c296f9cb7
13 changed files with 389 additions and 164 deletions

View File

@@ -1,28 +1,86 @@
name: ⚖️ Compare Bun Version
description: Compare the version of Bun to a specified version
description: Compare the installed Bun version against a version specification.
inputs:
bun-version:
description: The version of Bun to compare against
description: The version spec to compare against (e.g., '1.1.0', 'canary', '>1.2.0', '1.x').
required: true
default: "1.1.0"
runs:
using: composite
steps:
- name: 🛠️ Get installed Bun version
- name: 🛠️ Get installed Bun version and revision
id: bun
shell: bash
run: |
bun --version
echo "version=$(bun --version)" >> $GITHUB_OUTPUT
echo "version=$(bun --version | tr -d '\r\n')" >> $GITHUB_OUTPUT
echo "revision=$(bun --revision 2>/dev/null || true)" >> $GITHUB_OUTPUT
- name: ⚖️ Compare versions
shell: bash
env:
REQUESTED_SPEC: ${{ inputs.bun-version }}
ACTUAL_VERSION: ${{ steps.bun.outputs.version }}
ACTUAL_REVISION: ${{ steps.bun.outputs.revision }}
run: |
if [[ "${{ steps.bun.outputs.version }}" == "${{ inputs.bun-version }}" ]]; then
echo "Version is ${{ inputs.bun-version }}"
else
echo "Expected version to be ${{ inputs.bun-version }}, got ${{ steps.bun.outputs.version }}"
exit 1
set -euo pipefail
# Function to compare two semantic versions (e.g., version_compare 1.2.3 1.10.0)
# Returns: 0 if v1 == v2, 1 if v1 > v2, 2 if v1 < v2
version_compare() {
if [[ "$1" == "$2" ]]; then return 0; fi
local lowest=$(printf '%s\n' "$1" "$2" | sort -V | head -n1)
if [[ "$1" == "$lowest" ]]; then return 2; else return 1; fi
}
echo "Requested spec: ${REQUESTED_SPEC}"
echo "Actual version: ${ACTUAL_VERSION}"
# Case 1: 'latest' - always passes
if [[ "${REQUESTED_SPEC}" == "latest" ]]; then
echo "OK: Skipping explicit version check for 'latest'."
exit 0
fi
# Case 2: 'canary' - check for 'canary' in revision or version string
if [[ "${REQUESTED_SPEC}" == "canary" ]]; then
if [[ "${ACTUAL_REVISION}" == *canary* ]] || [[ "${ACTUAL_VERSION}" == *canary* ]]; then
echo "OK: Detected canary build (version: ${ACTUAL_VERSION}, revision: ${ACTUAL_REVISION:-n/a})."
exit 0
else
echo "Error: Expected a canary build, but got ${ACTUAL_VERSION} (revision: ${ACTUAL_REVISION:-n/a})."
exit 1
fi
fi
# Case 3: Semver ranges (e.g., >1.0.0, <2, 1.x, 1.1.0)
op_part=$(echo "${REQUESTED_SPEC}" | sed -E 's/^([><=]*).*/\1/')
version_part=$(echo "${REQUESTED_SPEC}" | sed -E 's/^[><= ]*//')
op="${op_part:-==}"
version_base="${version_part//.x/}"
# Handle wildcards like '1.x' or '1'
if [[ "${version_part}" == *.x* ]] || { [[ ! "${version_part}" == *.* ]] && [[ "${op}" == "==" ]]; }; then
if [[ "${ACTUAL_VERSION}" == "${version_base}" || "${ACTUAL_VERSION}" == "${version_base}".* ]]; then
echo "OK: Version ${ACTUAL_VERSION} matches wildcard spec '${REQUESTED_SPEC}'."
exit 0
else
echo "Error: Version ${ACTUAL_VERSION} does not match wildcard spec '${REQUESTED_SPEC}'."
exit 1
fi
fi
# Perform comparison for >, <, >=, <=, ==
version_compare "${ACTUAL_VERSION}" "${version_part}" && result=0 || result=$?
case "${op}" in
'==') if [[ ${result} -ne 0 ]]; then echo "Error: Expected version ${version_part}, but got ${ACTUAL_VERSION}." && exit 1; fi ;;
'>') if [[ ${result} -ne 1 ]]; then echo "Error: Expected version > ${version_part}, but got ${ACTUAL_VERSION}." && exit 1; fi ;;
'<') if [[ ${result} -ne 2 ]]; then echo "Error: Expected version < ${version_part}, but got ${ACTUAL_VERSION}." && exit 1; fi ;;
'>=') if [[ ${result} -eq 2 ]]; then echo "Error: Expected version >= ${version_part}, but got ${ACTUAL_VERSION}." && exit 1; fi ;;
'<=') if [[ ${result} -eq 1 ]]; then echo "Error: Expected version <= ${version_part}, but got ${ACTUAL_VERSION}." && exit 1; fi ;;
*) echo "Error: Unsupported operator '${op}' in spec '${REQUESTED_SPEC}'." && exit 1 ;;
esac
echo "OK: Version ${ACTUAL_VERSION} satisfies spec '${REQUESTED_SPEC}'."

View File

@@ -54,6 +54,10 @@ jobs:
- latest
- canary
- "1.1.0"
- "1.x"
- "1"
- "> 1.0.0"
- "< 2"
# https://github.com/oven-sh/setup-bun/issues/37
# - "1.x"
# - "1"
@@ -78,6 +82,11 @@ jobs:
run: |
bun --version
- name: ⚖️ Verify Bun version
uses: ./.github/actions/compare-bun-version
with:
bun-version: ${{ matrix.bun-version }}
setup-bun-from-file:
name: setup-bun from (${{ matrix.os }}, ${{ matrix.file.name }})
runs-on: ${{ matrix.os }}

View File

@@ -66,14 +66,15 @@ If you need to override the download URL, you can use the `bun-download-url` inp
## Inputs
| Name | Description | Default | Examples |
| ------------------ | ----------------------------------------------------- | ----------- | ------------------------------------------------ |
| `bun-version` | The version of Bun to download and install. | `latest` | `canary`, `1.0.0` |
| `bun-version-file` | The version of Bun to download and install from file. | `undefined` | `package.json`, `.bun-version`, `.tool-versions` |
| `bun-download-url` | URL to download .zip file for Bun release | | |
| `registry-url` | Registry URL where some private package is stored. | `undefined` | `"https://npm.pkg.github.com/"` |
| `scope` | Scope for private packages. | `undefined` | `"@foo"`, `"@orgname"` |
| `no-cache` | Disable caching of the downloaded executable. | `false` | `true`, `false` |
| Name | Description | Default | Examples |
| ------------------ | --------------------------------------------------------------------------------- | --------------------- | ------------------------------------------------ |
| `bun-version` | The version of Bun to download and install. | `latest` | `canary`, `1.0.0`, `1.0.x` |
| `bun-version-file` | The version of Bun to download and install from file. | `undefined` | `package.json`, `.bun-version`, `.tool-versions` |
| `bun-download-url` | URL to download .zip file for Bun release | | |
| `registry-url` | Registry URL where some private package is stored. | `undefined` | `"https://npm.pkg.github.com/"` |
| `scope` | Scope for private packages. | `undefined` | `"@foo"`, `"@orgname"` |
| `no-cache` | Disable caching of the downloaded executable. | `false` | `true`, `false` |
| `token` | Personal access token (PAT) used to fetch tags from the `oven-sh/bun` repository. | `${{ github.token }}` | `${{ secrets.GITHUB_TOKEN }}` |
## Outputs

View File

@@ -39,6 +39,10 @@ inputs:
type: boolean
default: false
description: Disable caching of bun executable.
token:
required: false
default: ${{ github.token }}
description: Personal access token (PAT) used to fetch tags from oven-sh/bun repository. Recommended for resolving wildcard/range versions to avoid GitHub API rate limiting.
outputs:
bun-version:

View File

@@ -11,6 +11,7 @@
"@actions/io": "^1.1.2",
"@actions/tool-cache": "^2.0.1",
"@iarna/toml": "^2.2.5",
"compare-versions": "^6.1.1",
},
"devDependencies": {
"@types/bun": "^1.1.13",
@@ -21,6 +22,9 @@
},
},
},
"patchedDependencies": {
"compare-versions@6.1.1": "patches/compare-versions@6.1.1.patch",
},
"packages": {
"@actions/cache": ["@actions/cache@4.0.3", "", { "dependencies": { "@actions/core": "^1.11.1", "@actions/exec": "^1.0.1", "@actions/glob": "^0.1.0", "@actions/http-client": "^2.1.1", "@actions/io": "^1.0.1", "@azure/abort-controller": "^1.1.0", "@azure/ms-rest-js": "^2.6.0", "@azure/storage-blob": "^12.13.0", "@protobuf-ts/plugin": "^2.9.4", "semver": "^6.3.1" } }, "sha512-SvrqFtYJ7I48A/uXNkoJrnukx5weQv1fGquhs3+4nkByZThBH109KTIqj5x/cGV7JGNvb8dLPVywUOqX1fjiXg=="],
@@ -144,6 +148,8 @@
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],

219
dist/setup/index.js generated vendored

File diff suppressed because one or more lines are too long

5
package-lock.json generated
View File

@@ -461,6 +461,11 @@
"node": ">= 0.8"
}
},
"node_modules/compare-versions": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
"integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="
},
"node_modules/concat-map": {
"version": "0.0.1",
"license": "MIT"

View File

@@ -28,7 +28,8 @@
"@actions/glob": "^0.4.0",
"@actions/io": "^1.1.2",
"@actions/tool-cache": "^2.0.1",
"@iarna/toml": "^2.2.5"
"@iarna/toml": "^2.2.5",
"compare-versions": "^6.1.1"
},
"devDependencies": {
"@types/bun": "^1.1.13",
@@ -36,5 +37,8 @@
"esbuild": "^0.19.2",
"prettier": "^3.4.2",
"typescript": "^4.9.5"
},
"patchedDependencies": {
"compare-versions@6.1.1": "patches/compare-versions@6.1.1.patch"
}
}

View File

@@ -0,0 +1,67 @@
diff --git a/lib/esm/satisfies.js b/lib/esm/satisfies.js
index 7586b71657332f855431c4dd4f05e9394fd9aac3..a6ec29bfc98907c67ed4af71fca73bd8bff88798 100644
--- a/lib/esm/satisfies.js
+++ b/lib/esm/satisfies.js
@@ -40,8 +40,9 @@ export const satisfies = (version, range) => {
// else range of either "~" or "^" is assumed
const [v1, v2, v3, , vp] = validateAndParse(version);
const [r1, r2, r3, , rp] = validateAndParse(range);
- const v = [v1, v2, v3];
+ const v = [v1, v2 !== null && v2 !== void 0 ? v2 : 'x', v3 !== null && v3 !== void 0 ? v3 : 'x'];
const r = [r1, r2 !== null && r2 !== void 0 ? r2 : 'x', r3 !== null && r3 !== void 0 ? r3 : 'x'];
+
// validate pre-release
if (rp) {
if (!vp)
diff --git a/lib/esm/utils.js b/lib/esm/utils.js
index b5cc8b9927ab38fc67032c133b531e95ec4cec15..ec56105fd2d806aa922f1488a27b02c56aff1865 100644
--- a/lib/esm/utils.js
+++ b/lib/esm/utils.js
@@ -28,7 +28,7 @@ const compareStrings = (a, b) => {
};
export const compareSegments = (a, b) => {
for (let i = 0; i < Math.max(a.length, b.length); i++) {
- const r = compareStrings(a[i] || '0', b[i] || '0');
+ const r = compareStrings(a[i] || 'x', b[i] || 'x');
if (r !== 0)
return r;
}
diff --git a/lib/umd/index.js b/lib/umd/index.js
index 2cfef261bca520e21ed41fc14950732b8aa6339b..1059784db86635f3aaaba83b5a72c5015e1d8490 100644
--- a/lib/umd/index.js
+++ b/lib/umd/index.js
@@ -152,7 +152,7 @@
// else range of either "~" or "^" is assumed
const [v1, v2, v3, , vp] = validateAndParse(version);
const [r1, r2, r3, , rp] = validateAndParse(range);
- const v = [v1, v2, v3];
+ const v = [v1, v2 !== null && v2 !== void 0 ? v2 : 'x', v3 !== null && v3 !== void 0 ? v3 : 'x'];
const r = [r1, r2 !== null && r2 !== void 0 ? r2 : 'x', r3 !== null && r3 !== void 0 ? r3 : 'x'];
// validate pre-release
if (rp) {
diff --git a/package.json b/package.json
index b05b3daf706d7ba4e594233f8791fc3007a8e2cd..e51e76b86f95e9ebf0b5dba3b82aeb119628528d 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,7 @@
"prepublishOnly": "npm run build",
"test": "c8 --reporter=lcov mocha"
},
- "main": "./lib/umd/index.js",
+ "main": "./lib/src/index.ts",
"module": "./lib/esm/index.js",
"types": "./lib/esm/index.d.ts",
"sideEffects": false,
diff --git a/src/satisfies.ts b/src/satisfies.ts
index 66cb171d7f32e68fdda6929d2da223b97a053737..6b4973f037843f264338a01efdc4ace5dcf042cd 100644
--- a/src/satisfies.ts
+++ b/src/satisfies.ts
@@ -43,7 +43,7 @@ export const satisfies = (version: string, range: string): boolean => {
// else range of either "~" or "^" is assumed
const [v1, v2, v3, , vp] = validateAndParse(version);
const [r1, r2, r3, , rp] = validateAndParse(range);
- const v = [v1, v2, v3];
+ const v = [v1, v2 ?? 'x', v3 ?? 'x'];
const r = [r1, r2 ?? 'x', r3 ?? 'x'];
// validate pre-release

View File

@@ -13,9 +13,11 @@ import { addPath, info, warning } from "@actions/core";
import { isFeatureAvailable, restoreCache } from "@actions/cache";
import { downloadTool, extractZip } from "@actions/tool-cache";
import { getExecOutput } from "@actions/exec";
import { writeBunfig, Registry } from "./bunfig";
import { Registry } from "./registry";
import { writeBunfig } from "./bunfig";
import { saveState } from "@actions/core";
import { addExtension, retry } from "./utils";
import { addExtension } from "./utils";
import { getDownloadUrl } from "./download-url";
import { cwd } from "node:process";
export type Input = {
@@ -27,6 +29,7 @@ export type Input = {
profile?: boolean;
registries?: Registry[];
noCache?: boolean;
token?: string;
};
export type Output = {
@@ -48,7 +51,7 @@ export default async (options: Input): Promise<Output> => {
const bunfigPath = join(cwd(), "bunfig.toml");
writeBunfig(bunfigPath, options.registries);
const url = getDownloadUrl(options);
const url = await getDownloadUrl(options);
const cacheEnabled = isCacheEnabled(options);
const binPath = join(homedir(), ".bun", "bin");
@@ -105,8 +108,7 @@ export default async (options: Input): Promise<Output> => {
if (!cacheHit) {
info(`Downloading a new version of Bun: ${url}`);
// TODO: remove this, temporary fix for https://github.com/oven-sh/setup-bun/issues/73
revision = await retry(async () => await downloadBun(url, bunPath), 3);
revision = await downloadBun(url, bunPath);
}
}
@@ -192,24 +194,6 @@ function isCacheEnabled(options: Input): boolean {
return isFeatureAvailable();
}
function getDownloadUrl(options: Input): string {
const { customUrl } = options;
if (customUrl) {
return customUrl;
}
const { version, os, arch, avx2, profile } = options;
const eversion = encodeURIComponent(version ?? "latest");
const eos = encodeURIComponent(os ?? process.platform);
const earch = encodeURIComponent(arch ?? process.arch);
const eavx2 = encodeURIComponent(avx2 ?? true);
const eprofile = encodeURIComponent(profile ?? false);
const { href } = new URL(
`${eversion}/${eos}/${earch}?avx2=${eavx2}&profile=${eprofile}`,
"https://bun.sh/download/",
);
return href;
}
async function extractBun(path: string): Promise<string> {
for (const entry of readdirSync(path, { withFileTypes: true })) {
const { name } = entry;

63
src/download-url.ts Normal file
View File

@@ -0,0 +1,63 @@
import { compareVersions, satisfies, validate } from "compare-versions";
import { Input } from "./action";
import { getArchitecture, getPlatform, request } from "./utils";
export async function getDownloadUrl(options: Input): Promise<string> {
const { customUrl } = options;
if (customUrl) {
return customUrl;
}
return await getSemverDownloadUrl(options);
}
async function getSemverDownloadUrl(options: Input): Promise<string> {
const res = (await (
await request("https://api.github.com/repos/oven-sh/bun/git/refs/tags", {
headers: options.token
? { "Authorization": `Bearer ${options.token}` }
: {},
})
).json()) as { ref: string }[];
let tags = res
.filter(
(tag) =>
tag.ref.startsWith("refs/tags/bun-v") || tag.ref === "refs/tags/canary",
)
.map((item) => item.ref.replace(/refs\/tags\/(bun-v)?/g, ""))
.filter(Boolean);
const { version, os, arch, avx2, profile } = options;
let tag = tags.find((t) => t === version);
if (!tag) {
tags = tags.filter((t) => validate(t)).sort(compareVersions);
const matchedTag =
version === "latest" || !version
? tags.at(-1)
: tags.filter((t) => satisfies(t, version)).at(-1);
if (!matchedTag) {
throw new Error(`No Bun release found matching version '${version}'`);
}
tag = `bun-v${matchedTag}`;
} else if (validate(tag)) {
tag = `bun-v${tag}`;
}
const eversion = encodeURIComponent(tag ?? version);
const eos = encodeURIComponent(os ?? getPlatform());
const earch = encodeURIComponent(arch ?? getArchitecture());
const eavx2 = encodeURIComponent(avx2 === false ? "-baseline" : "");
const eprofile = encodeURIComponent(profile === true ? "-profile" : "");
const { href } = new URL(
`${eversion}/bun-${eos}-${earch}${eavx2}${eprofile}.zip`,
"https://github.com/oven-sh/bun/releases/download/",
);
return href;
}

View File

@@ -30,6 +30,7 @@ runAction({
customUrl: getInput("bun-download-url") || undefined,
registries: registries,
noCache: getBooleanInput("no-cache") || false,
token: getInput("token"),
})
.then(({ version, revision, bunPath, url, cacheHit }) => {
setOutput("bun-version", version);

View File

@@ -3,19 +3,27 @@ import { info } from "node:console";
import { existsSync, readFileSync, renameSync } from "node:fs";
import { resolve, basename } from "node:path";
export function retry<T>(
fn: () => Promise<T>,
retries: number,
timeout = 10000,
): Promise<T> {
return fn().catch((err) => {
if (retries <= 0) {
throw err;
}
return new Promise((resolve) => setTimeout(resolve, timeout)).then(() =>
retry(fn, retries - 1, timeout),
);
export async function request(
url: string,
init?: RequestInit,
): Promise<Response> {
const headers = new Headers(init?.headers);
if (!headers.has("User-Agent")) {
headers.set("User-Agent", "@oven-sh/setup-bun");
}
const res = await fetch(url, {
...init,
headers,
});
if (!res.ok) {
const body = await res.text().catch(() => "");
throw new Error(
`Failed to fetch url ${url}. (status code: ${res.status}, status text: ${res.statusText})${body ? `\n${body}` : ""}`,
);
}
return res;
}
export function addExtension(path: string, ext: string): string {
@@ -27,6 +35,20 @@ export function addExtension(path: string, ext: string): string {
return path;
}
export function getPlatform(): string {
const platform = process.platform;
if (platform === "win32") return "windows";
return platform;
}
export function getArchitecture(): string {
const arch = process.arch;
if (arch === "arm64") return "aarch64";
return arch;
}
const FILE_VERSION_READERS = {
"package.json": (content: string) => {
const pkg = JSON.parse(content);