mirror of
https://github.com/jdx/mise-action.git
synced 2026-07-03 01:59:29 +00:00
fix: download mise with node fetch
This commit is contained in:
parent
5f61b63aff
commit
8e185e2855
4 changed files with 217 additions and 39 deletions
13
README.md
13
README.md
|
|
@ -116,6 +116,19 @@ When installing tools hosted on GitHub (like `gh`, `node`, `bun`, etc.), mise ne
|
|||
|
||||
**Note:** The action automatically uses `${{ github.token }}` as the default, so in most cases you don't need to explicitly provide it. However, if you encounter rate limit errors, make sure the token is being passed correctly.
|
||||
|
||||
## Proxy Configuration
|
||||
|
||||
The action downloads mise using Node.js `fetch`, so it does not require `curl`
|
||||
or `wget` to be installed in the runner container.
|
||||
|
||||
If your runner uses `HTTP_PROXY`, `HTTPS_PROXY`, or `NO_PROXY`, enable Node's
|
||||
environment proxy support before the action runs:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
NODE_USE_ENV_PROXY: "1"
|
||||
```
|
||||
|
||||
## Lock Files
|
||||
|
||||
If a repo mise lock file such as `mise.lock` is present in the working
|
||||
|
|
|
|||
108
dist/index.js
generated
vendored
108
dist/index.js
generated
vendored
|
|
@ -49,6 +49,7 @@ import process$1 from 'node:process';
|
|||
import https$1 from 'node:https';
|
||||
import require$$1$5 from 'tty';
|
||||
import fs$1 from 'node:fs';
|
||||
import { pipeline } from 'stream/promises';
|
||||
|
||||
// We use any as a valid input type
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
|
@ -89246,6 +89247,14 @@ const DEFAULT_CACHE_KEY_TEMPLATE = '{{cache_key_prefix}}-{{platform}}{{#if versi
|
|||
const ROOT_MISE_LOCK_FILE_PATTERNS = [/^\.?mise(?:\.[^.]+)?\.lock$/];
|
||||
const CONFIG_DIR_MISE_LOCK_FILE_PATTERNS = [/^mise(?:\.[^.]+)?\.lock$/];
|
||||
const CONFIG_MISE_LOCK_FILE_PATTERNS = [/^config(?:\.[^.]+)?\.lock$/];
|
||||
const ACTIVE_PROXY_ENV_VARS = [
|
||||
'HTTP_PROXY',
|
||||
'HTTPS_PROXY',
|
||||
'http_proxy',
|
||||
'https_proxy'
|
||||
];
|
||||
const FETCH_TIMEOUT_MS = 300_000;
|
||||
let warnedFetchProxy = false;
|
||||
async function run() {
|
||||
try {
|
||||
await setToolVersions();
|
||||
|
|
@ -89266,8 +89275,8 @@ async function run() {
|
|||
// etc.) don't silently send the runner's OIDC token to
|
||||
// a third-party cache without explicit consent.
|
||||
//
|
||||
// Note: `setupMise` fetches the mise binary itself with
|
||||
// `curl`, which doesn't go through mise's HTTP layer —
|
||||
// Note: `setupMise` fetches the mise binary itself with Node's
|
||||
// `fetch`, which doesn't go through mise's HTTP layer —
|
||||
// the wings rewriter only kicks in once the resulting
|
||||
// mise binary runs `mise install` and friends. Ordering
|
||||
// here is irrelevant for binary acceleration; we just
|
||||
|
|
@ -89495,25 +89504,19 @@ async function setupMise(version, fetchFromGitHub = false) {
|
|||
case '.zip': {
|
||||
await withExtractedZip(url, 'mise.zip', async (extractDir) => {
|
||||
const extractedMiseBinDir = path$1.join(extractDir, 'mise', 'bin');
|
||||
await mv(path$1.join(extractedMiseBinDir, 'mise.exe'), miseBinPath);
|
||||
await moveFile(path$1.join(extractedMiseBinDir, 'mise.exe'), miseBinPath);
|
||||
await installWindowsMiseShim(extractedMiseBinDir, miseShimPath);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case '.tar.zst':
|
||||
await exec('sh', [
|
||||
'-c',
|
||||
`curl -fsSL ${url} | tar --zstd -xf - -C ${os.tmpdir()} && mv ${os.tmpdir()}/mise/bin/mise ${miseBinPath}`
|
||||
]);
|
||||
await installFromTar(url, 'mise.tar.zst', ['--zstd'], miseBinPath);
|
||||
break;
|
||||
case '.tar.gz':
|
||||
await exec('sh', [
|
||||
'-c',
|
||||
`curl -fsSL ${url} | tar -xzf - -C ${os.tmpdir()} && mv ${os.tmpdir()}/mise/bin/mise ${miseBinPath}`
|
||||
]);
|
||||
await installFromTar(url, 'mise.tar.gz', ['-z'], miseBinPath);
|
||||
break;
|
||||
default:
|
||||
await exec('sh', ['-c', `curl -fsSL ${url} > ${miseBinPath}`]);
|
||||
await downloadFile(url, miseBinPath);
|
||||
await exec('chmod', ['+x', miseBinPath]);
|
||||
break;
|
||||
}
|
||||
|
|
@ -89551,7 +89554,7 @@ async function withExtractedZip(url, archiveName, fn) {
|
|||
try {
|
||||
const archivePath = path$1.join(tempDir, archiveName);
|
||||
const extractDir = path$1.join(tempDir, 'extract');
|
||||
await exec('curl', ['-fsSL', url, '--output', archivePath]);
|
||||
await downloadFile(url, archivePath);
|
||||
await exec('unzip', [archivePath, '-d', extractDir]);
|
||||
await fn(extractDir);
|
||||
}
|
||||
|
|
@ -89567,7 +89570,7 @@ async function installWindowsMiseShim(extractedMiseBinDir, miseShimPath) {
|
|||
info('mise-shim.exe not found in the mise archive; skipping');
|
||||
return;
|
||||
}
|
||||
await mv(extractedMiseShimPath, miseShimPath);
|
||||
await moveFile(extractedMiseShimPath, miseShimPath);
|
||||
}
|
||||
async function ensureWindowsMiseShim(miseBinPath, miseShimPath, version) {
|
||||
if (process.platform !== 'win32')
|
||||
|
|
@ -89587,6 +89590,34 @@ async function ensureWindowsMiseShim(miseBinPath, miseShimPath, version) {
|
|||
warning(`Failed to install mise-shim.exe: ${errorMessage(err)}. Continuing because mise can fall back to file shim mode on Windows.`);
|
||||
}
|
||||
}
|
||||
async function installFromTar(url, archiveName, tarArgs, miseBinPath) {
|
||||
const tempDir = await fs.promises.mkdtemp(path$1.join(os.tmpdir(), 'mise-action-'));
|
||||
try {
|
||||
const archivePath = path$1.join(tempDir, archiveName);
|
||||
await downloadFile(url, archivePath);
|
||||
await exec('tar', [...tarArgs, '-xf', archivePath, '-C', tempDir]);
|
||||
await moveFile(path$1.join(tempDir, 'mise', 'bin', 'mise'), miseBinPath);
|
||||
}
|
||||
finally {
|
||||
await rmRF(tempDir);
|
||||
}
|
||||
}
|
||||
async function moveFile(sourcePath, targetPath) {
|
||||
try {
|
||||
await mv(sourcePath, targetPath);
|
||||
}
|
||||
catch (err) {
|
||||
if (!isCrossDeviceRename(err))
|
||||
throw err;
|
||||
await fs.promises.copyFile(sourcePath, targetPath);
|
||||
await fs.promises.unlink(sourcePath);
|
||||
}
|
||||
}
|
||||
function isCrossDeviceRename(err) {
|
||||
return (err instanceof Error &&
|
||||
'code' in err &&
|
||||
err.code === 'EXDEV');
|
||||
}
|
||||
async function getInstalledMiseVersion(miseBinPath) {
|
||||
const versionOutput = await getExecOutput(miseBinPath, ['version', '--json'], { silent: true });
|
||||
const versionJson = JSON.parse(versionOutput.stdout);
|
||||
|
|
@ -89605,11 +89636,50 @@ async function zstdInstalled() {
|
|||
}
|
||||
}
|
||||
async function latestMiseVersion() {
|
||||
const rsp = await getExecOutput('curl', [
|
||||
'-fsSL',
|
||||
'https://mise.jdx.dev/VERSION'
|
||||
]);
|
||||
return rsp.stdout.trim();
|
||||
return (await fetchText('https://mise.jdx.dev/VERSION')).trim();
|
||||
}
|
||||
async function fetchText(url) {
|
||||
const rsp = await fetchUrl(url);
|
||||
return await rsp.text();
|
||||
}
|
||||
async function downloadFile(url, filePath) {
|
||||
const rsp = await fetchUrl(url);
|
||||
if (!rsp.body) {
|
||||
throw new Error(`Failed to download ${url}: empty response body`);
|
||||
}
|
||||
const tempFilePath = path$1.join(path$1.dirname(filePath), `.${path$1.basename(filePath)}.${crypto$1.randomUUID()}.tmp`);
|
||||
try {
|
||||
await pipeline(Readable.fromWeb(rsp.body), fs.createWriteStream(tempFilePath));
|
||||
await moveFile(tempFilePath, filePath);
|
||||
}
|
||||
catch (err) {
|
||||
await fs.promises.rm(tempFilePath, { force: true });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
async function fetchUrl(url) {
|
||||
warnIfFetchMayIgnoreProxy();
|
||||
const rsp = await fetch(url, {
|
||||
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
||||
});
|
||||
if (!rsp.ok) {
|
||||
throw new Error(`Failed to download ${url}: ${rsp.status} ${rsp.statusText}`);
|
||||
}
|
||||
return rsp;
|
||||
}
|
||||
function warnIfFetchMayIgnoreProxy() {
|
||||
if (warnedFetchProxy)
|
||||
return;
|
||||
warnedFetchProxy = true;
|
||||
const proxyEnvVar = ACTIVE_PROXY_ENV_VARS.find(name => process.env[name]);
|
||||
if (!proxyEnvVar)
|
||||
return;
|
||||
if (process.env.NODE_USE_ENV_PROXY === '1')
|
||||
return;
|
||||
if (process.execArgv.includes('--use-env-proxy'))
|
||||
return;
|
||||
warning(`${proxyEnvVar} is set, but Node env proxy support is not enabled. ` +
|
||||
'Set NODE_USE_ENV_PROXY=1 at the workflow or job level if mise downloads need to use HTTP_PROXY, HTTPS_PROXY, or NO_PROXY.');
|
||||
}
|
||||
async function setToolVersions() {
|
||||
const toolVersions = getInput('tool_versions');
|
||||
|
|
|
|||
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
133
src/index.ts
133
src/index.ts
|
|
@ -7,6 +7,8 @@ import * as crypto from 'crypto'
|
|||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import { Readable } from 'stream'
|
||||
import { pipeline } from 'stream/promises'
|
||||
import * as Handlebars from 'handlebars'
|
||||
|
||||
// Configuration file patterns for cache key generation
|
||||
|
|
@ -45,6 +47,14 @@ const DEFAULT_CACHE_KEY_TEMPLATE =
|
|||
const ROOT_MISE_LOCK_FILE_PATTERNS = [/^\.?mise(?:\.[^.]+)?\.lock$/]
|
||||
const CONFIG_DIR_MISE_LOCK_FILE_PATTERNS = [/^mise(?:\.[^.]+)?\.lock$/]
|
||||
const CONFIG_MISE_LOCK_FILE_PATTERNS = [/^config(?:\.[^.]+)?\.lock$/]
|
||||
const ACTIVE_PROXY_ENV_VARS = [
|
||||
'HTTP_PROXY',
|
||||
'HTTPS_PROXY',
|
||||
'http_proxy',
|
||||
'https_proxy'
|
||||
]
|
||||
const FETCH_TIMEOUT_MS = 300_000
|
||||
let warnedFetchProxy = false
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
|
|
@ -67,8 +77,8 @@ async function run(): Promise<void> {
|
|||
// etc.) don't silently send the runner's OIDC token to
|
||||
// a third-party cache without explicit consent.
|
||||
//
|
||||
// Note: `setupMise` fetches the mise binary itself with
|
||||
// `curl`, which doesn't go through mise's HTTP layer —
|
||||
// Note: `setupMise` fetches the mise binary itself with Node's
|
||||
// `fetch`, which doesn't go through mise's HTTP layer —
|
||||
// the wings rewriter only kicks in once the resulting
|
||||
// mise binary runs `mise install` and friends. Ordering
|
||||
// here is irrelevant for binary acceleration; we just
|
||||
|
|
@ -335,25 +345,22 @@ async function setupMise(
|
|||
case '.zip': {
|
||||
await withExtractedZip(url, 'mise.zip', async extractDir => {
|
||||
const extractedMiseBinDir = path.join(extractDir, 'mise', 'bin')
|
||||
await io.mv(path.join(extractedMiseBinDir, 'mise.exe'), miseBinPath)
|
||||
await moveFile(
|
||||
path.join(extractedMiseBinDir, 'mise.exe'),
|
||||
miseBinPath
|
||||
)
|
||||
await installWindowsMiseShim(extractedMiseBinDir, miseShimPath)
|
||||
})
|
||||
break
|
||||
}
|
||||
case '.tar.zst':
|
||||
await exec.exec('sh', [
|
||||
'-c',
|
||||
`curl -fsSL ${url} | tar --zstd -xf - -C ${os.tmpdir()} && mv ${os.tmpdir()}/mise/bin/mise ${miseBinPath}`
|
||||
])
|
||||
await installFromTar(url, 'mise.tar.zst', ['--zstd'], miseBinPath)
|
||||
break
|
||||
case '.tar.gz':
|
||||
await exec.exec('sh', [
|
||||
'-c',
|
||||
`curl -fsSL ${url} | tar -xzf - -C ${os.tmpdir()} && mv ${os.tmpdir()}/mise/bin/mise ${miseBinPath}`
|
||||
])
|
||||
await installFromTar(url, 'mise.tar.gz', ['-z'], miseBinPath)
|
||||
break
|
||||
default:
|
||||
await exec.exec('sh', ['-c', `curl -fsSL ${url} > ${miseBinPath}`])
|
||||
await downloadFile(url, miseBinPath)
|
||||
await exec.exec('chmod', ['+x', miseBinPath])
|
||||
break
|
||||
}
|
||||
|
|
@ -402,7 +409,7 @@ async function withExtractedZip(
|
|||
const archivePath = path.join(tempDir, archiveName)
|
||||
const extractDir = path.join(tempDir, 'extract')
|
||||
|
||||
await exec.exec('curl', ['-fsSL', url, '--output', archivePath])
|
||||
await downloadFile(url, archivePath)
|
||||
await exec.exec('unzip', [archivePath, '-d', extractDir])
|
||||
await fn(extractDir)
|
||||
} finally {
|
||||
|
|
@ -422,7 +429,7 @@ async function installWindowsMiseShim(
|
|||
return
|
||||
}
|
||||
|
||||
await io.mv(extractedMiseShimPath, miseShimPath)
|
||||
await moveFile(extractedMiseShimPath, miseShimPath)
|
||||
}
|
||||
|
||||
async function ensureWindowsMiseShim(
|
||||
|
|
@ -456,6 +463,43 @@ async function ensureWindowsMiseShim(
|
|||
}
|
||||
}
|
||||
|
||||
async function installFromTar(
|
||||
url: string,
|
||||
archiveName: string,
|
||||
tarArgs: string[],
|
||||
miseBinPath: string
|
||||
): Promise<void> {
|
||||
const tempDir = await fs.promises.mkdtemp(
|
||||
path.join(os.tmpdir(), 'mise-action-')
|
||||
)
|
||||
try {
|
||||
const archivePath = path.join(tempDir, archiveName)
|
||||
await downloadFile(url, archivePath)
|
||||
await exec.exec('tar', [...tarArgs, '-xf', archivePath, '-C', tempDir])
|
||||
await moveFile(path.join(tempDir, 'mise', 'bin', 'mise'), miseBinPath)
|
||||
} finally {
|
||||
await io.rmRF(tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
async function moveFile(sourcePath: string, targetPath: string): Promise<void> {
|
||||
try {
|
||||
await io.mv(sourcePath, targetPath)
|
||||
} catch (err) {
|
||||
if (!isCrossDeviceRename(err)) throw err
|
||||
await fs.promises.copyFile(sourcePath, targetPath)
|
||||
await fs.promises.unlink(sourcePath)
|
||||
}
|
||||
}
|
||||
|
||||
function isCrossDeviceRename(err: unknown): boolean {
|
||||
return (
|
||||
err instanceof Error &&
|
||||
'code' in err &&
|
||||
(err as NodeJS.ErrnoException).code === 'EXDEV'
|
||||
)
|
||||
}
|
||||
|
||||
async function getInstalledMiseVersion(miseBinPath: string): Promise<string> {
|
||||
const versionOutput = await exec.getExecOutput(
|
||||
miseBinPath,
|
||||
|
|
@ -480,11 +524,62 @@ async function zstdInstalled(): Promise<boolean> {
|
|||
}
|
||||
|
||||
async function latestMiseVersion(): Promise<string> {
|
||||
const rsp = await exec.getExecOutput('curl', [
|
||||
'-fsSL',
|
||||
'https://mise.jdx.dev/VERSION'
|
||||
])
|
||||
return rsp.stdout.trim()
|
||||
return (await fetchText('https://mise.jdx.dev/VERSION')).trim()
|
||||
}
|
||||
|
||||
async function fetchText(url: string): Promise<string> {
|
||||
const rsp = await fetchUrl(url)
|
||||
return await rsp.text()
|
||||
}
|
||||
|
||||
async function downloadFile(url: string, filePath: string): Promise<void> {
|
||||
const rsp = await fetchUrl(url)
|
||||
if (!rsp.body) {
|
||||
throw new Error(`Failed to download ${url}: empty response body`)
|
||||
}
|
||||
|
||||
const tempFilePath = path.join(
|
||||
path.dirname(filePath),
|
||||
`.${path.basename(filePath)}.${crypto.randomUUID()}.tmp`
|
||||
)
|
||||
try {
|
||||
await pipeline(
|
||||
Readable.fromWeb(rsp.body),
|
||||
fs.createWriteStream(tempFilePath)
|
||||
)
|
||||
await moveFile(tempFilePath, filePath)
|
||||
} catch (err) {
|
||||
await fs.promises.rm(tempFilePath, { force: true })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUrl(url: string): Promise<Response> {
|
||||
warnIfFetchMayIgnoreProxy()
|
||||
const rsp = await fetch(url, {
|
||||
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
||||
})
|
||||
if (!rsp.ok) {
|
||||
throw new Error(
|
||||
`Failed to download ${url}: ${rsp.status} ${rsp.statusText}`
|
||||
)
|
||||
}
|
||||
return rsp
|
||||
}
|
||||
|
||||
function warnIfFetchMayIgnoreProxy(): void {
|
||||
if (warnedFetchProxy) return
|
||||
warnedFetchProxy = true
|
||||
|
||||
const proxyEnvVar = ACTIVE_PROXY_ENV_VARS.find(name => process.env[name])
|
||||
if (!proxyEnvVar) return
|
||||
if (process.env.NODE_USE_ENV_PROXY === '1') return
|
||||
if (process.execArgv.includes('--use-env-proxy')) return
|
||||
|
||||
core.warning(
|
||||
`${proxyEnvVar} is set, but Node env proxy support is not enabled. ` +
|
||||
'Set NODE_USE_ENV_PROXY=1 at the workflow or job level if mise downloads need to use HTTP_PROXY, HTTPS_PROXY, or NO_PROXY.'
|
||||
)
|
||||
}
|
||||
|
||||
async function setToolVersions(): Promise<void> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue