Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +27,7 @@ jobs:
run: npm ci && npm run build

- name: Setup kosli CLI
id: setup
uses: ./
with:
version: ${{ matrix.version }}
Expand All @@ -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 }}
Expand All @@ -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)
9 changes: 8 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
21 changes: 20 additions & 1 deletion src/download.js
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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 };
23 changes: 17 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
64 changes: 63 additions & 1 deletion test/download.test.js
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -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");
});