13
0
Fork 0
mirror of https://github.com/jdx/mise-action.git synced 2026-06-29 00:00:44 +00:00

fix: fall back to wget when curl is unavailable

Keep streaming curl|tar downloads for speed and use wget only when curl
is missing, instead of routing downloads through Node fetch.
This commit is contained in:
Taku Kodma 2026-06-17 10:07:18 +10:00
parent 8e185e2855
commit 0e80c99049
No known key found for this signature in database
GPG key ID: 2FA149ECEAB1E16D
5 changed files with 108 additions and 207 deletions

View file

@ -2,4 +2,4 @@
npm ci
npm run all
git add dist
git add -f dist

View file

@ -116,19 +116,6 @@ 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

132
dist/index.js generated vendored
View file

@ -49,7 +49,6 @@ 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 */
@ -89247,14 +89246,7 @@ 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;
let cachedDownloadTool;
async function run() {
try {
await setToolVersions();
@ -89275,8 +89267,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 Node's
// `fetch`, which doesn't go through mise's HTTP layer —
// Note: `setupMise` fetches the mise binary itself with
// `curl` or `wget`, 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
@ -89504,19 +89496,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 moveFile(path$1.join(extractedMiseBinDir, 'mise.exe'), miseBinPath);
await mv(path$1.join(extractedMiseBinDir, 'mise.exe'), miseBinPath);
await installWindowsMiseShim(extractedMiseBinDir, miseShimPath);
});
break;
}
case '.tar.zst':
await installFromTar(url, 'mise.tar.zst', ['--zstd'], miseBinPath);
await installFromTarUrl(url, '--zstd -xf -', miseBinPath);
break;
case '.tar.gz':
await installFromTar(url, 'mise.tar.gz', ['-z'], miseBinPath);
await installFromTarUrl(url, '-xzf -', miseBinPath);
break;
default:
await downloadFile(url, miseBinPath);
await downloadToFile(url, miseBinPath);
await exec('chmod', ['+x', miseBinPath]);
break;
}
@ -89554,7 +89546,7 @@ async function withExtractedZip(url, archiveName, fn) {
try {
const archivePath = path$1.join(tempDir, archiveName);
const extractDir = path$1.join(tempDir, 'extract');
await downloadFile(url, archivePath);
await downloadToFile(url, archivePath);
await exec('unzip', [archivePath, '-d', extractDir]);
await fn(extractDir);
}
@ -89570,7 +89562,7 @@ async function installWindowsMiseShim(extractedMiseBinDir, miseShimPath) {
info('mise-shim.exe not found in the mise archive; skipping');
return;
}
await moveFile(extractedMiseShimPath, miseShimPath);
await mv(extractedMiseShimPath, miseShimPath);
}
async function ensureWindowsMiseShim(miseBinPath, miseShimPath, version) {
if (process.platform !== 'win32')
@ -89590,33 +89582,50 @@ 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);
async function getDownloadTool() {
if (cachedDownloadTool)
return cachedDownloadTool;
if (await which('curl', true)) {
cachedDownloadTool = 'curl';
}
finally {
await rmRF(tempDir);
else if (await which('wget', true)) {
cachedDownloadTool = 'wget';
}
else {
throw new Error('Neither curl nor wget is available to download mise');
}
info(`Using ${cachedDownloadTool} to download mise`);
return cachedDownloadTool;
}
async function downloadToStdoutShell(url) {
const tool = await getDownloadTool();
return tool === 'curl' ? `curl -fsSL ${url}` : `wget -qO- ${url}`;
}
async function downloadToFile(url, filePath) {
const tool = await getDownloadTool();
if (tool === 'curl') {
await exec('curl', ['-fsSL', url, '--output', filePath]);
}
else {
await exec('wget', ['-qO', filePath, url]);
}
}
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);
async function downloadText(url) {
const tool = await getDownloadTool();
if (tool === 'curl') {
const rsp = await getExecOutput('curl', ['-fsSL', url]);
return rsp.stdout.trim();
}
const rsp = await getExecOutput('wget', ['-qO-', url]);
return rsp.stdout.trim();
}
function isCrossDeviceRename(err) {
return (err instanceof Error &&
'code' in err &&
err.code === 'EXDEV');
async function installFromTarUrl(url, tarFlags, miseBinPath) {
const tmpdir = os.tmpdir();
const download = await downloadToStdoutShell(url);
await exec('sh', [
'-c',
`${download} | tar ${tarFlags} -C ${tmpdir} && mv ${tmpdir}/mise/bin/mise ${miseBinPath}`
]);
}
async function getInstalledMiseVersion(miseBinPath) {
const versionOutput = await getExecOutput(miseBinPath, ['version', '--json'], { silent: true });
@ -89636,50 +89645,7 @@ async function zstdInstalled() {
}
}
async function latestMiseVersion() {
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.');
return downloadText('https://mise.jdx.dev/VERSION');
}
async function setToolVersions() {
const toolVersions = getInput('tool_versions');

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View file

@ -7,8 +7,6 @@ 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
@ -47,14 +45,9 @@ 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
type DownloadTool = 'curl' | 'wget'
let cachedDownloadTool: DownloadTool | undefined
async function run(): Promise<void> {
try {
@ -77,8 +70,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 Node's
// `fetch`, which doesn't go through mise's HTTP layer —
// Note: `setupMise` fetches the mise binary itself with
// `curl` or `wget`, 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
@ -345,22 +338,19 @@ async function setupMise(
case '.zip': {
await withExtractedZip(url, 'mise.zip', async extractDir => {
const extractedMiseBinDir = path.join(extractDir, 'mise', 'bin')
await moveFile(
path.join(extractedMiseBinDir, 'mise.exe'),
miseBinPath
)
await io.mv(path.join(extractedMiseBinDir, 'mise.exe'), miseBinPath)
await installWindowsMiseShim(extractedMiseBinDir, miseShimPath)
})
break
}
case '.tar.zst':
await installFromTar(url, 'mise.tar.zst', ['--zstd'], miseBinPath)
await installFromTarUrl(url, '--zstd -xf -', miseBinPath)
break
case '.tar.gz':
await installFromTar(url, 'mise.tar.gz', ['-z'], miseBinPath)
await installFromTarUrl(url, '-xzf -', miseBinPath)
break
default:
await downloadFile(url, miseBinPath)
await downloadToFile(url, miseBinPath)
await exec.exec('chmod', ['+x', miseBinPath])
break
}
@ -409,7 +399,7 @@ async function withExtractedZip(
const archivePath = path.join(tempDir, archiveName)
const extractDir = path.join(tempDir, 'extract')
await downloadFile(url, archivePath)
await downloadToFile(url, archivePath)
await exec.exec('unzip', [archivePath, '-d', extractDir])
await fn(extractDir)
} finally {
@ -429,7 +419,7 @@ async function installWindowsMiseShim(
return
}
await moveFile(extractedMiseShimPath, miseShimPath)
await io.mv(extractedMiseShimPath, miseShimPath)
}
async function ensureWindowsMiseShim(
@ -463,41 +453,54 @@ async function ensureWindowsMiseShim(
}
}
async function installFromTar(
async function getDownloadTool(): Promise<DownloadTool> {
if (cachedDownloadTool) return cachedDownloadTool
if (await io.which('curl', true)) {
cachedDownloadTool = 'curl'
} else if (await io.which('wget', true)) {
cachedDownloadTool = 'wget'
} else {
throw new Error('Neither curl nor wget is available to download mise')
}
core.info(`Using ${cachedDownloadTool} to download mise`)
return cachedDownloadTool
}
async function downloadToStdoutShell(url: string): Promise<string> {
const tool = await getDownloadTool()
return tool === 'curl' ? `curl -fsSL ${url}` : `wget -qO- ${url}`
}
async function downloadToFile(url: string, filePath: string): Promise<void> {
const tool = await getDownloadTool()
if (tool === 'curl') {
await exec.exec('curl', ['-fsSL', url, '--output', filePath])
} else {
await exec.exec('wget', ['-qO', filePath, url])
}
}
async function downloadText(url: string): Promise<string> {
const tool = await getDownloadTool()
if (tool === 'curl') {
const rsp = await exec.getExecOutput('curl', ['-fsSL', url])
return rsp.stdout.trim()
}
const rsp = await exec.getExecOutput('wget', ['-qO-', url])
return rsp.stdout.trim()
}
async function installFromTarUrl(
url: string,
archiveName: string,
tarArgs: string[],
tarFlags: 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'
)
const tmpdir = os.tmpdir()
const download = await downloadToStdoutShell(url)
await exec.exec('sh', [
'-c',
`${download} | tar ${tarFlags} -C ${tmpdir} && mv ${tmpdir}/mise/bin/mise ${miseBinPath}`
])
}
async function getInstalledMiseVersion(miseBinPath: string): Promise<string> {
@ -524,62 +527,7 @@ async function zstdInstalled(): Promise<boolean> {
}
async function latestMiseVersion(): Promise<string> {
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.'
)
return downloadText('https://mise.jdx.dev/VERSION')
}
async function setToolVersions(): Promise<void> {