From c246a0668658c7dc4dda3788ffea2ba798faf2d3 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Sat, 18 Apr 2026 15:14:16 +0100 Subject: [PATCH 1/3] Support `latest` as version input * Accepting `latest` resolves the newest release of kosli-dev/cli at runtime via the GitHub API, so workflows can auto-track upstream without hand-bumping the version. * Adds an optional `github-token` input (defaulting to `${{ github.token }}`) to authenticate the API call and avoid the 60/hr shared-IP rate limit, exposes a `version` step output containing the resolved version, and reuses `@actions/tool-cache` so repeat runs on the same runner skip the download. --- .github/workflows/test.yml | 21 +++++++++++-- action.yml | 9 +++++- src/download.js | 21 ++++++++++++- src/index.js | 23 ++++++++++---- test/download.test.js | 64 +++++++++++++++++++++++++++++++++++++- 5 files changed, 127 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a27c825..3d31874 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: os: [macos-latest, windows-latest, ubuntu-latest] - version: [2.11.27] + version: [2.11.27, latest] steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -27,6 +27,7 @@ jobs: run: npm ci && npm run build - name: Setup kosli CLI + id: setup uses: ./ with: version: ${{ matrix.version }} @@ -38,7 +39,8 @@ jobs: kosli version --short >> $GITHUB_ENV echo "\n" >> $GITHUB_ENV echo 'EOF' >> $GITHUB_ENV - - name: Verify + - name: Verify pinned version + if: matrix.version != 'latest' shell: python env: KOSLI_VERSION_EXPECTED: ${{ matrix.version }} @@ -47,3 +49,18 @@ jobs: sys.exit( int(not os.environ["KOSLI_VERSION_EXPECTED"] in os.environ["KOSLI_VERSION_INSTALLED"]) ) + - name: Verify latest resolved to a semver + if: matrix.version == 'latest' + shell: python + env: + KOSLI_VERSION_RESOLVED: ${{ steps.setup.outputs.version }} + run: | + import os, re, sys + installed = os.environ["KOSLI_VERSION_INSTALLED"].strip() + resolved = os.environ["KOSLI_VERSION_RESOLVED"].strip() + if not re.match(r"^\d+\.\d+\.\d+", resolved): + print(f"resolved version {resolved!r} does not look like semver") + sys.exit(1) + if resolved not in installed: + print(f"resolved {resolved!r} not found in installed {installed!r}") + sys.exit(1) diff --git a/action.yml b/action.yml index 4f38ac1..29fc692 100644 --- a/action.yml +++ b/action.yml @@ -2,9 +2,16 @@ name: setup-kosli-cli description: Install the Kosli CLI on Github Actions runners inputs: version: - description: Version of Kosli CLI + description: Version of Kosli CLI. Use `latest` to install the newest release from GitHub. required: false default: 2.11.43 + github-token: + description: Token used to authenticate GitHub API calls when resolving `latest`. + required: false + default: ${{ github.token }} +outputs: + version: + description: The resolved Kosli CLI version that was installed. branding: icon: 'download-cloud' color: 'blue' diff --git a/src/download.js b/src/download.js index 018b876..59a7ed4 100644 --- a/src/download.js +++ b/src/download.js @@ -1,4 +1,5 @@ const path = require("path"); +const github = require("@actions/github"); // Map node arch to arch in download url // arch in [arm, x32, x64...] (https://nodejs.org/api/os.html#os_os_arch) @@ -28,4 +29,22 @@ function getDownloadUrl({ version, platform, arch }) { return `https://github.com/kosli-dev/cli/releases/download/v${version}/${filename}.${extension}`; } -module.exports = { getDownloadUrl }; +async function resolveVersion(version, token, octokit) { + if (version !== "latest") { + return version; + } + const client = octokit || github.getOctokit(token); + let release; + try { + release = await client.rest.repos.getLatestRelease({ + owner: "kosli-dev", + repo: "cli" + }); + } catch (e) { + throw new Error(`failed to resolve latest Kosli CLI version from GitHub: ${e.message}`); + } + const tag = release.data.tag_name; + return tag.startsWith("v") ? tag.slice(1) : tag; +} + +module.exports = { getDownloadUrl, resolveVersion }; diff --git a/src/index.js b/src/index.js index 8b8186f..1c70d72 100644 --- a/src/index.js +++ b/src/index.js @@ -2,20 +2,31 @@ const path = require("path"); const os = require("os"); const core = require("@actions/core"); const tc = require("@actions/tool-cache"); -const { getDownloadUrl } = require("./download"); +const { getDownloadUrl, resolveVersion } = require("./download"); async function setup() { try { const version = core.getInput("version"); + const token = core.getInput("github-token"); const platform = os.platform(); const arch = os.arch(); - const downloadUrl = getDownloadUrl({ version, platform, arch }); - console.log(`installing Kosli CLI from ${downloadUrl} ...`); - const pathToTarball = await tc.downloadTool(downloadUrl); - const pathToCLI = await tc.extractTar(pathToTarball); + const resolvedVersion = await resolveVersion(version, token); + + let pathToCLI = tc.find("kosli", resolvedVersion); + if (!pathToCLI) { + const downloadUrl = getDownloadUrl({ version: resolvedVersion, platform, arch }); + console.log(`installing Kosli CLI from ${downloadUrl} ...`); + const pathToTarball = await tc.downloadTool(downloadUrl); + const extracted = await tc.extractTar(pathToTarball); + pathToCLI = await tc.cacheDir(extracted, "kosli", resolvedVersion); + } else { + console.log(`using cached Kosli CLI v${resolvedVersion} from ${pathToCLI}`); + } + core.addPath(pathToCLI); - console.log(`installed Kosli CLI v${version} to ${pathToCLI}`); + core.setOutput("version", resolvedVersion); + console.log(`installed Kosli CLI v${resolvedVersion} to ${pathToCLI}`); } catch (e) { core.setFailed(e); } diff --git a/test/download.test.js b/test/download.test.js index 4efcf4e..1d46177 100644 --- a/test/download.test.js +++ b/test/download.test.js @@ -1,5 +1,5 @@ const test = require("ava"); -const { getDownloadUrl } = require("../src/download"); +const { getDownloadUrl, resolveVersion } = require("../src/download"); const baseUrl = "https://github.com/kosli-dev/cli/releases/download/"; const testCases = [ @@ -19,3 +19,65 @@ testCases.forEach(element => { t.is(res, baseUrl + expected); }); }); + +function fakeOctokit(response) { + return { + rest: { + repos: { + getLatestRelease: async ({ owner, repo }) => { + if (typeof response === "function") { + return response({ owner, repo }); + } + return response; + } + } + } + }; +} + +test("resolveVersion passes through a concrete semver unchanged", async t => { + const result = await resolveVersion("2.11.43", "token-unused"); + t.is(result, "2.11.43"); +}); + +test("resolveVersion with 'latest' strips the leading v from the release tag", async t => { + const octokit = fakeOctokit({ data: { tag_name: "v2.12.0" } }); + const result = await resolveVersion("latest", "", octokit); + t.is(result, "2.12.0"); +}); + +test("resolveVersion with 'latest' returns the tag unchanged if there is no leading v", async t => { + const octokit = fakeOctokit({ data: { tag_name: "2.12.0" } }); + const result = await resolveVersion("latest", "", octokit); + t.is(result, "2.12.0"); +}); + +test("resolveVersion with 'latest' requests the kosli-dev/cli repo", async t => { + let captured; + const octokit = fakeOctokit(args => { + captured = args; + return { data: { tag_name: "v9.9.9" } }; + }); + await resolveVersion("latest", "", octokit); + t.deepEqual(captured, { owner: "kosli-dev", repo: "cli" }); +}); + +test("resolveVersion surfaces a descriptive error when the API call fails", async t => { + const octokit = { + rest: { + repos: { + getLatestRelease: async () => { + throw new Error("HTTP 403 rate limit exceeded"); + } + } + } + }; + await t.throwsAsync(resolveVersion("latest", "", octokit), { + message: /failed to resolve latest Kosli CLI version.*rate limit/ + }); +}); + +test("resolveVersion treats 'Latest' (mixed case) as a literal tag, not an alias", async t => { + const result = await resolveVersion("Latest", "token-unused"); + t.is(result, "Latest"); +}); From 2fa35d8436db0cc315ba84cfc07f82ec5e34799d Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Sun, 19 Apr 2026 21:05:32 +0100 Subject: [PATCH 2/3] feat: use latest as default version --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index 29fc692..a86828b 100644 --- a/action.yml +++ b/action.yml @@ -4,7 +4,7 @@ inputs: version: description: Version of Kosli CLI. Use `latest` to install the newest release from GitHub. required: false - default: 2.11.43 + default: latest github-token: description: Token used to authenticate GitHub API calls when resolving `latest`. required: false From ec3d07720d1f7c18967f6d6a574fd5770196d55a Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Sun, 19 Apr 2026 21:25:22 +0100 Subject: [PATCH 3/3] docs: update README to reflect latest changes --- README.md | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2b2afb0..46493e3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ and will install and expose a specified version of the `kosli` CLI on the runner ## Usage -Setup the `kosli` CLI: +Setup the `kosli` CLI (installs the latest release by default): ```yaml steps: @@ -25,15 +25,29 @@ steps: - name: setup-kosli-cli uses: kosli-dev/setup-cli-action@v3 with: - version: - 2.11.43 + version: 2.11.43 +``` + +To explicitly pin to the newest published release at runtime, pass `latest`: + +```yaml +steps: +- name: setup-kosli-cli + uses: kosli-dev/setup-cli-action@v3 + with: + version: latest ``` ## Inputs -The actions supports the following inputs: +The action supports the following inputs: + +- `version`: The version of `kosli` to install. Accepts a semver (e.g. `2.11.43`) or the alias `latest`, which resolves to the newest GitHub release of `kosli-dev/cli` at runtime. Defaults to `latest`. +- `github-token`: Token used to authenticate the GitHub API call that resolves `latest`. Defaults to `${{ github.token }}`; normally you do not need to set this. + +## Outputs -- `version`: The version of `kosli` to install, defaulting to `2.11.43` +- `version`: The resolved `kosli` CLI version that was installed. When `version: latest` is used, this will contain the concrete semver (e.g. `2.12.0`) and can be referenced by later steps via `steps..outputs.version`. ## Example job See [Kosli CLI documentation](https://docs.kosli.com/)