Skip to content

Commit 1c15a15

Browse files
committed
fix: toolchain caching
Signed-off-by: Sam Gammon <sam@elide.ventures>
1 parent 6c9193b commit 1c15a15

2 files changed

Lines changed: 109 additions & 6 deletions

File tree

__tests__/releases-download.test.ts

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,8 @@ mock.module('../src/command', () => ({
5656
elideInfo: jest.fn()
5757
}))
5858

59-
const { downloadRelease, resolveLatestVersion } = await import(
60-
'../src/releases'
61-
)
59+
const { downloadRelease, resolveLatestVersion, toSemverCacheKey } =
60+
await import('../src/releases')
6261
const { default: buildOptions } = await import('../src/options')
6362

6463
describe('resolveLatestVersion', () => {
@@ -116,6 +115,28 @@ describe('resolveLatestVersion', () => {
116115
})
117116
})
118117

118+
describe('toSemverCacheKey', () => {
119+
it('should pass through valid semver', () => {
120+
expect(toSemverCacheKey('1.0.0')).toBe('1.0.0')
121+
expect(toSemverCacheKey('1.0.0-beta10')).toBe('1.0.0-beta10')
122+
expect(toSemverCacheKey('1.0.0-alpha9')).toBe('1.0.0-alpha9')
123+
expect(toSemverCacheKey('2.1.3')).toBe('2.1.3')
124+
})
125+
126+
it('should convert nightly tags to semver prerelease', () => {
127+
expect(toSemverCacheKey('nightly-20260328')).toBe('0.0.0-nightly.20260328')
128+
expect(toSemverCacheKey('nightly-2026-03-28')).toBe(
129+
'0.0.0-nightly.20260328'
130+
)
131+
expect(toSemverCacheKey('nightly-20251231')).toBe('0.0.0-nightly.20251231')
132+
})
133+
134+
it('should pass through unknown formats unchanged', () => {
135+
expect(toSemverCacheKey('dev')).toBe('dev')
136+
expect(toSemverCacheKey('custom-build')).toBe('custom-build')
137+
})
138+
})
139+
119140
describe('downloadRelease', () => {
120141
beforeEach(() => {
121142
debugMock.mockClear()
@@ -426,6 +447,59 @@ describe('downloadRelease', () => {
426447
expect(downloadToolMock).not.toHaveBeenCalled()
427448
})
428449

450+
it('nightly versions should use semver cache keys', async () => {
451+
findMock.mockReturnValue('')
452+
requestMock.mockResolvedValue({
453+
data: { tag_name: 'nightly-20260328', name: 'Nightly' }
454+
})
455+
const options = buildOptions({
456+
os: 'linux',
457+
arch: 'amd64',
458+
version: 'latest'
459+
})
460+
await downloadRelease(options)
461+
462+
// find should be called with the semver cache key, not the raw tag
463+
expect(findMock).toHaveBeenCalledWith(
464+
'elide',
465+
'0.0.0-nightly.20260328',
466+
'amd64'
467+
)
468+
// cacheDir should also use the semver key
469+
expect(cacheDirMock).toHaveBeenCalledWith(
470+
expect.any(String),
471+
'elide',
472+
'0.0.0-nightly.20260328',
473+
'amd64'
474+
)
475+
})
476+
477+
it('second run with nightly should hit cache', async () => {
478+
findMock.mockReturnValue(
479+
'/cache/tools/elide/0.0.0-nightly.20260328/amd64'
480+
)
481+
requestMock.mockResolvedValue({
482+
data: { tag_name: 'nightly-20260328', name: 'Nightly' }
483+
})
484+
const options = buildOptions({
485+
os: 'linux',
486+
arch: 'amd64',
487+
version: 'latest'
488+
})
489+
const result = await downloadRelease(options)
490+
491+
expect(findMock).toHaveBeenCalledWith(
492+
'elide',
493+
'0.0.0-nightly.20260328',
494+
'amd64'
495+
)
496+
expect(downloadToolMock).not.toHaveBeenCalled()
497+
expect(result.cached).toBe(true)
498+
expect(result.elideBin).toBe(
499+
'/cache/tools/elide/0.0.0-nightly.20260328/amd64/bin'
500+
)
501+
})
502+
429503
it('different architectures should not share cache', async () => {
430504
findMock.mockImplementation(
431505
(_name: string, _version: string, arch: string) => {

src/releases.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,34 @@ const GITHUB_DEFAULT_HEADERS = {
1616
'X-GitHub-Api-Version': GITHUB_API_VERSION
1717
}
1818

19+
// Matches tags like "nightly-20260328" or "nightly-2026-03-28"
20+
const NIGHTLY_TAG_RE = /^nightly-(.+)$/
21+
22+
/**
23+
* Convert a release tag to a valid semver string for use with @actions/tool-cache.
24+
*
25+
* tool-cache's `find()` calls `semver.clean()` on the version, which returns null
26+
* for non-semver strings like "nightly-20260328". This causes cache lookups to
27+
* silently fail (never hit, never store correctly).
28+
*
29+
* Mapping:
30+
* "1.0.0" → "1.0.0" (already semver)
31+
* "1.0.0-beta10" → "1.0.0-beta10" (valid semver prerelease)
32+
* "nightly-20260328" → "0.0.0-nightly.20260328"
33+
*
34+
* We use prerelease (not build metadata with +) because semver.clean strips
35+
* build metadata, making it useless for cache key matching.
36+
*/
37+
export function toSemverCacheKey(tag: string): string {
38+
const nightlyMatch = tag.match(NIGHTLY_TAG_RE)
39+
if (nightlyMatch) {
40+
// Use prerelease segment so semver.clean preserves it
41+
const datePart = nightlyMatch[1].replaceAll('-', '')
42+
return `0.0.0-nightly.${datePart}`
43+
}
44+
return tag
45+
}
46+
1947
/**
2048
* Version info resolved for a release of Elide.
2149
*/
@@ -359,12 +387,13 @@ async function maybeDownload(
359387
let elidePathTarget = elideHome
360388
let elideBin: string = `${elideHome}${sep}bin`
361389
let elideDir: string | null = null
390+
const cacheVersion = toSemverCacheKey(version.tag_name)
362391

363392
try {
364393
core.debug(
365-
`Checking for cached tool 'elide' at version '${version.tag_name}'`
394+
`Checking for cached tool 'elide' at version '${version.tag_name}' (cache key: ${cacheVersion})`
366395
)
367-
elideDir = toolCache.find('elide', version.tag_name, options.arch)
396+
elideDir = toolCache.find('elide', cacheVersion, options.arch)
368397
} catch (err) {
369398
core.debug(`Failed to locate Elide in tool cache: ${err}`)
370399
}
@@ -412,7 +441,7 @@ async function maybeDownload(
412441
const cachedPath = await toolCache.cacheDir(
413442
elideHome,
414443
'elide',
415-
version.tag_name,
444+
cacheVersion,
416445
options.arch
417446
)
418447

0 commit comments

Comments
 (0)