Skip to content

Commit 9542447

Browse files
authored
Merge pull request #10 from kosli-dev/20260418_add_latest
feat: Support `latest` as version input
2 parents 103b91e + ec3d077 commit 9542447

6 files changed

Lines changed: 147 additions & 17 deletions

File tree

.github/workflows/test.yml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
strategy:
1919
matrix:
2020
os: [macos-latest, windows-latest, ubuntu-latest]
21-
version: [2.11.27]
21+
version: [2.11.27, latest]
2222
steps:
2323
- name: Checkout
2424
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -27,6 +27,7 @@ jobs:
2727
run: npm ci && npm run build
2828

2929
- name: Setup kosli CLI
30+
id: setup
3031
uses: ./
3132
with:
3233
version: ${{ matrix.version }}
@@ -38,7 +39,8 @@ jobs:
3839
kosli version --short >> $GITHUB_ENV
3940
echo "\n" >> $GITHUB_ENV
4041
echo 'EOF' >> $GITHUB_ENV
41-
- name: Verify
42+
- name: Verify pinned version
43+
if: matrix.version != 'latest'
4244
shell: python
4345
env:
4446
KOSLI_VERSION_EXPECTED: ${{ matrix.version }}
@@ -47,3 +49,18 @@ jobs:
4749
sys.exit(
4850
int(not os.environ["KOSLI_VERSION_EXPECTED"] in os.environ["KOSLI_VERSION_INSTALLED"])
4951
)
52+
- name: Verify latest resolved to a semver
53+
if: matrix.version == 'latest'
54+
shell: python
55+
env:
56+
KOSLI_VERSION_RESOLVED: ${{ steps.setup.outputs.version }}
57+
run: |
58+
import os, re, sys
59+
installed = os.environ["KOSLI_VERSION_INSTALLED"].strip()
60+
resolved = os.environ["KOSLI_VERSION_RESOLVED"].strip()
61+
if not re.match(r"^\d+\.\d+\.\d+", resolved):
62+
print(f"resolved version {resolved!r} does not look like semver")
63+
sys.exit(1)
64+
if resolved not in installed:
65+
print(f"resolved {resolved!r} not found in installed {installed!r}")
66+
sys.exit(1)

README.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ and will install and expose a specified version of the `kosli` CLI on the runner
1111

1212
## Usage
1313

14-
Setup the `kosli` CLI:
14+
Setup the `kosli` CLI (installs the latest release by default):
1515

1616
```yaml
1717
steps:
@@ -25,15 +25,29 @@ steps:
2525
- name: setup-kosli-cli
2626
uses: kosli-dev/setup-cli-action@v3
2727
with:
28-
version:
29-
2.11.43
28+
version: 2.11.43
29+
```
30+
31+
To explicitly pin to the newest published release at runtime, pass `latest`:
32+
33+
```yaml
34+
steps:
35+
- name: setup-kosli-cli
36+
uses: kosli-dev/setup-cli-action@v3
37+
with:
38+
version: latest
3039
```
3140

3241
## Inputs
3342

34-
The actions supports the following inputs:
43+
The action supports the following inputs:
44+
45+
- `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`.
46+
- `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.
47+
48+
## Outputs
3549

36-
- `version`: The version of `kosli` to install, defaulting to `2.11.43`
50+
- `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.<id>.outputs.version`.
3751

3852
## Example job
3953
See [Kosli CLI documentation](https://docs.kosli.com/)

action.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@ name: setup-kosli-cli
22
description: Install the Kosli CLI on Github Actions runners
33
inputs:
44
version:
5-
description: Version of Kosli CLI
5+
description: Version of Kosli CLI. Use `latest` to install the newest release from GitHub.
66
required: false
7-
default: 2.11.43
7+
default: latest
8+
github-token:
9+
description: Token used to authenticate GitHub API calls when resolving `latest`.
10+
required: false
11+
default: ${{ github.token }}
12+
outputs:
13+
version:
14+
description: The resolved Kosli CLI version that was installed.
815
branding:
916
icon: 'download-cloud'
1017
color: 'blue'

src/download.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const path = require("path");
2+
const github = require("@actions/github");
23

34
// Map node arch to arch in download url
45
// arch in [arm, x32, x64...] (https://nodejs.org/api/os.html#os_os_arch)
@@ -28,4 +29,22 @@ function getDownloadUrl({ version, platform, arch }) {
2829
return `https://github.com/kosli-dev/cli/releases/download/v${version}/${filename}.${extension}`;
2930
}
3031

31-
module.exports = { getDownloadUrl };
32+
async function resolveVersion(version, token, octokit) {
33+
if (version !== "latest") {
34+
return version;
35+
}
36+
const client = octokit || github.getOctokit(token);
37+
let release;
38+
try {
39+
release = await client.rest.repos.getLatestRelease({
40+
owner: "kosli-dev",
41+
repo: "cli"
42+
});
43+
} catch (e) {
44+
throw new Error(`failed to resolve latest Kosli CLI version from GitHub: ${e.message}`);
45+
}
46+
const tag = release.data.tag_name;
47+
return tag.startsWith("v") ? tag.slice(1) : tag;
48+
}
49+
50+
module.exports = { getDownloadUrl, resolveVersion };

src/index.js

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,31 @@ const path = require("path");
22
const os = require("os");
33
const core = require("@actions/core");
44
const tc = require("@actions/tool-cache");
5-
const { getDownloadUrl } = require("./download");
5+
const { getDownloadUrl, resolveVersion } = require("./download");
66

77
async function setup() {
88
try {
99
const version = core.getInput("version");
10+
const token = core.getInput("github-token");
1011
const platform = os.platform();
1112
const arch = os.arch();
12-
const downloadUrl = getDownloadUrl({ version, platform, arch });
13-
console.log(`installing Kosli CLI from ${downloadUrl} ...`);
1413

15-
const pathToTarball = await tc.downloadTool(downloadUrl);
16-
const pathToCLI = await tc.extractTar(pathToTarball);
14+
const resolvedVersion = await resolveVersion(version, token);
15+
16+
let pathToCLI = tc.find("kosli", resolvedVersion);
17+
if (!pathToCLI) {
18+
const downloadUrl = getDownloadUrl({ version: resolvedVersion, platform, arch });
19+
console.log(`installing Kosli CLI from ${downloadUrl} ...`);
20+
const pathToTarball = await tc.downloadTool(downloadUrl);
21+
const extracted = await tc.extractTar(pathToTarball);
22+
pathToCLI = await tc.cacheDir(extracted, "kosli", resolvedVersion);
23+
} else {
24+
console.log(`using cached Kosli CLI v${resolvedVersion} from ${pathToCLI}`);
25+
}
26+
1727
core.addPath(pathToCLI);
18-
console.log(`installed Kosli CLI v${version} to ${pathToCLI}`);
28+
core.setOutput("version", resolvedVersion);
29+
console.log(`installed Kosli CLI v${resolvedVersion} to ${pathToCLI}`);
1930
} catch (e) {
2031
core.setFailed(e);
2132
}

test/download.test.js

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const test = require("ava");
2-
const { getDownloadUrl } = require("../src/download");
2+
const { getDownloadUrl, resolveVersion } = require("../src/download");
33

44
const baseUrl = "https://github.com/kosli-dev/cli/releases/download/";
55
const testCases = [
@@ -19,3 +19,65 @@ testCases.forEach(element => {
1919
t.is(res, baseUrl + expected);
2020
});
2121
});
22+
23+
function fakeOctokit(response) {
24+
return {
25+
rest: {
26+
repos: {
27+
getLatestRelease: async ({ owner, repo }) => {
28+
if (typeof response === "function") {
29+
return response({ owner, repo });
30+
}
31+
return response;
32+
}
33+
}
34+
}
35+
};
36+
}
37+
38+
test("resolveVersion passes through a concrete semver unchanged", async t => {
39+
const result = await resolveVersion("2.11.43", "token-unused");
40+
t.is(result, "2.11.43");
41+
});
42+
43+
test("resolveVersion with 'latest' strips the leading v from the release tag", async t => {
44+
const octokit = fakeOctokit({ data: { tag_name: "v2.12.0" } });
45+
const result = await resolveVersion("latest", "", octokit);
46+
t.is(result, "2.12.0");
47+
});
48+
49+
test("resolveVersion with 'latest' returns the tag unchanged if there is no leading v", async t => {
50+
const octokit = fakeOctokit({ data: { tag_name: "2.12.0" } });
51+
const result = await resolveVersion("latest", "", octokit);
52+
t.is(result, "2.12.0");
53+
});
54+
55+
test("resolveVersion with 'latest' requests the kosli-dev/cli repo", async t => {
56+
let captured;
57+
const octokit = fakeOctokit(args => {
58+
captured = args;
59+
return { data: { tag_name: "v9.9.9" } };
60+
});
61+
await resolveVersion("latest", "", octokit);
62+
t.deepEqual(captured, { owner: "kosli-dev", repo: "cli" });
63+
});
64+
65+
test("resolveVersion surfaces a descriptive error when the API call fails", async t => {
66+
const octokit = {
67+
rest: {
68+
repos: {
69+
getLatestRelease: async () => {
70+
throw new Error("HTTP 403 rate limit exceeded");
71+
}
72+
}
73+
}
74+
};
75+
await t.throwsAsync(resolveVersion("latest", "", octokit), {
76+
message: /failed to resolve latest Kosli CLI version.*rate limit/
77+
});
78+
});
79+
80+
test("resolveVersion treats 'Latest' (mixed case) as a literal tag, not an alias", async t => {
81+
const result = await resolveVersion("Latest", "token-unused");
82+
t.is(result, "Latest");
83+
});

0 commit comments

Comments
 (0)