Compare commits

...

17 commits

Author SHA1 Message Date
mashehu
b30f81e0d5 add todo item back 2024-01-12 18:02:02 +00:00
mashehu
a082e105b0 make linter happy 2024-01-12 18:02:02 +00:00
aedbf9a630 fix: Support versions of Nextflow without "all" variants 2024-01-12 18:02:02 +00:00
99c654d38a fix: Add check for non-semver version strings in cache check 2024-01-12 18:02:02 +00:00
f65da33a7d test: Update version checking tests 2024-01-12 18:02:02 +00:00
85718639f5 test: Update API consistency tests 2024-01-12 18:02:02 +00:00
670f4c751c refactor: Update main script to use NextflowRelease objects 2024-01-12 18:02:02 +00:00
2483bb745d refactor: Outsource API calls to OctokitWrapper 2024-01-12 18:02:02 +00:00
2a5cf63744 docs: Clarify why CAPSULE_LOG is set 2024-01-12 18:02:02 +00:00
3e1cca9d8c refactor: Add and remove imports from main script 2024-01-12 18:02:02 +00:00
9a573cd481 refactor: Make install_nextflow work using NextflowRelease objects 2024-01-12 18:02:02 +00:00
5a5c42e549 refactor: Make get_nextflow_release work using NextflowRelease objects 2024-01-12 18:02:02 +00:00
bc65c05e25 refactor: Make tag filtering work on NextflowRelease objects 2024-01-12 18:02:02 +00:00
cdb0393711 refactor!: Move all Octokit code to own file
Separation of concerns. We want to completely separate the internals of
Octokit from this application, so move any reference/call to Octokit into
its own file to symbolize that.
2024-01-12 18:02:02 +00:00
ca8fcaccab feat: Add nextflow_release function
WIth the goal being to convert Octokit data into NextflowRelease objects,
create a function that can do that in one line.
2024-01-12 18:02:02 +00:00
a97128956e refactor: Move nextflow_bin_url function to NextflowRelease.ts
Nextflow binary URLs should be stored in the new NextflowRelease object, so
it should essentially be private for the NextflowRelease type. Move the
function to that file to signify that (but retain export for testing
purposes).
2024-01-12 18:02:02 +00:00
7cc27d1095 feat: Add NextflowRelease type 2024-01-12 18:02:02 +00:00
6 changed files with 279 additions and 138 deletions

View file

@ -1,41 +1,15 @@
import * as core from "@actions/core" import * as core from "@actions/core"
import { GitHub } from "@actions/github/lib/utils"
import * as tc from "@actions/tool-cache" import * as tc from "@actions/tool-cache"
import retry from "async-retry" import retry from "async-retry"
import * as fs from "fs" import * as fs from "fs"
import semver from "semver" import semver from "semver"
const NEXTFLOW_REPO = { owner: "nextflow-io", repo: "nextflow" } import { NextflowRelease } from "./nextflow-release"
// HACK Private but I want to test this function tag_filter(version: string): (r: NextflowRelease) => Boolean {
export async function all_nf_releases(
ok: InstanceType<typeof GitHub>
): Promise<object[]> {
return await ok.paginate(
ok.rest.repos.listReleases,
NEXTFLOW_REPO,
response => response.data
)
}
// HACK Private but I want to test this
export async function latest_stable_release_data(
ok: InstanceType<typeof GitHub>
): Promise<object> {
const { data: stable_release } = await ok.rest.repos.getLatestRelease(
NEXTFLOW_REPO
)
return stable_release
}
export async function release_data(
version: string,
ok: InstanceType<typeof GitHub>
): Promise<object> {
// Setup tag-based filtering // Setup tag-based filtering
let filter = (r: object): boolean => { let filter = (r: NextflowRelease): boolean => {
return semver.satisfies(r["tag_name"], version, true) return semver.satisfies(r.versionNumber, version, true)
} }
// Check if the user passed a 'latest*' tag, and override filtering // Check if the user passed a 'latest*' tag, and override filtering
@ -44,52 +18,45 @@ export async function release_data(
if (version.includes("-everything")) { if (version.includes("-everything")) {
// No filtering // No filtering
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
filter = (r: object) => { filter = (r: NextflowRelease) => {
return true return true
} }
} else if (version.includes("-edge")) { } else if (version.includes("-edge")) {
filter = r => { filter = (r: NextflowRelease) => {
return r["tag_name"].endsWith("-edge") return r.versionNumber.endsWith("-edge")
} }
} else { } else {
// This is special: passing 'latest' or 'latest-stable' allows us to use filter = (r: NextflowRelease) => {
// the latest stable GitHub release direct from the API return !r.isEdge
const stable_release = await latest_stable_release_data(ok)
return stable_release
} }
} }
}
return filter
}
// Get all the releases export async function get_nextflow_release(
const all_releases: object[] = await all_nf_releases(ok) version: string,
releases: NextflowRelease[]
const matching_releases = all_releases.filter(filter) ): Promise<NextflowRelease> {
// Filter the releases
const filter = tag_filter(version)
const matching_releases = releases.filter(filter)
matching_releases.sort((x, y) => { matching_releases.sort((x, y) => {
// HACK IDK why the value flip is necessary with the return // HACK IDK why the value flip is necessary with the return
return semver.compare(x["tag_name"], y["tag_name"], true) * -1 return semver.compare(x.versionNumber, y.versionNumber, true) * -1
}) })
return matching_releases[0] return matching_releases[0]
} }
export function nextflow_bin_url(release: object, get_all: boolean): string {
const release_assets = release["assets"]
const all_asset = release_assets.filter((a: object) => {
return a["browser_download_url"].endsWith("-all")
})[0]
const regular_asset = release_assets.filter((a: object) => {
return a["name"] === "nextflow"
})[0]
const dl_asset = get_all ? all_asset : regular_asset
return dl_asset.browser_download_url
}
export async function install_nextflow( export async function install_nextflow(
url: string, release: NextflowRelease,
version: string get_all: boolean
): Promise<string> { ): Promise<string> {
const url = get_all ? release.allBinaryURL : release.binaryURL
const version = release.versionNumber
core.debug(`Downloading Nextflow from ${url}`) core.debug(`Downloading Nextflow from ${url}`)
const nf_dl_path = await retry( const nf_dl_path = await retry(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -119,6 +86,11 @@ export async function install_nextflow(
} }
export function check_cache(version: string): boolean { export function check_cache(version: string): boolean {
// A 'latest*' version indicates that a cached version would be invalid until
// the version is resolved: abort
if (version.includes("latest")) {
return false
}
const cleaned_version = semver.clean(version, true) const cleaned_version = semver.clean(version, true)
if (cleaned_version === null) { if (cleaned_version === null) {
return false return false

View file

@ -1,20 +1,20 @@
import * as core from "@actions/core" import * as core from "@actions/core"
import * as exec from "@actions/exec" import * as exec from "@actions/exec"
import * as github from "@actions/github"
import { GitHub } from "@actions/github/lib/utils"
import * as tc from "@actions/tool-cache" import * as tc from "@actions/tool-cache"
import * as fs from "fs" import * as fs from "fs"
import semver from "semver" import semver from "semver"
import { import {
check_cache, check_cache,
install_nextflow, get_nextflow_release,
nextflow_bin_url, install_nextflow
release_data
} from "./functions" } from "./functions"
import { NextflowRelease } from "./nextflow-release"
import { pull_releases, setup_octokit } from "./octokit-wrapper"
async function run(): Promise<void> { async function run(): Promise<void> {
// Set environment variables // CAPSULE_LOG leads to a bunch of boilerplate being output to the logs: turn
// it off
core.exportVariable("CAPSULE_LOG", "none") core.exportVariable("CAPSULE_LOG", "none")
// Read in the arguments // Read in the arguments
@ -28,25 +28,16 @@ async function run(): Promise<void> {
} }
// Setup the API // Setup the API
let octokit: InstanceType<typeof GitHub> | undefined const octokit = await setup_octokit(token)
try {
octokit = github.getOctokit(token) const releases = await pull_releases(octokit)
} catch (e: unknown) {
if (e instanceof Error) {
core.setFailed(
`Could not authenticate to GitHub Releases API with provided token\n${e.message}`
)
}
}
// Get the release info for the desired release // Get the release info for the desired release
let release = {} let release = {} as NextflowRelease
let resolved_version = "" let resolved_version = ""
try { try {
if (octokit !== undefined) { release = await get_nextflow_release(version, releases)
release = await release_data(version, octokit) resolved_version = release.versionNumber
}
resolved_version = release["tag_name"]
core.info( core.info(
`Input version '${version}' resolved to Nextflow ${release["name"]}` `Input version '${version}' resolved to Nextflow ${release["name"]}`
) )
@ -58,20 +49,10 @@ async function run(): Promise<void> {
} }
} }
// Get the download url for the desired release
let url = ""
try {
url = nextflow_bin_url(release, get_all)
core.info(`Preparing to download from ${url}`)
} catch (e: unknown) {
if (e instanceof Error) {
core.setFailed(`Could not parse the download URL\n${e.message}`)
}
}
try { try {
// Download Nextflow and add it to path // Download Nextflow and add it to path
if (!check_cache(resolved_version)) { if (!check_cache(resolved_version)) {
const nf_install_path = await install_nextflow(url, resolved_version) const nf_install_path = await install_nextflow(release, get_all)
const cleaned_version = String(semver.clean(resolved_version, true)) const cleaned_version = String(semver.clean(resolved_version, true))
const nf_path = await tc.cacheDir( const nf_path = await tc.cacheDir(
nf_install_path, nf_install_path,

49
src/nextflow-release.ts Normal file
View file

@ -0,0 +1,49 @@
/**
* Houses the pertinent data that GitHub exposes for each Nextflow release
*/
export type NextflowRelease = {
versionNumber: string
isEdge: boolean
binaryURL: string
allBinaryURL: string
}
/**
* Converts the raw OctoKit data into a structured NextflowRelease
* @param data A "release" data struct from OctoKit
* @returns `data` converted into a `NextflowRelease`
*/
export function nextflow_release(data: object): NextflowRelease {
const nf_release: NextflowRelease = {
versionNumber: data["tag_name"],
isEdge: data["prerelease"],
binaryURL: nextflow_bin_url(data, false),
allBinaryURL: nextflow_bin_url(data, true)
}
return nf_release
}
/**
* Gets the download URL of a Nextflow binary
* @param release A "release" data struct from OctoKit
* @param get_all Whether to return the url for the "all" variant of Nextflow
* @returns The URL of the Nextflow binary
*/
export function nextflow_bin_url(release: object, get_all: boolean): string {
const release_assets = release["assets"]
const all_asset = release_assets.filter((a: object) => {
return a["browser_download_url"].endsWith("-all")
})[0]
const regular_asset = release_assets.filter((a: object) => {
return a["name"] === "nextflow"
})[0]
const dl_asset = get_all ? all_asset : regular_asset
if (dl_asset) {
return dl_asset.browser_download_url
} else {
// Old pre-release versions of Nextflow didn't have an "all" variant. To
// avoid downstream errors, substitute the regular url here.
return regular_asset.browser_download_url
}
}

62
src/octokit-wrapper.ts Normal file
View file

@ -0,0 +1,62 @@
import * as core from "@actions/core"
import * as github from "@actions/github"
import { GitHub } from "@actions/github/lib/utils"
import { nextflow_release, NextflowRelease } from "./nextflow-release"
const NEXTFLOW_REPO = { owner: "nextflow-io", repo: "nextflow" }
export async function setup_octokit(
github_token: string
): Promise<InstanceType<typeof GitHub>> {
let octokit = {} as InstanceType<typeof GitHub>
try {
octokit = github.getOctokit(github_token)
} catch (e: unknown) {
if (e instanceof Error) {
core.setFailed(
`Could not authenticate to GitHub Releases API with provided token\n${e.message}`
)
}
}
return octokit
}
export async function pull_releases(
octokit: InstanceType<typeof GitHub>
): Promise<NextflowRelease[]> {
const all_release_data: object[] = await all_nf_release_data(octokit)
const all_releases: NextflowRelease[] = []
for (const data of all_release_data) {
all_releases.push(nextflow_release(data))
}
return all_releases
}
export async function all_nf_release_data(
ok: InstanceType<typeof GitHub>
): Promise<object[]> {
return await ok.paginate(
ok.rest.repos.listReleases,
NEXTFLOW_REPO,
response => response.data
)
}
export async function latest_stable_release_data(
ok: InstanceType<typeof GitHub>
): Promise<object> {
const { data: stable_release } = await ok.rest.repos.getLatestRelease(
NEXTFLOW_REPO
)
return stable_release
}
export async function pull_latest_stable_release(
ok: InstanceType<typeof GitHub>
): Promise<NextflowRelease> {
const latest_release = await latest_stable_release_data(ok)
return nextflow_release(latest_release)
}

View file

@ -1,37 +1,113 @@
import * as github from "@actions/github" import test from "ava" // eslint-disable-line import/no-unresolved
import { GitHub } from "@actions/github/lib/utils"
import anyTest, { TestFn } from "ava" // eslint-disable-line import/no-unresolved
import * as functions from "../src/functions" import * as functions from "../src/functions"
import { getReleaseTag, getToken } from "./utils" import { NextflowRelease } from "../src/nextflow-release"
const test = anyTest as TestFn<{ // The Nextflow releases we are going to use for testing follow a regular
token: string // pattern: create a mock function to bootstrap some test data without repeating
octokit: InstanceType<typeof GitHub> // ourselves
}> function nf_release_gen(version_number: string): NextflowRelease {
const is_edge = version_number.endsWith("-edge")
test.before(t => { const release: NextflowRelease = {
const first = true versionNumber: version_number,
const current_token = getToken(first) isEdge: is_edge,
t.context = { binaryURL: `https://github.com/nextflow-io/nextflow/releases/download/${version_number}/nextflow`,
token: current_token, allBinaryURL: `https://github.com/nextflow-io/nextflow/releases/download/${version_number}/nextflow-${version_number.replace(
octokit: github.getOctokit(current_token) "v",
""
)}-all`
}
return release
} }
})
test("all_nf_releases", async t => { // A mock set of Nextflow releases
const result = await functions.all_nf_releases(t.context["octokit"]) const edge_is_newer = [
t.is(typeof result, "object") nf_release_gen("v23.09.1-edge"),
}) nf_release_gen("v23.04.3"),
nf_release_gen("v23.04.2")
]
const edge_is_older = [
nf_release_gen("v23.04.3"),
nf_release_gen("v23.04.2"),
nf_release_gen("v23.03.0-edge")
]
test("lastest_stable_release_data", async t => { /*
const result = await functions.latest_stable_release_data( The whole reason this action exists is to handle the cases where a final
t.context["octokit"] release is the "bleeding edge" release, rather than the "edge" release, even
though that's what the name would imply. Therefore, we need to test that the
'latest-everything' parameter can find the correct one regardless of whether
an "edge" release or a stable release is the true latest
*/
const release_filter_macro = test.macro(
async (
t,
input_version: string,
expected_version: string,
releases: NextflowRelease[]
) => {
const matched_release = await functions.get_nextflow_release(
input_version,
releases
)
t.is(matched_release.versionNumber, expected_version)
}
)
test(
"Latest-everything install with newer edge release",
release_filter_macro,
"latest-everything",
"v23.09.1-edge",
edge_is_newer
)
test(
"Latest-everything install with older edge release",
release_filter_macro,
"latest-everything",
"v23.04.3",
edge_is_older
)
test(
"Latest-edge install with newer edge release",
release_filter_macro,
"latest-edge",
"v23.09.1-edge",
edge_is_newer
)
test(
"Latest-edge install with older edge release",
release_filter_macro,
"latest-edge",
"v23.03.0-edge",
edge_is_older
)
test(
"Latest-stable install with newer edge release",
release_filter_macro,
"latest",
"v23.04.3",
edge_is_newer
)
test(
"Latest-stable install with older edge release",
release_filter_macro,
"latest",
"v23.04.3",
edge_is_older
)
test(
"Fully versioned tag release",
release_filter_macro,
"v23.04.2",
"v23.04.2",
edge_is_newer
)
test(
"Partially versioned tag release",
release_filter_macro,
"v23.04",
"v23.04.3",
edge_is_newer
) )
t.is(typeof result, "object")
const expected = await getReleaseTag("nextflow-io/nextflow", false)
t.is(result["tag_name"], expected)
})
test.todo("nextflow_bin_url")
test.todo("install_nextflow") test.todo("install_nextflow")

View file

@ -2,8 +2,9 @@ import * as github from "@actions/github"
import { GitHub } from "@actions/github/lib/utils" import { GitHub } from "@actions/github/lib/utils"
import anyTest, { TestFn } from "ava" // eslint-disable-line import/no-unresolved import anyTest, { TestFn } from "ava" // eslint-disable-line import/no-unresolved
import { release_data } from "../src/functions" import { nextflow_bin_url } from "../src/nextflow-release"
import { getReleaseTag, getToken } from "./utils" import { all_nf_release_data } from "../src/octokit-wrapper"
import { getToken } from "./utils"
const test = anyTest as TestFn<{ const test = anyTest as TestFn<{
token: string token: string
@ -19,22 +20,22 @@ test.before(t => {
} }
}) })
const macro = test.macro(async (t, version: string) => { const exists_macro = test.macro(async (t, object_name: string) => {
let expected const all_releases = await all_nf_release_data(t.context.octokit)
if (version === "latest-stable") { const first_release = all_releases[0]
expected = await getReleaseTag("nextflow-io/nextflow", false) t.assert(first_release.hasOwnProperty(object_name))
} else if (version === "latest-edge") {
expected = await getReleaseTag("nextflow-io/nextflow", true)
} else if (version === "latest-everything") {
expected = await getReleaseTag("nextflow-io/nextflow", undefined)
} else {
expected = version
}
const result = await release_data(version, t.context["octokit"])
t.is(result["tag_name"], expected)
}) })
test("hard version", macro, "v22.10.2") test("OctoKit returns tag", exists_macro, "tag_name")
test("latest-stable", macro, "latest-stable") test("Octokit returns prerelease", exists_macro, "prerelease")
test("latest-edge", macro, "latest-edge") test("Octokit returns assets", exists_macro, "assets")
test("latest-everything", macro, "latest-everything")
const binary_url_macro = test.macro(async (t, get_all: boolean) => {
const all_releases = await all_nf_release_data(t.context.octokit)
const first_release = all_releases[0]
const url = nextflow_bin_url(first_release, get_all)
t.notThrows(() => new URL(url))
})
test("Nextflow binary URL valid", binary_url_macro, false)
test("Nextflow 'all' binary URL valid", binary_url_macro, true)