mirror of
https://github.com/jdx/mise-action.git
synced 2026-05-14 13:50:33 +00:00
Revert "fix(cache): isolate cache keys per working_directory in monorepos (#360)"
This reverts commit 891faa7084.
This commit is contained in:
parent
c53b9236f0
commit
a157c4e176
5 changed files with 80 additions and 690 deletions
219
.github/workflows/test-monorepo-cache.yml
vendored
219
.github/workflows/test-monorepo-cache.yml
vendored
|
|
@ -1,219 +0,0 @@
|
|||
# Test monorepo cache isolation: when working_directory is set, each directory
|
||||
# should get its own cache key based on its config hierarchy, not all configs
|
||||
# in the repo. This prevents cache pollution between unrelated projects.
|
||||
name: "test-monorepo-cache"
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
# Simulates a monorepo with:
|
||||
# - Root config with shared tool (actionlint)
|
||||
# - Frontend with its own tool (shfmt)
|
||||
# - Backend with its own tool (shellcheck)
|
||||
# Frontend and backend both inherit the root config.
|
||||
SETUP_MONOREPO: |
|
||||
mkdir -p apps/frontend apps/backend
|
||||
echo '[tools]' > mise.toml
|
||||
echo 'actionlint = "1"' >> mise.toml
|
||||
echo '[tools]' > apps/frontend/mise.toml
|
||||
echo 'shfmt = "3"' >> apps/frontend/mise.toml
|
||||
echo '[tools]' > apps/backend/mise.toml
|
||||
echo 'shellcheck = "0.10"' >> apps/backend/mise.toml
|
||||
|
||||
# Simple single-tool config for mise_dir tests
|
||||
SETUP_SIMPLE: |
|
||||
echo '[tools]' > mise.toml
|
||||
echo 'actionlint = "1"' >> mise.toml
|
||||
|
||||
jobs:
|
||||
# Install ONLY backend tools and save cache.
|
||||
# This cache should NOT be shared with frontend.
|
||||
install-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: ${{ env.SETUP_MONOREPO }}
|
||||
- uses: ./
|
||||
with:
|
||||
working_directory: apps/backend
|
||||
cache_key_prefix: monorepo-test-${{ github.run_id }}
|
||||
|
||||
# Try to restore frontend after backend cache exists - should be cache MISS
|
||||
# since frontend has a different working_directory and thus a different cache key.
|
||||
restore-frontend:
|
||||
needs: install-backend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: ${{ env.SETUP_MONOREPO }}
|
||||
- uses: ./
|
||||
with:
|
||||
working_directory: apps/frontend
|
||||
install: false
|
||||
cache_key_prefix: monorepo-test-${{ github.run_id }}
|
||||
- name: Verify cache isolation
|
||||
run: |
|
||||
# With the fix, frontend has a different cache key, so cache miss = no tools
|
||||
# With the bug, frontend has same key as backend, so cache hit = has shellcheck but not shfmt
|
||||
if [ -f ~/.local/share/mise/shims/shellcheck ]; then
|
||||
echo "FAIL: shellcheck found - frontend got backend's cache (shared key bug)"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: No backend tools found - cache keys are properly isolated"
|
||||
|
||||
# Install frontend and save cache (used by unrelated-change-no-bust)
|
||||
install-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: ${{ env.SETUP_MONOREPO }}
|
||||
- uses: ./
|
||||
with:
|
||||
working_directory: apps/frontend
|
||||
cache_key_prefix: unrelated-${{ github.run_id }}
|
||||
|
||||
# Test that changing an UNRELATED directory's config does NOT bust the cache.
|
||||
# Only configs affecting the working_directory are hashed.
|
||||
unrelated-change-no-bust:
|
||||
needs: install-frontend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: ${{ env.SETUP_MONOREPO }}
|
||||
|
||||
# Modify BACKEND config (unrelated to frontend)
|
||||
- run: echo 'jq = "1"' >> apps/backend/mise.toml
|
||||
|
||||
# Try to restore frontend - should still be a cache HIT
|
||||
- uses: ./
|
||||
with:
|
||||
working_directory: apps/frontend
|
||||
install: false
|
||||
cache_key_prefix: unrelated-${{ github.run_id }}
|
||||
|
||||
- name: Verify cache still hits after unrelated change
|
||||
run: |
|
||||
if [ ! -f ~/.local/share/mise/shims/shfmt ]; then
|
||||
echo "FAIL: shfmt missing - unrelated backend change busted frontend cache"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Frontend cache survived unrelated backend change"
|
||||
|
||||
# Test that changing a parent config changes the child's cache key.
|
||||
# We modify the root mise.toml and verify the cache key is different
|
||||
# (cache miss on restore attempt).
|
||||
parent-config-change:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: ${{ env.SETUP_MONOREPO }}
|
||||
|
||||
# Install frontend with original root config
|
||||
- uses: ./
|
||||
with:
|
||||
working_directory: apps/frontend
|
||||
cache_key_prefix: parent-change-${{ github.run_id }}
|
||||
|
||||
# Clear local mise state to simulate a fresh restore
|
||||
- run: rm -rf ~/.local/share/mise/installs ~/.local/share/mise/shims
|
||||
|
||||
# Modify root config (add a new tool)
|
||||
- run: echo 'taplo = "0.9"' >> mise.toml
|
||||
|
||||
# Try to restore - should be a cache MISS because root config changed
|
||||
- uses: ./
|
||||
id: restore-after-change
|
||||
with:
|
||||
working_directory: apps/frontend
|
||||
install: false
|
||||
cache_key_prefix: parent-change-${{ github.run_id }}
|
||||
|
||||
# Verify cache miss (shfmt not present because we didn't install)
|
||||
- name: Verify cache miss after parent change
|
||||
run: |
|
||||
if [ -f ~/.local/share/mise/shims/shfmt ]; then
|
||||
echo "FAIL: shfmt found - cache should have missed after parent config change"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Cache missed as expected after parent config change"
|
||||
|
||||
# Test that changing a lock file changes the cache key.
|
||||
# Lock files pin exact versions, so changes should bust the cache.
|
||||
lock-file-change:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
mkdir -p apps/project
|
||||
echo '[tools]' > apps/project/mise.toml
|
||||
echo 'shfmt = "3"' >> apps/project/mise.toml
|
||||
echo 'lockfile = "v1"' > apps/project/mise.lock
|
||||
|
||||
# Install with original lock file
|
||||
- uses: ./
|
||||
with:
|
||||
working_directory: apps/project
|
||||
cache_key_prefix: lock-change-${{ github.run_id }}
|
||||
|
||||
# Clear local mise state
|
||||
- run: rm -rf ~/.local/share/mise/installs ~/.local/share/mise/shims
|
||||
|
||||
# Modify lock file
|
||||
- run: echo 'lockfile = "v2"' > apps/project/mise.lock
|
||||
|
||||
# Try to restore - should be cache MISS because lock file changed
|
||||
- uses: ./
|
||||
with:
|
||||
working_directory: apps/project
|
||||
install: false
|
||||
cache_key_prefix: lock-change-${{ github.run_id }}
|
||||
|
||||
- name: Verify cache miss after lock file change
|
||||
run: |
|
||||
if [ -f ~/.local/share/mise/shims/shfmt ]; then
|
||||
echo "FAIL: shfmt found - cache should have missed after lock file change"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Cache missed as expected after lock file change"
|
||||
|
||||
# Test that IDENTICAL content in DIFFERENT paths produces DIFFERENT cache keys.
|
||||
# This prevents hash collisions from concatenating file contents without separators.
|
||||
install-identical-content-alpha:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
mkdir -p apps/alpha
|
||||
echo '[tools]' > apps/alpha/mise.toml
|
||||
echo 'shfmt = "3"' >> apps/alpha/mise.toml
|
||||
- uses: ./
|
||||
with:
|
||||
working_directory: apps/alpha
|
||||
cache_key_prefix: identical-content-${{ github.run_id }}
|
||||
|
||||
# Try to restore with IDENTICAL content but DIFFERENT path - should be cache MISS
|
||||
restore-identical-content-beta:
|
||||
needs: install-identical-content-alpha
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
mkdir -p apps/beta
|
||||
# Exact same content as alpha
|
||||
echo '[tools]' > apps/beta/mise.toml
|
||||
echo 'shfmt = "3"' >> apps/beta/mise.toml
|
||||
- uses: ./
|
||||
with:
|
||||
working_directory: apps/beta
|
||||
install: false
|
||||
cache_key_prefix: identical-content-${{ github.run_id }}
|
||||
- name: Verify different paths produce different cache keys
|
||||
run: |
|
||||
if [ -f ~/.local/share/mise/shims/shfmt ]; then
|
||||
echo "FAIL: shfmt found - identical content in different path caused cache collision"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Identical content in different paths correctly produced different cache keys"
|
||||
|
|
@ -1 +0,0 @@
|
|||
CLAUDE.md
|
||||
251
dist/index.js
generated
vendored
251
dist/index.js
generated
vendored
|
|
@ -50025,28 +50025,21 @@ const MISE_CONFIG_FILE_PATTERNS = [
|
|||
`**/.tool-versions`
|
||||
];
|
||||
// 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}}';
|
||||
const DEFAULT_CACHE_KEY_TEMPLATE = '{{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() {
|
||||
try {
|
||||
await setToolVersions();
|
||||
await setMiseToml();
|
||||
let cacheKey;
|
||||
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 = { 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 = { 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();
|
||||
|
|
@ -50054,12 +50047,13 @@ async function run() {
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -50167,89 +50161,21 @@ async function setEnvVars() {
|
|||
core.addPath(shimsDir);
|
||||
}
|
||||
}
|
||||
async function restoreMiseBinaryCache(version) {
|
||||
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}`;
|
||||
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) {
|
||||
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(fn) {
|
||||
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() {
|
||||
async function restoreMiseCache() {
|
||||
core.startGroup('Restoring mise cache');
|
||||
const cachePath = miseDir();
|
||||
// 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);
|
||||
if (!key) {
|
||||
core.info('Tools caching disabled');
|
||||
return { key: '', hit: false };
|
||||
const primaryKey = await processCacheKeyTemplate(cacheKeyTemplate);
|
||||
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(version, fetchFromGitHub = false) {
|
||||
const miseBinDir = path.join(miseDir(), 'bin');
|
||||
|
|
@ -50383,6 +50309,9 @@ const writeFile = async (p, body) => await core.group(`Writing ${p}`, async () =
|
|||
});
|
||||
run();
|
||||
function miseDir() {
|
||||
const dir = core.getState('MISE_DIR');
|
||||
if (dir)
|
||||
return dir;
|
||||
const miseDir = core.getInput('mise_dir');
|
||||
if (miseDir)
|
||||
return miseDir;
|
||||
|
|
@ -50395,20 +50324,16 @@ function miseDir() {
|
|||
return path.join(LOCALAPPDATA, 'mise');
|
||||
return path.join(os.homedir(), '.local', 'share', 'mise');
|
||||
}
|
||||
async function saveToolsCache(state) {
|
||||
if (!core.getBooleanInput('cache_save') || state.hit || !state.key) {
|
||||
return;
|
||||
}
|
||||
await core.group('Saving mise tools cache', async () => {
|
||||
async function saveCache(cacheKey) {
|
||||
await core.group(`Saving mise cache`, async () => {
|
||||
const cachePath = miseDir();
|
||||
if (!fs.existsSync(cachePath)) {
|
||||
core.warning(`Cache folder path does not exist: ${cachePath}`);
|
||||
throw new Error(`Cache folder path does not exist on disk: ${cachePath}`);
|
||||
}
|
||||
const cacheId = await cache.saveCache([cachePath], cacheKey);
|
||||
if (cacheId === -1)
|
||||
return;
|
||||
}
|
||||
const cacheId = await cache.saveCache([cachePath], state.key);
|
||||
if (cacheId !== -1) {
|
||||
core.info(`Tools cache saved with key: ${state.key}`);
|
||||
}
|
||||
core.info(`Cache saved from ${cachePath} with key: ${cacheKey}`);
|
||||
});
|
||||
}
|
||||
async function getTarget() {
|
||||
|
|
@ -50434,38 +50359,8 @@ async function processCacheKeyTemplate(template) {
|
|||
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;
|
||||
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 = '';
|
||||
if (installArgs) {
|
||||
|
|
@ -50478,20 +50373,12 @@ async function processCacheKeyTemplate(template) {
|
|||
installArgsHash = crypto.createHash('sha256').update(tools).digest('hex');
|
||||
}
|
||||
}
|
||||
// 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
|
||||
};
|
||||
|
|
@ -50516,68 +50403,6 @@ 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, child) {
|
||||
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) {
|
||||
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 = JSON.parse(output.stdout);
|
||||
const configFiles = [];
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
|
|
|||
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
297
src/index.ts
297
src/index.ts
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue