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
Binary file modified dist/index.js.cache
Binary file not shown.
4 changes: 2 additions & 2 deletions dist/index.js.cache.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

52 changes: 47 additions & 5 deletions src/command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import { writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'

async function execElide(bin: string, args?: string[]): Promise<void> {
core.debug(`Executing: bin=${bin}, args=${args}`)
Expand Down Expand Up @@ -27,16 +30,28 @@ export enum ElideArgument {
/**
* Prewarm the provided Elide binary by running a small script.
*
* Runs a temp script file rather than inline code (`run -c`), since inline
* eval flags differ across Elide versions while `elide run <file>` is stable.
* Best-effort: prewarming is an optimization, so failures are logged as
* warnings and never fail the action.
*
* @param bin Path to the Elide binary.
* @return Promise which resolves when finished.
*/
export async function prewarm(bin: string): Promise<void> {
core.info(`Prewarming Elide at bin: ${bin}`)
return execElide(bin, [
ElideCommand.RUN,
'-c',
'"console.log(\'Elide ready.\')"'
])
try {
const script = join(tmpdir(), 'elide-prewarm.js')
writeFileSync(script, "console.log('Elide ready.')\n")
const exit = await exec.exec(`"${bin}"`, [ElideCommand.RUN, script], {
ignoreReturnCode: true
})
if (exit !== 0) {
core.warning(`Elide prewarm exited with code ${exit} (non-fatal)`)
}
} catch (err) {
core.warning(`Elide prewarm failed (non-fatal): ${err}`)
}
}

/**
Expand All @@ -62,3 +77,30 @@ export async function obtainVersion(bin: string): Promise<string> {
.trim()
.replaceAll('%0A', '')
}

/**
* Normalize a version string for tolerant comparison: trim, drop a leading `v`,
* and strip semver build metadata (everything from the first `+`). Lets a
* release tag like `1.2.0+20260430` or `v1.2.0` compare equal to a binary that
* self-reports `1.2.0`.
*
* @param value Version or tag string.
* @return Normalized version string.
*/
export function normalizeVersion(value: string): string {
return value.trim().replace(/^v/i, '').replace(/\+.*$/, '')
}

/**
* Whether two version strings refer to the same release, ignoring a leading `v`
* and build metadata. An empty or unparseable side never matches.
*
* @param a First version or tag string.
* @param b Second version or tag string.
* @return `true` if the normalized versions are equal.
*/
export function versionsMatch(a: string, b: string): boolean {
const na = normalizeVersion(a)
const nb = normalizeVersion(b)
return na.length > 0 && na === nb
}
6 changes: 3 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as core from '@actions/core'
import * as io from '@actions/io'
import { ActionOutputName, ElideSetupActionOutputs } from './outputs'
import { prewarm, info, obtainVersion } from './command'
import { prewarm, info, obtainVersion, versionsMatch } from './command'

import buildOptions, {
OptionName,
Expand Down Expand Up @@ -145,7 +145,7 @@ export async function run(

/* istanbul ignore next */
if (
version === effectiveOptions.version ||
versionsMatch(version, effectiveOptions.version) ||
effectiveOptions.version === 'local'
) {
core.info(
Expand Down Expand Up @@ -179,7 +179,7 @@ export async function run(
const version = await obtainVersion(release.elidePath)

/* istanbul ignore next */
if (version !== release.version.tag_name) {
if (!versionsMatch(version, release.version.tag_name)) {
core.warning(
`Elide version mismatch: expected '${release.version.tag_name}', but got '${version}'`
)
Expand Down
104 changes: 100 additions & 4 deletions src/releases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,22 @@ import { obtainVersion } from './command'
import { which, mv } from '@actions/io'
import { spawnSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { join } from 'node:path'

const downloadBase = 'https://gha.elide.zip'
const downloadPathV1 = 'cli/v1/snapshot'

/**
* A downloadable asset attached to a GitHub release.
*/
export type ReleaseAsset = {
// Asset filename, e.g. `elide.linux-amd64.txz`.
name: string

// Direct download URL (`browser_download_url`).
url: string
}

/**
* Version info resolved for a release of Elide.
*/
Expand All @@ -24,6 +36,9 @@ export type ElideVersionInfo = {

// Whether this version is resolved (`false`) or user-provided (`true`).
userProvided: boolean

// Release assets, when resolved from a GitHub release (e.g. via `latest`).
assets?: ReleaseAsset[]
}

/**
Expand Down Expand Up @@ -97,11 +112,44 @@ export interface DownloadedToolInfo {
}

/**
* Build a download URL for an Elide release; if a custom URL is provided as part of the set of
* `options`, use it instead.
* Platform label used in release asset filenames (`elide.<os>-<arch>.<ext>`).
* These differ from the action's internal {@link ElideOS}/{@link ElideArch}
* values: assets use `macos` (not `darwin`) and `arm64` (not `aarch64`).
*
* @param version Version we are downloading.
* @param options Effective options.
* @return Asset platform label, e.g. `linux-amd64` or `macos-arm64`.
*/
function assetPlatformLabel(options: ElideSetupActionOptions): string {
const os = options.os === ElideOS.MACOS ? 'macos' : options.os
const arch = options.arch === ElideArch.ARM64 ? 'arm64' : options.arch
return `${os}-${arch}`
}

/**
* Resolve the download URL for this platform from a release's attached assets,
* if available.
*
* @param version Resolved version info (may carry release assets).
* @param options Effective options.
* @param ext Archive extension to match (`txz`, `tgz`, or `zip`).
* @return Asset download URL, or `null` if no matching asset is present.
*/
function findAssetUrl(
version: ElideVersionInfo,
options: ElideSetupActionOptions,
ext: string
): string | null {
if (!version.assets || version.assets.length === 0) return null
const wanted = `elide.${assetPlatformLabel(options)}.${ext}`
return version.assets.find(a => a.name === wanted)?.url ?? null
}

/**
* Build a download URL for an Elide release: prefer the resolved release's
* platform asset, falling back to the CDN mirror.
*
* @param options Effective options.
* @param version Version we are downloading.
* @return URL and archive type to use.
*/
async function buildDownloadUrl(
Expand All @@ -122,6 +170,16 @@ async function buildDownloadUrl(
archiveType = ArchiveType.TXZ
}

// Prefer the GitHub release asset when we resolved a release (e.g. `latest`).
// The CDN mirror is keyed by stable-release tag and does not host nightly
// tags (`<semver>+<datestamp>`), so the release asset is the authoritative
// per-version source and the only one that works for nightly-as-latest.
const assetUrl = findAssetUrl(version, options, ext)
if (assetUrl) {
core.debug(`Using release asset download URL: ${assetUrl}`)
return { archiveType, url: new URL(assetUrl) }
}

return {
archiveType,
url: new URL(
Expand Down Expand Up @@ -268,10 +326,37 @@ export async function resolveLatestVersion(
return {
name,
tag_name: latest.data.tag_name,
userProvided: !!token
userProvided: !!token,
assets: (latest.data.assets ?? []).map(a => ({
name: a.name,
url: a.browser_download_url
}))
}
}

/**
* Locate the `elide` binary within an unpacked install or cache dir. CDN
* snapshot archives unpack with the binary at the root; GitHub release assets
* are full distributions with the binary under `bin/`. Probe both.
*
* @param home Unpacked install/cache directory.
* @param options Effective setup action options.
* @return Binary path + its containing dir (for PATH), or `null` if absent.
*/
function locateBinary(
home: string,
options: ElideSetupActionOptions
): { elidePath: string; elideBin: string } | null {
/* istanbul ignore next */
const exe = options.os === ElideOS.WINDOWS ? 'elide.exe' : 'elide'
for (const sub of ['', 'bin']) {
const dir = sub ? join(home, sub) : home
const candidate = join(dir, exe)
if (existsSync(candidate)) return { elidePath: candidate, elideBin: dir }
}
return null
}

/**
* Conditionally download the desired version of Elide, or use a cached version, if available.
*
Expand Down Expand Up @@ -373,6 +458,17 @@ async function maybeDownload(
}
}

// Resolve where the binary actually landed: root for CDN snapshot archives,
// `bin/` for full-distribution release assets. Pointing `elideBin` (added to
// PATH) at the real bin dir also exposes the bundled toolchain (javac,
// kotlinc, …) for release assets. Falls back to the legacy root paths when
// the probe finds nothing.
const located = locateBinary(elidePathTarget, options)
if (located) {
elidePath = located.elidePath
elideBin = located.elideBin
}

const result = {
version,
elidePath,
Expand Down
Loading