Revert "fix(cache): isolate cache keys per working_directory in monorepos" (#364)

Reverts jdx/mise-action#360

https://github.com/jdx/mise-action/issues/363

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Reverts the monorepo cache isolation change and simplifies caching to
a single cache for the entire `mise` directory.
> 
> - Replace binary/tools caches with a single cache via
`restoreMiseCache`/`saveCache`; set `cache-hit` from one restore
> - Default key template drops `dir_hash`; `file_hash` computed from
repo-wide glob patterns (no `working_directory`-specific config walk)
> - Persist `PRIMARY_KEY` and `MISE_DIR` in action state; `miseDir()`
reads from state
> - Remove monorepo cache isolation workflow `test-monorepo-cache.yml`;
minor cleanup in `AGENTS.md`
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
a157c4e176. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
This commit is contained in:
jdx 2026-01-20 06:31:18 -06:00 committed by GitHub
parent c53b9236f0
commit a836e1b817
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 80 additions and 690 deletions

View file

@ -40,38 +40,23 @@ const MISE_CONFIG_FILE_PATTERNS = [
// Default cache key template
const DEFAULT_CACHE_KEY_TEMPLATE =
'{{cache_key_prefix}}-{{platform}}-{{file_hash}}-{{dir_hash}}{{#if version}}-{{version}}{{/if}}{{#if mise_env}}-{{mise_env}}{{/if}}{{#if install_args_hash}}-{{install_args_hash}}{{/if}}'
interface CacheState {
key: string
hit: boolean
}
'{{cache_key_prefix}}-{{platform}}-{{file_hash}}{{#if version}}-{{version}}{{/if}}{{#if mise_env}}-{{mise_env}}{{/if}}{{#if install_args_hash}}-{{install_args_hash}}{{/if}}'
async function run(): Promise<void> {
try {
await setToolVersions()
await setMiseToml()
let cacheKey: string | undefined
if (core.getBooleanInput('cache')) {
cacheKey = await restoreMiseCache()
} else {
core.setOutput('cache-hit', false)
}
const version = core.getInput('version')
const cacheEnabled = core.getBooleanInput('cache')
// Restore binary cache, install mise, save binary cache if needed
let binaryCache: CacheState = { key: '', hit: false }
if (cacheEnabled) {
binaryCache = await restoreMiseBinaryCache(version)
}
await setupMise(version, core.getBooleanInput('fetch_from_github'))
if (cacheEnabled) {
await saveMiseBinaryCache(binaryCache)
}
// Restore tools cache (needs mise installed to compute key for working_directory)
let toolsCache: CacheState = { key: '', hit: false }
if (cacheEnabled) {
toolsCache = await restoreToolsCache()
}
core.setOutput('cache-hit', binaryCache.hit && toolsCache.hit)
const fetchFromGitHub = core.getBooleanInput('fetch_from_github')
await setupMise(version, fetchFromGitHub)
await setEnvVars()
if (core.getBooleanInput('reshim')) {
await miseReshim()
@ -79,12 +64,13 @@ async function run(): Promise<void> {
await testMise()
if (core.getBooleanInput('install')) {
await miseInstall()
if (cacheEnabled) {
await saveToolsCache(toolsCache)
if (cacheKey && core.getBooleanInput('cache_save')) {
await saveCache(cacheKey)
}
}
await miseLs()
if (core.getBooleanInput('env')) {
const loadEnv = core.getBooleanInput('env')
if (loadEnv) {
await exportMiseEnv()
}
} catch (err) {
@ -208,104 +194,27 @@ async function setEnvVars(): Promise<void> {
}
}
async function restoreMiseBinaryCache(version: string): Promise<CacheState> {
const binPath = path.join(miseDir(), 'bin')
const platform = await getTarget()
const resolvedVersion = version || 'latest'
const cacheKeyPrefix = core.getInput('cache_key_prefix') || 'mise-v0'
// Include hash of miseDir path to handle custom mise_dir configurations
const dirHash = crypto
.createHash('sha256')
.update(miseDir())
.digest('hex')
.slice(0, 8)
const key = `${cacheKeyPrefix}-binary-${platform}-${resolvedVersion}-${dirHash}`
async function restoreMiseCache(): Promise<string | undefined> {
core.startGroup('Restoring mise cache')
const cachePath = miseDir()
const cacheKey = await core.group('Restoring mise binary cache', async () => {
const restored = await cache.restoreCache([binPath], key)
if (restored) {
core.info(`mise binary cache restored from key: ${restored}`)
} else {
core.info(`mise binary cache not found for ${key}`)
}
return restored
})
return { key, hit: Boolean(cacheKey) }
}
async function saveMiseBinaryCache(state: CacheState): Promise<void> {
if (!core.getBooleanInput('cache_save') || state.hit || !state.key) {
return
}
await core.group('Saving mise binary cache', async () => {
const binPath = path.join(miseDir(), 'bin')
if (!fs.existsSync(binPath)) {
return
}
const cacheId = await cache.saveCache([binPath], state.key)
if (cacheId !== -1) {
core.info(`Binary cache saved with key: ${state.key}`)
}
})
}
/**
* Runs a function while preserving the mise binary. The tools cache includes
* bin/, so restoring it could overwrite the binary that setupMise() just
* installed. This backs up the binary before and restores it after.
*/
async function withBinaryBackup<T>(fn: () => Promise<T>): Promise<T> {
const binPath = path.join(miseDir(), 'bin')
const binBackup = path.join(
os.tmpdir(),
`mise-bin-backup-${crypto.randomBytes(8).toString('hex')}`
)
if (!fs.existsSync(binPath)) {
throw new Error(`Expected binary at ${binPath} but it does not exist`)
}
await io.cp(binPath, binBackup, { recursive: true })
try {
return await fn()
} finally {
try {
await io.cp(binBackup, binPath, { recursive: true, force: true })
} finally {
// cleanup even if the restore fails
await io.rmRF(binBackup)
}
}
}
async function restoreToolsCache(): Promise<CacheState> {
// Use custom cache key if provided, otherwise use default template
const cacheKeyTemplate =
core.getInput('cache_key') || DEFAULT_CACHE_KEY_TEMPLATE
const key = await processCacheKeyTemplate(cacheKeyTemplate)
const primaryKey = await processCacheKeyTemplate(cacheKeyTemplate)
if (!key) {
core.info('Tools caching disabled')
return { key: '', hit: false }
core.saveState('PRIMARY_KEY', primaryKey)
core.saveState('MISE_DIR', cachePath)
const cacheKey = await cache.restoreCache([cachePath], primaryKey)
core.setOutput('cache-hit', Boolean(cacheKey))
if (!cacheKey) {
core.info(`mise cache not found for ${primaryKey}`)
return primaryKey
}
const cacheKey = await withBinaryBackup(() =>
core.group('Restoring mise tools cache', async () => {
const cachePath = miseDir()
const restored = await cache.restoreCache([cachePath], key)
if (restored) {
core.info(`mise tools cache restored from key: ${restored}`)
} else {
core.info(`mise tools cache not found for ${key}`)
}
return restored
})
)
return { key, hit: Boolean(cacheKey) }
core.info(`mise cache restored from key: ${cacheKey}`)
}
async function setupMise(
@ -468,6 +377,9 @@ const writeFile = async (p: fs.PathLike, body: string): Promise<void> =>
run()
function miseDir(): string {
const dir = core.getState('MISE_DIR')
if (dir) return dir
const miseDir = core.getInput('mise_dir')
if (miseDir) return miseDir
@ -480,23 +392,18 @@ function miseDir(): string {
return path.join(os.homedir(), '.local', 'share', 'mise')
}
async function saveToolsCache(state: CacheState): Promise<void> {
if (!core.getBooleanInput('cache_save') || state.hit || !state.key) {
return
}
await core.group('Saving mise tools cache', async () => {
async function saveCache(cacheKey: string): Promise<void> {
await core.group(`Saving mise cache`, async () => {
const cachePath = miseDir()
if (!fs.existsSync(cachePath)) {
core.warning(`Cache folder path does not exist: ${cachePath}`)
return
throw new Error(`Cache folder path does not exist on disk: ${cachePath}`)
}
const cacheId = await cache.saveCache([cachePath], state.key)
if (cacheId !== -1) {
core.info(`Tools cache saved with key: ${state.key}`)
}
const cacheId = await cache.saveCache([cachePath], cacheKey)
if (cacheId === -1) return
core.info(`Cache saved from ${cachePath} with key: ${cacheKey}`)
})
}
@ -518,48 +425,16 @@ async function getTarget(): Promise<string> {
}
}
async function processCacheKeyTemplate(
template: string
): Promise<string | null> {
async function processCacheKeyTemplate(template: string): Promise<string> {
// Get all available variables
const version = core.getInput('version')
const installArgs = core.getInput('install_args')
const cacheKeyPrefix = core.getInput('cache_key_prefix') || 'mise-v0'
const miseEnv = process.env.MISE_ENV?.replace(/,/g, '-')
const platform = await getTarget()
const workingDirectory = core.getInput('working_directory')
// Calculate file hash
// When working_directory is set, use mise config ls to get only relevant config files
// Otherwise, use the glob pattern to get all config files in the repo
let fileHash: string
if (workingDirectory) {
const configFiles = await configFilesForPath(workingDirectory)
if (configFiles === null) {
// Failed to get config files, skip caching
return null
}
// Hash the contents of the config files
// Include file path in each update to prevent collisions between different
// file arrangements with the same concatenated content (e.g., "ab"+"cdef"
// vs "abc"+"def")
const hash = crypto.createHash('sha256')
try {
for (const file of configFiles) {
hash.update(file)
const content = await fs.promises.readFile(file)
hash.update(content)
}
fileHash = hash.digest('hex')
} catch (error) {
core.warning(
`Failed to read config file for cache key: ${error}. Caching will be disabled.`
)
return null
}
} else {
fileHash = await glob.hashFiles(MISE_CONFIG_FILE_PATTERNS.join('\n'))
}
const fileHash = await glob.hashFiles(MISE_CONFIG_FILE_PATTERNS.join('\n'))
// Calculate install args hash
let installArgsHash = ''
@ -574,21 +449,12 @@ async function processCacheKeyTemplate(
}
}
// Calculate mise dir hash to isolate caches for different mise_dir configurations
// This matches the binary cache key which also includes dir_hash
const dirHash = crypto
.createHash('sha256')
.update(miseDir())
.digest('hex')
.slice(0, 8)
// Prepare base template data
const baseTemplateData = {
version,
cache_key_prefix: cacheKeyPrefix,
platform,
file_hash: fileHash,
dir_hash: dirHash,
mise_env: miseEnv,
install_args_hash: installArgsHash
}
@ -617,84 +483,3 @@ async function isMusl() {
})
return stderr.indexOf('musl') > -1
}
/**
* Checks if a file path is contained within a directory.
*
* Uses path.relative() to compute the relative path from parent to child.
* If the result starts with ".." or is absolute, the child is outside the parent.
*
* @example
* isPathWithin("/workspace", "/workspace/src/file.ts") // true
* isPathWithin("/workspace", "/workspace-other/file.ts") // false (not a child)
* isPathWithin("/workspace", "/etc/passwd") // false (unrelated)
* isPathWithin("C:\\work", "D:\\other\\file.ts") // false (different drive on Windows)
*/
function isPathWithin(parent: string, child: string): boolean {
const relative = path.relative(parent, child)
return !relative.startsWith('..') && !path.isAbsolute(relative)
}
/**
* Get config files that affect the given working directory using `mise config ls --json`.
* This returns the hierarchy of configs (directory's own config + inherited parents).
* Filters to only files within GITHUB_WORKSPACE and adds corresponding .lock files.
*/
async function configFilesForPath(
workingDirectory: string
): Promise<string[] | null> {
const githubWorkspace = process.env.GITHUB_WORKSPACE || process.cwd()
// Use explicit path to mise binary instead of relying on PATH
const miseBinPath = path.join(
miseDir(),
'bin',
process.platform === 'win32' ? 'mise.exe' : 'mise'
)
try {
const output = await exec.getExecOutput(
miseBinPath,
['config', 'ls', '--json'],
{
cwd: workingDirectory,
silent: true
}
)
const configs: Array<{ path: string }> = JSON.parse(output.stdout)
const configFiles: string[] = []
for (const config of configs) {
const configPath = config.path
// Filter to only files within GITHUB_WORKSPACE
if (!isPathWithin(githubWorkspace, configPath)) {
continue
}
configFiles.push(configPath)
// Include corresponding lock files if they exist
let lockPath: string | undefined
if (configPath.endsWith('.toml')) {
// mise.toml -> mise.lock
lockPath = configPath.replace(/\.toml$/, '.lock')
} else if (configPath.endsWith('.tool-versions')) {
// .tool-versions -> mise.lock in the same directory
lockPath = path.join(path.dirname(configPath), 'mise.lock')
}
if (
lockPath &&
fs.existsSync(lockPath) &&
!configFiles.includes(lockPath)
) {
configFiles.push(lockPath)
}
}
return configFiles.sort()
} catch (error) {
core.warning(
`Failed to get config files for working_directory "${workingDirectory}": ${error}. Caching will be disabled.`
)
return null
}
}