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
5 changes: 0 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,6 @@ jobs:
# runner: macos-latest
# installer: pkg

# --- macOS AMD64 ---
- name: macos-amd64 (archive)
runner: macos-15-intel
installer: archive

# --- Windows AMD64 ---
- name: windows-amd64 (archive)
runner: windows-latest
Expand Down
2 changes: 1 addition & 1 deletion __tests__/install-apt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe('install-apt', () => {
])

expect(result.elidePath).toBe('/usr/bin/elide')
expect(result.version.tag_name).toBe('1.0.0')
expect(result.version.tag_name).toBe('latest')
})

it('should map aarch64 to arm64 for apt', async () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/install-msi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ describe('install-msi', () => {
const result = await installViaMsi(options)

expect(result.elidePath).toBe('C:\\Elide\\bin\\elide.exe')
expect(result.version.tag_name).toBe('1.0.0')
expect(result.version.tag_name).toBe('latest')
expect(result.version.userProvided).toBe(false)
})

Expand Down
2 changes: 1 addition & 1 deletion __tests__/install-pkg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ describe('install-pkg', () => {
const result = await installViaPkg(options)

expect(result.elidePath).toBe('/usr/local/bin/elide')
expect(result.version.tag_name).toBe('1.0.0')
expect(result.version.tag_name).toBe('latest')
expect(result.elideBin).toBe('/usr/local/bin')
expect(result.elideHome).toBe('/usr/local/bin')
})
Expand Down
6 changes: 3 additions & 3 deletions __tests__/install-rpm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe('install-rpm', () => {
'/tmp/elide.rpm'
])
expect(result.elidePath).toBe('/usr/bin/elide')
expect(result.version.tag_name).toBe('1.0.0')
expect(result.version.tag_name).toBe('latest')
})

it('should fall back to rpm when dnf is not available', async () => {
Expand Down Expand Up @@ -137,7 +137,7 @@ describe('install-rpm', () => {
expect.arrayContaining(['dnf'])
)
expect(result.elidePath).toBe('/usr/bin/elide')
expect(result.version.tag_name).toBe('1.0.0')
expect(result.version.tag_name).toBe('latest')
})

it('should return correct release info', async () => {
Expand All @@ -155,7 +155,7 @@ describe('install-rpm', () => {
expect(result.elidePath).toBe('/usr/bin/elide')
expect(result.elideBin).toBe('/usr/bin')
expect(result.elideHome).toBe('/usr/bin')
expect(result.version.tag_name).toBe('1.0.0')
expect(result.version.tag_name).toBe('1.2.3')
expect(result.version.userProvided).toBe(true)
})
})
4 changes: 2 additions & 2 deletions __tests__/install-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ describe('install-shell', () => {
'--gha'
])
expect(result.elidePath).toBe('/usr/local/bin/elide')
expect(result.version.tag_name).toBe('1.0.0')
expect(result.version.tag_name).toBe('latest')
})

it('should pass --version when a specific version is requested', async () => {
Expand Down Expand Up @@ -137,7 +137,7 @@ describe('install-shell', () => {
'-Gha'
])
expect(result.elidePath).toBe('/usr/local/bin/elide')
expect(result.version.tag_name).toBe('1.0.0')
expect(result.version.tag_name).toBe('latest')
})

it('should pass -Version when a specific version is requested', async () => {
Expand Down
50 changes: 49 additions & 1 deletion __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ mock.module('../src/telemetry', () => ({
}))

const main = await import('../src/main')
const { versionMatchesTag } = main
const { default: buildOptions, OptionName } = await import('../src/options')
const { ElideArch, ElideOS } = await import('../src/releases')
const { ActionOutputName } = await import('../src/main')
Expand All @@ -105,6 +106,53 @@ const setupMocks = () => {
setFailedMock.mockImplementation(() => {})
}

describe('versionMatchesTag', () => {
it('exact match', () => {
expect(versionMatchesTag('1.0.0', '1.0.0')).toBe(true)
})

it('exact match with prerelease tag', () => {
expect(versionMatchesTag('1.0.0-beta10', '1.0.0-beta10')).toBe(true)
})

it('exact match with build-metadata tag', () => {
expect(versionMatchesTag('1.4.0+20260707', '1.4.0+20260707')).toBe(true)
})

it('commit-hash suffix on plain semver tag', () => {
expect(versionMatchesTag('1.0.0.6f7ffa7', '1.0.0')).toBe(true)
})

it('commit-hash suffix on prerelease tag', () => {
expect(versionMatchesTag('1.0.0-beta10.6f7ffa7', '1.0.0-beta10')).toBe(true)
})

it('commit-hash suffix on build-metadata tag', () => {
expect(versionMatchesTag('1.4.0+20260707.6f7ffa7', '1.4.0+20260707')).toBe(
true
)
})

it('different version — no match', () => {
expect(versionMatchesTag('9.9.9', '1.0.0')).toBe(false)
})

it('binary ahead of requested prerelease — no match', () => {
expect(versionMatchesTag('1.0.0', '1.0.0-beta10')).toBe(false)
})

it('tag is not a prefix without dot separator — no match', () => {
// "1.0.01" must not match tag "1.0.0" (no dot between them)
expect(versionMatchesTag('1.0.01', '1.0.0')).toBe(false)
})

it('binary build date does not match tag build date — no match', () => {
expect(versionMatchesTag('1.4.0+20260708.6f7ffa7', '1.4.0+20260707')).toBe(
false
)
})
})

describe('action', () => {
beforeEach(() => {
execMock.mockClear()
Expand Down Expand Up @@ -436,7 +484,7 @@ describe('action', () => {
it('should warn on version mismatch', async () => {
setupMocks()
obtainVersionMock.mockResolvedValueOnce('1.0.0').mockResolvedValue('9.9.9')
await main.run({ force: true, installer: 'shell' })
await main.run({ force: true, installer: 'shell', version: '1.0.0' })
expect(warningMock).toHaveBeenCalledWith(
expect.stringContaining('Elide version mismatch'),
expect.objectContaining({ title: 'Version Mismatch' })
Expand Down
38 changes: 38 additions & 0 deletions __tests__/releases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,50 @@ const {
resolveLatestVersion,
buildDownloadUrl,
buildCdnAssetUrl,
toSemverCacheKey,
ArchiveType,
cdnOs,
cdnArch
} = await import('../src/releases')
const { default: buildOptions } = await import('../src/options')

describe('toSemverCacheKey', () => {
it('should leave plain semver unchanged', () => {
expect(toSemverCacheKey('1.0.0')).toBe('1.0.0')
})

it('should leave semver prerelease unchanged', () => {
expect(toSemverCacheKey('1.0.0-beta10')).toBe('1.0.0-beta10')
})

it('should convert nightly tag to prerelease form', () => {
expect(toSemverCacheKey('nightly-20260328')).toBe('0.0.0-nightly.20260328')
})

it('should strip dashes from nightly date portion', () => {
expect(toSemverCacheKey('nightly-2026-03-28')).toBe(
'0.0.0-nightly.20260328'
)
})

it('should convert build metadata tag to prerelease form', () => {
expect(toSemverCacheKey('1.4.0+20260707')).toBe('1.4.0-build.20260707')
})

it('should preserve prerelease when build metadata is present', () => {
expect(toSemverCacheKey('1.0.0-beta10+20260707')).toBe(
'1.0.0-beta10-build.20260707'
)
})

it('should handle multi-part build metadata', () => {
// e.g. if the tag itself embeds date+commit: 1.4.0+20260707.abc123
expect(toSemverCacheKey('1.4.0+20260707.abc123')).toBe(
'1.4.0-build.20260707.abc123'
)
})
})

describe('elide release', () => {
it('should support resolving the latest version', async () => {
expect(await resolveLatestVersion()).not.toBeNull()
Expand Down
2 changes: 1 addition & 1 deletion src/install-apt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export async function installViaApt(

return {
version: {
tag_name: version,
tag_name: options.version,
userProvided: options.version !== 'latest'
},
elidePath,
Expand Down
5 changes: 4 additions & 1 deletion src/install-msi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export async function installViaMsi(
core.info(`Elide ${version} installed via MSI at ${elidePath}`)

return {
version: { tag_name: version, userProvided: options.version !== 'latest' },
version: {
tag_name: options.version,
userProvided: options.version !== 'latest'
},
elidePath,
elideHome,
elideBin
Expand Down
5 changes: 4 additions & 1 deletion src/install-pkg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ export async function installViaPkg(
core.info(`Elide ${version} installed via PKG at ${elidePath}`)

return {
version: { tag_name: version, userProvided: options.version !== 'latest' },
version: {
tag_name: options.version,
userProvided: options.version !== 'latest'
},
elidePath,
elideHome,
elideBin
Expand Down
5 changes: 4 additions & 1 deletion src/install-rpm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ export async function installViaRpm(
core.info(`Elide ${version} installed via RPM at ${elidePath}`)

return {
version: { tag_name: version, userProvided: options.version !== 'latest' },
version: {
tag_name: options.version,
userProvided: options.version !== 'latest'
},
elidePath,
elideHome,
elideBin
Expand Down
4 changes: 2 additions & 2 deletions src/install-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ async function installViaBash(

return {
version: {
tag_name: version,
tag_name: options.version,
userProvided: options.version !== 'latest'
},
elidePath,
Expand Down Expand Up @@ -105,7 +105,7 @@ async function installViaPowerShell(

return {
version: {
tag_name: version,
tag_name: options.version,
userProvided: options.version !== 'latest'
},
elidePath,
Expand Down
19 changes: 17 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ import { installViaMsi } from './install-msi'
import { installViaPkg } from './install-pkg'
import { installViaRpm } from './install-rpm'

/**
* Returns true if a binary's self-reported version corresponds to the requested
* tag. An exact match always qualifies; a commit-hash suffix separated by a dot
* (e.g. "1.4.0+20260707.6f7ffa7" for tag "1.4.0+20260707") also qualifies,
* because the binary appends ".{commithash}" to the tag it was built from.
*/
export function versionMatchesTag(ver: string, tag: string): boolean {
return ver === tag || ver.startsWith(tag + '.')
}

export function notSupported(options: ElideSetupActionOptions): null | Error {
const spec = `${options.os}-${options.arch}`
switch (spec) {
Expand Down Expand Up @@ -183,7 +193,7 @@ export async function run(
const version = await obtainVersion(existing)

if (
version === effectiveOptions.version ||
versionMatchesTag(version, effectiveOptions.version) ||
effectiveOptions.version === 'local'
) {
core.notice(`Existing Elide ${version} preserved at ${existing}`, {
Expand Down Expand Up @@ -255,7 +265,12 @@ export async function run(
const ver = await obtainVersion(release.elidePath)

const isNightly = release.version.tag_name.startsWith('nightly-')
if (!isNightly && ver !== release.version.tag_name) {
const isSymbolic = release.version.tag_name === 'latest'
if (
!isNightly &&
!isSymbolic &&
!versionMatchesTag(ver, release.version.tag_name)
) {
core.warning(
`Elide version mismatch: expected '${release.version.tag_name}', but got '${ver}'`,
{ title: 'Version Mismatch' }
Expand Down
19 changes: 14 additions & 5 deletions src/releases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,24 @@ const GITHUB_DEFAULT_HEADERS = {
// Matches tags like "nightly-20260328" or "nightly-2026-03-28"
const NIGHTLY_TAG_RE = /^nightly-(.+)$/

// Matches tags with semver build metadata, like "1.4.0+20260707"
const BUILD_META_TAG_RE = /^([^+]+)\+(.+)$/

/**
* Convert a release tag to a valid semver string for use with @actions/tool-cache.
*
* tool-cache's `find()` calls `semver.clean()` on the version, which returns null
* for non-semver strings like "nightly-20260328". This causes cache lookups to
* silently fail (never hit, never store correctly).
*
* Mapping:
* "1.0.0" → "1.0.0" (already semver)
* "1.0.0-beta10" → "1.0.0-beta10" (valid semver prerelease)
* "nightly-20260328" → "0.0.0-nightly.20260328"
*
* We use prerelease (not build metadata with +) because semver.clean strips
* build metadata, making it useless for cache key matching.
*
* Mapping:
* "1.0.0" → "1.0.0" (already semver)
* "1.0.0-beta10" → "1.0.0-beta10" (valid semver prerelease)
* "1.4.0+20260707" → "1.4.0-build.20260707" (build metadata → prerelease)
* "nightly-20260328" → "0.0.0-nightly.20260328"
*/
export function toSemverCacheKey(tag: string): string {
const nightlyMatch = tag.match(NIGHTLY_TAG_RE)
Expand All @@ -41,6 +45,11 @@ export function toSemverCacheKey(tag: string): string {
const datePart = nightlyMatch[1].replaceAll('-', '')
return `0.0.0-nightly.${datePart}`
}
const buildMetaMatch = tag.match(BUILD_META_TAG_RE)
if (buildMetaMatch) {
// semver.clean strips +build metadata; convert to prerelease so it's preserved
return `${buildMetaMatch[1]}-build.${buildMetaMatch[2]}`
}
return tag
}

Expand Down
Loading