From 0f15feecd98e3727ae84cc8a74d583fece8e4223 Mon Sep 17 00:00:00 2001 From: harehare Date: Tue, 18 Aug 2026 23:53:24 +0900 Subject: [PATCH 1/3] docs: improve README and add missing MIT license file README referenced a LICENSE file and MIT license that never existed in the repo. Also add badges, platform support table, and a development section, aligned with the harehare/mq README style. --- LICENSE | 21 +++++++++++++++++++++ README.md | 46 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..18074fe --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Takahiro Sato + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 995ff0d..0b3c67a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ +
+ # Setup mq -This GitHub Action will setup [mq](https://github.com/harehare/mq) in your GitHub Actions workflow, allowing you +**Set up [mq](https://github.com/harehare/mq) in your GitHub Actions workflow.** + +[![ci](https://img.shields.io/github/actions/workflow/status/harehare/setup-mq/ci.yml?style=flat-square&logo=github-actions&label=ci)](https://github.com/harehare/setup-mq/actions/workflows/ci.yml) +[![marketplace](https://img.shields.io/badge/marketplace-Setup%20mq-2088FF?style=flat-square&logo=githubactions&logoColor=white)](https://github.com/marketplace/actions/setup-mq) +[![LICENCE](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) + +
+ +This GitHub Action sets up [mq](https://github.com/harehare/mq) in your GitHub Actions workflow, allowing you to easily integrate mq into your CI/CD pipeline. ## Usage @@ -18,7 +28,9 @@ steps: ### With additional binaries -You can install additional binaries from `mq-XXX` repositories using the `bins` option. +mq ships companion tools (`lsp`, `dbg`, `test`, `crawl`) as part of its own releases, and other tools +are distributed from separate `mq-XXX` repositories (e.g. `mq-foo`). Use the `bins` option to install +either kind by name. ```yaml steps: @@ -27,17 +39,37 @@ steps: uses: harehare/setup-mq@v1 with: version: 'v0.1.0' - bins: 'foo,bar' # Installs binaries from mq-foo and mq-bar repositories + bins: 'crawl,foo' # Installs mq-crawl from the mq release, and mq-foo from the mq-foo repository - name: Run mq run: echo "# Test" | mq '.h' ``` ## Inputs -| Name | Description | Required | Default | -| --------- | ---------------------------------------------------------------------------- | -------- | -------------- | -| `version` | mq version to install | No | Latest version | -| `bins` | Comma-separated list of additional binaries to install from `mq-XXX` repositories | No | `''` | +| Name | Description | Required | Default | +| -------------- | --------------------------------------------------------------------------------- | -------- | --------------------- | +| `version` | mq version to install | No | Latest version | +| `bins` | Comma-separated list of additional binaries to install from `mq-XXX` repositories | No | `''` | +| `github-token` | Token used to query the GitHub Releases API, mainly to avoid rate limiting | No | `${{ github.token }}` | + +## Supported platforms + +| OS | Architecture | Notes | +| ------- | ------------ | ---------------------------------------------- | +| Linux | x64, arm64 | glibc and musl (e.g. Alpine) are auto-detected | +| macOS | arm64 | | +| Windows | x64, arm64 | | + +A job summary showing the installed tool versions is written to the workflow run automatically. + +## Development + +```bash +npm install +npm test +npm run lint +npm run bundle # builds dist/index.js +``` ## License From 395a9b0adfa2629c0484462c16ddce34f4b627a4 Mon Sep 17 00:00:00 2001 From: harehare Date: Tue, 18 Aug 2026 23:53:29 +0900 Subject: [PATCH 2/3] fix(lint): drop non-existent typescript/naming-convention rule oxlint 1.78.0 tightened config validation and now rejects rules that aren't actually implemented by a plugin. typescript/naming-convention was never a real oxlint rule (likely copied from an ESLint config with @typescript-eslint/naming-convention), so npm run lint was failing to even parse the config. --- .oxlintrc.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 04d3036..445ef27 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,7 +1,4 @@ { "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/crates/oxc_linter/src/options/oxlintrc.schema.json", - "rules": { - "typescript/naming-convention": "off" - }, "ignorePatterns": ["rollup.config.ts", "vitest.config.ts", "__tests__/**"] } From f0add11f65a44d3259755b956c5b8d22e4d9033b Mon Sep 17 00:00:00 2001 From: harehare Date: Tue, 18 Aug 2026 23:53:39 +0900 Subject: [PATCH 3/3] feat: detect musl runners, write job summary, fail fast on missing asset - Auto-detect glibc vs musl on Linux (via /etc/alpine-release) and pick the matching mq release asset, so the action now works on Alpine-based runners/containers, which it previously couldn't install onto at all. - Write a GITHUB_STEP_SUMMARY table listing the installed tool versions and platform for each run. - setupMq now throws instead of silently logging and returning when no matching platform asset is found, so the action fails loudly instead of leaving mq off PATH with no indication why. dist/index.js rebuilt via npm run package. --- __tests__/main.test.ts | 121 +++++++++++++-- dist/index.js | 342 +++++++++++++++++++++++++++++++++++++++-- src/main.ts | 102 +++++++++--- 3 files changed, 519 insertions(+), 46 deletions(-) diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index c7a8e80..479792c 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { promises as fs } from 'node:fs'; import * as core from '@actions/core'; import * as github from '@actions/github'; import * as tc from '@actions/tool-cache'; @@ -17,6 +18,8 @@ vi.mock('node:fs', () => ({ copyFile: vi.fn(), chmod: vi.fn(), mkdir: vi.fn(), + // Rejects by default, simulating a glibc-based (non-Alpine) runner. + access: vi.fn().mockRejectedValue(new Error('ENOENT')), }, })); @@ -33,6 +36,12 @@ vi.mock('@actions/core', () => { return ''; }); + const summary = { + addHeading: vi.fn().mockReturnThis(), + addTable: vi.fn().mockReturnThis(), + write: vi.fn().mockResolvedValue(undefined), + }; + return { getInput, info: vi.fn(), @@ -40,6 +49,7 @@ vi.mock('@actions/core', () => { warning: vi.fn(), setFailed: vi.fn(), addPath: vi.fn(), + summary, }; }); @@ -77,6 +87,10 @@ vi.mock('@actions/tool-cache', () => ({ return 'latest_tool/mq-crawl'; } + if (url === 'latest_url/mq-musl') { + return 'latest_tool/mq-musl'; + } + return ''; }), extractTar: vi.fn().mockResolvedValue('/path/to/extracted/directory'), @@ -114,6 +128,10 @@ vi.mock('@actions/github', () => ({ browser_download_url: 'latest_url/mq-crawl', name: 'mq-crawl-x86_64-unknown-linux-gnu', }, + { + browser_download_url: 'latest_url/mq-musl', + name: 'mq-x86_64-unknown-linux-musl', + }, ], }, }; @@ -186,6 +204,11 @@ vi.mock('@actions/github', () => ({ describe('GitHub Action', () => { beforeEach(() => { vi.clearAllMocks(); + // afterEach's resetAllMocks() wipes the module-mock default, so restore it here. + vi.mocked(fs.access).mockRejectedValue(new Error('ENOENT')); + vi.mocked(core.summary.addHeading).mockReturnThis(); + vi.mocked(core.summary.addTable).mockReturnThis(); + vi.mocked(core.summary.write).mockResolvedValue(undefined as any); }); afterEach(() => { @@ -267,12 +290,8 @@ describe('GitHub Action', () => { expect(tc.downloadTool).toHaveBeenCalledWith('bin_bar_url/mq-bar'); // Verify info messages for additional bins - expect(core.info).toHaveBeenCalledWith( - expect.stringContaining('foo'), - ); - expect(core.info).toHaveBeenCalledWith( - expect.stringContaining('bar'), - ); + expect(core.info).toHaveBeenCalledWith(expect.stringContaining('foo')); + expect(core.info).toHaveBeenCalledWith(expect.stringContaining('bar')); }); it('should handle bins with whitespace correctly', async () => { @@ -319,9 +338,7 @@ describe('GitHub Action', () => { await run(); expect(tc.downloadTool).toHaveBeenCalledWith('bin_foo_url/mq-foo'); - expect(core.info).toHaveBeenCalledWith( - expect.stringContaining('foo'), - ); + expect(core.info).toHaveBeenCalledWith(expect.stringContaining('foo')); }); it('should not setup bins when bins input is empty', async () => { @@ -425,6 +442,92 @@ describe('GitHub Action', () => { expect(tc.downloadTool).toHaveBeenCalledWith('bin_foo_url/mq-foo'); }); + it('should install the musl asset on an Alpine-based runner', async () => { + vi.mocked(fs.access).mockImplementationOnce(async (target) => { + if (target === '/etc/alpine-release') { + return undefined; + } + throw new Error('ENOENT'); + }); + + vi.mocked(core.getInput).mockImplementation((name) => { + if (name === 'version') { + return ''; + } + + if (name === 'github-token') { + return 'fake-token'; + } + + return ''; + }); + + await run(); + + expect(tc.downloadTool).toHaveBeenCalledWith('latest_url/mq-musl'); + expect(core.addPath).toHaveBeenCalledWith('latest_tool'); + }); + + it('should write a job summary after a successful setup', async () => { + vi.mocked(core.getInput).mockImplementation((name) => { + if (name === 'version') { + return 'v0.1.0'; + } + + if (name === 'github-token') { + return 'fake-token'; + } + + return ''; + }); + + await run(); + + expect(core.summary.addHeading).toHaveBeenCalledWith('Setup mq', 2); + expect(core.summary.addTable).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.arrayContaining(['mq', 'v0.1.0', 'linux_x64_gnu']), + ]), + ); + expect(core.summary.write).toHaveBeenCalled(); + }); + + it('should fail when the mq release has no matching asset for the platform', async () => { + const octokit = vi.mocked(github.getOctokit); + octokit.mockReturnValue({ + rest: { + repos: { + getLatestRelease: vi.fn(async () => ({ + data: { + tag_name: 'v1.0.0', + assets: [], + }, + })), + getReleaseByTag: vi.fn(), + }, + }, + } as any); + + vi.mocked(core.getInput).mockImplementation((name) => { + if (name === 'version') { + return ''; + } + + if (name === 'github-token') { + return 'fake-token'; + } + + return ''; + }); + + await run(); + + expect(core.setFailed).toHaveBeenCalledWith( + expect.stringContaining('Not Found mq'), + ); + expect(core.addPath).not.toHaveBeenCalled(); + }); + it('should warn when a bin release has no matching asset', async () => { const octokit = vi.mocked(github.getOctokit); octokit.mockReturnValue({ diff --git a/dist/index.js b/dist/index.js index fcae113..edbe294 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,4 +1,4 @@ -import process$1 from'node:process';import*as os$1 from'node:os';import*as path$1 from'node:path';import {promises as promises$1}from'node:fs';import*as os from'os';import os__default,{EOL}from'os';import*as crypto from'crypto';import*as fs from'fs';import {promises,existsSync,readFileSync}from'fs';import*as path from'path';import*as http from'http';import http__default from'http';import*as https from'https';import https__default from'https';import'net';import require$$1 from'tls';import events$1 from'events';import {ok}from'assert';import*as require$$6 from'util';import require$$6__default from'util';import require$$0$1 from'node:assert';import require$$0$3 from'node:net';import require$$2 from'node:http';import require$$0$2 from'node:stream';import require$$0 from'node:buffer';import require$$0$4 from'node:util';import require$$7 from'node:querystring';import require$$8 from'node:events';import require$$0$5 from'node:diagnostics_channel';import require$$5 from'node:tls';import require$$1$2 from'node:zlib';import require$$5$1 from'node:perf_hooks';import require$$8$1 from'node:util/types';import require$$1$1 from'node:worker_threads';import require$$1$3 from'node:url';import require$$5$2 from'node:async_hooks';import require$$1$4 from'node:console';import require$$1$5 from'node:dns';import require$$5$3 from'string_decoder';import'child_process';import'timers';import*as stream from'stream';// We use any as a valid input type +import process$1 from'node:process';import*as os$1 from'node:os';import*as path$1 from'node:path';import {promises as promises$1}from'node:fs';import*as os from'os';import os__default,{EOL}from'os';import*as crypto from'crypto';import*as fs from'fs';import {promises,constants as constants$6,existsSync,readFileSync}from'fs';import*as path from'path';import*as http from'http';import http__default from'http';import*as https from'https';import https__default from'https';import'net';import require$$1 from'tls';import events$1 from'events';import {ok}from'assert';import*as require$$6 from'util';import require$$6__default from'util';import require$$0$1 from'node:assert';import require$$0$3 from'node:net';import require$$2 from'node:http';import require$$0$2 from'node:stream';import require$$0 from'node:buffer';import require$$0$4 from'node:util';import require$$7 from'node:querystring';import require$$8 from'node:events';import require$$0$5 from'node:diagnostics_channel';import require$$5 from'node:tls';import require$$1$2 from'node:zlib';import require$$5$1 from'node:perf_hooks';import require$$8$1 from'node:util/types';import require$$1$1 from'node:worker_threads';import require$$1$3 from'node:url';import require$$5$2 from'node:async_hooks';import require$$1$4 from'node:console';import require$$1$5 from'node:dns';import require$$5$3 from'string_decoder';import'child_process';import'timers';import*as stream from'stream';// We use any as a valid input type /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Sanitizes an input into a string so it can be passed into issueCommand safely @@ -27582,7 +27582,7 @@ var MediaTypes$1; function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); -};(undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { +};var __awaiter$6 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } @@ -27591,7 +27591,269 @@ var MediaTypes$1; step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; -const { access, appendFile, writeFile } = promises;var __awaiter$5 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { +const { access, appendFile, writeFile } = promises; +const SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY'; +class Summary { + constructor() { + this._buffer = ''; + } + /** + * Finds the summary file path from the environment, rejects if env var is not found or file does not exist + * Also checks r/w permissions. + * + * @returns step summary file path + */ + filePath() { + return __awaiter$6(this, void 0, void 0, function* () { + if (this._filePath) { + return this._filePath; + } + const pathFromEnv = process.env[SUMMARY_ENV_VAR]; + if (!pathFromEnv) { + throw new Error(`Unable to find environment variable for $${SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.`); + } + try { + yield access(pathFromEnv, constants$6.R_OK | constants$6.W_OK); + } + catch (_a) { + throw new Error(`Unable to access summary file: '${pathFromEnv}'. Check if the file has correct read/write permissions.`); + } + this._filePath = pathFromEnv; + return this._filePath; + }); + } + /** + * Wraps content in an HTML tag, adding any HTML attributes + * + * @param {string} tag HTML tag to wrap + * @param {string | null} content content within the tag + * @param {[attribute: string]: string} attrs key-value list of HTML attributes to add + * + * @returns {string} content wrapped in HTML element + */ + wrap(tag, content, attrs = {}) { + const htmlAttrs = Object.entries(attrs) + .map(([key, value]) => ` ${key}="${value}"`) + .join(''); + if (!content) { + return `<${tag}${htmlAttrs}>`; + } + return `<${tag}${htmlAttrs}>${content}`; + } + /** + * Writes text in the buffer to the summary buffer file and empties buffer. Will append by default. + * + * @param {SummaryWriteOptions} [options] (optional) options for write operation + * + * @returns {Promise} summary instance + */ + write(options) { + return __awaiter$6(this, void 0, void 0, function* () { + const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite); + const filePath = yield this.filePath(); + const writeFunc = overwrite ? writeFile : appendFile; + yield writeFunc(filePath, this._buffer, { encoding: 'utf8' }); + return this.emptyBuffer(); + }); + } + /** + * Clears the summary buffer and wipes the summary file + * + * @returns {Summary} summary instance + */ + clear() { + return __awaiter$6(this, void 0, void 0, function* () { + return this.emptyBuffer().write({ overwrite: true }); + }); + } + /** + * Returns the current summary buffer as a string + * + * @returns {string} string of summary buffer + */ + stringify() { + return this._buffer; + } + /** + * If the summary buffer is empty + * + * @returns {boolen} true if the buffer is empty + */ + isEmptyBuffer() { + return this._buffer.length === 0; + } + /** + * Resets the summary buffer without writing to summary file + * + * @returns {Summary} summary instance + */ + emptyBuffer() { + this._buffer = ''; + return this; + } + /** + * Adds raw text to the summary buffer + * + * @param {string} text content to add + * @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false) + * + * @returns {Summary} summary instance + */ + addRaw(text, addEOL = false) { + this._buffer += text; + return addEOL ? this.addEOL() : this; + } + /** + * Adds the operating system-specific end-of-line marker to the buffer + * + * @returns {Summary} summary instance + */ + addEOL() { + return this.addRaw(EOL); + } + /** + * Adds an HTML codeblock to the summary buffer + * + * @param {string} code content to render within fenced code block + * @param {string} lang (optional) language to syntax highlight code + * + * @returns {Summary} summary instance + */ + addCodeBlock(code, lang) { + const attrs = Object.assign({}, (lang && { lang })); + const element = this.wrap('pre', this.wrap('code', code), attrs); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML list to the summary buffer + * + * @param {string[]} items list of items to render + * @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false) + * + * @returns {Summary} summary instance + */ + addList(items, ordered = false) { + const tag = ordered ? 'ol' : 'ul'; + const listItems = items.map(item => this.wrap('li', item)).join(''); + const element = this.wrap(tag, listItems); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML table to the summary buffer + * + * @param {SummaryTableCell[]} rows table rows + * + * @returns {Summary} summary instance + */ + addTable(rows) { + const tableBody = rows + .map(row => { + const cells = row + .map(cell => { + if (typeof cell === 'string') { + return this.wrap('td', cell); + } + const { header, data, colspan, rowspan } = cell; + const tag = header ? 'th' : 'td'; + const attrs = Object.assign(Object.assign({}, (colspan && { colspan })), (rowspan && { rowspan })); + return this.wrap(tag, data, attrs); + }) + .join(''); + return this.wrap('tr', cells); + }) + .join(''); + const element = this.wrap('table', tableBody); + return this.addRaw(element).addEOL(); + } + /** + * Adds a collapsable HTML details element to the summary buffer + * + * @param {string} label text for the closed state + * @param {string} content collapsable content + * + * @returns {Summary} summary instance + */ + addDetails(label, content) { + const element = this.wrap('details', this.wrap('summary', label) + content); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML image tag to the summary buffer + * + * @param {string} src path to the image you to embed + * @param {string} alt text description of the image + * @param {SummaryImageOptions} options (optional) addition image attributes + * + * @returns {Summary} summary instance + */ + addImage(src, alt, options) { + const { width, height } = options || {}; + const attrs = Object.assign(Object.assign({}, (width && { width })), (height && { height })); + const element = this.wrap('img', null, Object.assign({ src, alt }, attrs)); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML section heading element + * + * @param {string} text heading text + * @param {number | string} [level=1] (optional) the heading level, default: 1 + * + * @returns {Summary} summary instance + */ + addHeading(text, level) { + const tag = `h${level}`; + const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag) + ? tag + : 'h1'; + const element = this.wrap(allowedTag, text); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML thematic break (
) to the summary buffer + * + * @returns {Summary} summary instance + */ + addSeparator() { + const element = this.wrap('hr', null); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML line break (
) to the summary buffer + * + * @returns {Summary} summary instance + */ + addBreak() { + const element = this.wrap('br', null); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML blockquote to the summary buffer + * + * @param {string} text quote text + * @param {string} cite (optional) citation url + * + * @returns {Summary} summary instance + */ + addQuote(text, cite) { + const attrs = Object.assign({}, (cite && { cite })); + const element = this.wrap('blockquote', text, attrs); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML anchor tag to the summary buffer + * + * @param {string} text link text/content + * @param {string} href hyperlink + * + * @returns {Summary} summary instance + */ + addLink(text, href) { + const element = this.wrap('a', text, { href }); + return this.addRaw(element).addEOL(); + } +} +const _summary = new Summary(); +const summary = _summary;var __awaiter$5 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } @@ -36322,9 +36584,27 @@ const PLATFORM_MAP = { darwin_arm64: 'aarch64-apple-darwin', win32_x64: 'x86_64-pc-windows-msvc.exe', win32_arm64: 'aarch64-pc-windows-msvc.exe', - linux_arm64: 'aarch64-unknown-linux-gnu', - linux_x64: 'x86_64-unknown-linux-gnu', + linux_x64_gnu: 'x86_64-unknown-linux-gnu', + linux_x64_musl: 'x86_64-unknown-linux-musl', + linux_arm64_gnu: 'aarch64-unknown-linux-gnu', + linux_arm64_musl: 'aarch64-unknown-linux-musl', }; +// Alpine is the de facto standard musl-based distro for CI/containers, and always ships this file. +async function detectLinuxLibc() { + try { + await promises$1.access('/etc/alpine-release'); + return 'musl'; + } + catch { + return 'gnu'; + } +} +async function getPlatformKey(platform, arch) { + if (platform === 'linux') { + return `linux_${arch}_${await detectLinuxLibc()}`; + } + return `${platform}_${arch}`; +} async function run() { try { const { arch, platform } = process$1; @@ -36339,8 +36619,10 @@ async function run() { const version = getInput('version'); const token = getInput('github-token'); const binsInput = getInput('bins'); + const platformKey = await getPlatformKey(platform, arch); // Setup main mq tool - await setupMq(token, platform, arch, version); + const mqResult = await setupMq(token, platformKey, arch, version); + let binResults = []; // Setup additional bins from mq-XXX repositories if (binsInput) { const bins = binsInput @@ -36350,9 +36632,11 @@ async function run() { if (bins.length > 0) { await promises$1.mkdir(MQ_BIN_DIR, { recursive: true }); addPath(MQ_BIN_DIR); - await Promise.all(bins.map(async (bin) => setupAdditionalBin(token, platform, arch, bin, version))); + const results = await Promise.all(bins.map(async (bin) => setupAdditionalBin(token, platformKey, bin, version))); + binResults = results.filter((r) => r !== undefined); } } + await writeSummary(platformKey, mqResult, binResults); } catch (error) { console.log('error', error); @@ -36364,17 +36648,16 @@ async function run() { } } } -async function setupMq(token, platform, arch, version) { +async function setupMq(token, platformKey, arch, version) { const release = await getRelease({ token, repo: REPO, toolName: TOOL_NAME, - platform: `${platform}_${arch}`, + platform: platformKey, version, }); if (!release.url || !release.version) { - info(`Not Found ${TOOL_NAME} version ${version} for ${platform}-${arch}`); - return; + throw new Error(`Not Found ${TOOL_NAME} version ${version} for ${platformKey}`); } let toolPath = find(TOOL_NAME, release.version, arch); const isAct = process$1.env.ACT === 'true'; @@ -36391,28 +36674,53 @@ async function setupMq(token, platform, arch, version) { } } addPath(toolPath); - info(`Setting up ${TOOL_NAME} version ${version} for ${platform}-${arch}`); + info(`Setting up ${TOOL_NAME} version ${release.version} for ${platformKey}`); + return { version: release.version, path: toolPath }; } -async function setupAdditionalBin(token, platform, arch, bin, version) { +async function setupAdditionalBin(token, platformKey, bin, version) { const isBundled = MQ_BUNDLED_TOOLS.has(bin); const repo = isBundled ? REPO : `mq-${bin}`; - const toolName = isBundled ? `mq-${bin}` : bin.startsWith('mq-') ? bin : `mq-${bin}`; + const toolName = isBundled + ? `mq-${bin}` + : bin.startsWith('mq-') + ? bin + : `mq-${bin}`; const release = await getRelease({ token, repo, toolName, - platform: `${platform}_${arch}`, + platform: platformKey, version: isBundled ? version : undefined, }); if (!release.url || !release.version) { - warning(`Not Found ${toolName} for ${platform}-${arch} in ${repo}`); - return; + warning(`Not Found ${toolName} for ${platformKey} in ${repo}`); + return undefined; } const downloadPath = await downloadTool(release.url); const binPath = path$1.join(MQ_BIN_DIR, toolName); await promises$1.copyFile(downloadPath, binPath); await promises$1.chmod(binPath, '755'); info(`Setting up ${toolName} version ${release.version} from ${repo}`); + return { name: toolName, version: release.version }; +} +async function writeSummary(platformKey, mqResult, binResults) { + try { + await summary + .addHeading('Setup mq', 2) + .addTable([ + [ + { data: 'Tool', header: true }, + { data: 'Version', header: true }, + { data: 'Platform', header: true }, + ], + [TOOL_NAME, mqResult.version, platformKey], + ...binResults.map((bin) => [bin.name, bin.version, platformKey]), + ]) + .write(); + } + catch (error) { + warning(`Failed to write job summary: ${error instanceof Error ? error.message : 'Unknown error'}`); + } } async function getRelease(options) { const { token, repo, toolName, platform, version } = options; diff --git a/src/main.ts b/src/main.ts index 13d30e3..afb827e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,12 +19,35 @@ const PLATFORM_MAP = { darwin_arm64: 'aarch64-apple-darwin', win32_x64: 'x86_64-pc-windows-msvc.exe', win32_arm64: 'aarch64-pc-windows-msvc.exe', - linux_arm64: 'aarch64-unknown-linux-gnu', - linux_x64: 'x86_64-unknown-linux-gnu', + linux_x64_gnu: 'x86_64-unknown-linux-gnu', + linux_x64_musl: 'x86_64-unknown-linux-musl', + linux_arm64_gnu: 'aarch64-unknown-linux-gnu', + linux_arm64_musl: 'aarch64-unknown-linux-musl', } as const; type Platform = keyof typeof PLATFORM_MAP; +// Alpine is the de facto standard musl-based distro for CI/containers, and always ships this file. +async function detectLinuxLibc(): Promise<'gnu' | 'musl'> { + try { + await fs.access('/etc/alpine-release'); + return 'musl'; + } catch { + return 'gnu'; + } +} + +async function getPlatformKey( + platform: 'darwin' | 'win32' | 'linux', + arch: 'x64' | 'arm64', +): Promise { + if (platform === 'linux') { + return `linux_${arch}_${await detectLinuxLibc()}` as Platform; + } + + return `${platform}_${arch}` as Platform; +} + type Release = { version?: string; url?: string; @@ -55,9 +78,11 @@ export async function run(): Promise { const version: string = core.getInput('version'); const token = core.getInput('github-token'); const binsInput: string = core.getInput('bins'); + const platformKey = await getPlatformKey(platform, arch); // Setup main mq tool - await setupMq(token, platform, arch, version); + const mqResult = await setupMq(token, platformKey, arch, version); + let binResults: { name: string; version: string }[] = []; // Setup additional bins from mq-XXX repositories if (binsInput) { @@ -70,13 +95,17 @@ export async function run(): Promise { await fs.mkdir(MQ_BIN_DIR, { recursive: true }); core.addPath(MQ_BIN_DIR); - await Promise.all( + const results = await Promise.all( bins.map(async (bin) => - setupAdditionalBin(token, platform, arch, bin, version), + setupAdditionalBin(token, platformKey, bin, version), ), ); + + binResults = results.filter((r) => r !== undefined); } } + + await writeSummary(platformKey, mqResult, binResults); } catch (error) { console.log('error', error); if (error instanceof Error) { @@ -89,23 +118,22 @@ export async function run(): Promise { async function setupMq( token: string, - platform: string, + platformKey: Platform, arch: string, version: string, -): Promise { +): Promise<{ version: string; path: string }> { const release = await getRelease({ token, repo: REPO, toolName: TOOL_NAME, - platform: `${platform}_${arch}` as Platform, + platform: platformKey, version, }); if (!release.url || !release.version) { - core.info( - `Not Found ${TOOL_NAME} version ${version} for ${platform}-${arch}`, + throw new Error( + `Not Found ${TOOL_NAME} version ${version} for ${platformKey}`, ); - return; } let toolPath = tc.find(TOOL_NAME, release.version, arch); @@ -132,31 +160,36 @@ async function setupMq( core.addPath(toolPath); core.info( - `Setting up ${TOOL_NAME} version ${version} for ${platform}-${arch}`, + `Setting up ${TOOL_NAME} version ${release.version} for ${platformKey}`, ); + + return { version: release.version, path: toolPath }; } async function setupAdditionalBin( token: string, - platform: string, - arch: string, + platformKey: Platform, bin: string, version?: string, -): Promise { +): Promise<{ name: string; version: string } | undefined> { const isBundled = MQ_BUNDLED_TOOLS.has(bin); const repo = isBundled ? REPO : `mq-${bin}`; - const toolName = isBundled ? `mq-${bin}` : bin.startsWith('mq-') ? bin : `mq-${bin}`; + const toolName = isBundled + ? `mq-${bin}` + : bin.startsWith('mq-') + ? bin + : `mq-${bin}`; const release = await getRelease({ token, repo, toolName, - platform: `${platform}_${arch}` as Platform, + platform: platformKey, version: isBundled ? version : undefined, }); if (!release.url || !release.version) { - core.warning(`Not Found ${toolName} for ${platform}-${arch} in ${repo}`); - return; + core.warning(`Not Found ${toolName} for ${platformKey} in ${repo}`); + return undefined; } const downloadPath = await tc.downloadTool(release.url); @@ -166,6 +199,33 @@ async function setupAdditionalBin( await fs.chmod(binPath, '755'); core.info(`Setting up ${toolName} version ${release.version} from ${repo}`); + + return { name: toolName, version: release.version }; +} + +async function writeSummary( + platformKey: Platform, + mqResult: { version: string; path: string }, + binResults: { name: string; version: string }[], +): Promise { + try { + await core.summary + .addHeading('Setup mq', 2) + .addTable([ + [ + { data: 'Tool', header: true }, + { data: 'Version', header: true }, + { data: 'Platform', header: true }, + ], + [TOOL_NAME, mqResult.version, platformKey], + ...binResults.map((bin) => [bin.name, bin.version, platformKey]), + ]) + .write(); + } catch (error) { + core.warning( + `Failed to write job summary: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); + } } async function getRelease(options: GetReleaseOptions): Promise { @@ -186,7 +246,9 @@ async function getRelease(options: GetReleaseOptions): Promise { const assetName = `${toolName}-${PLATFORM_MAP[platform]}`; core.info(`Looking for asset: ${assetName}`); - core.info(`Available assets: ${latestReleaseResponse.data.assets.map((a: any) => a.name).join(', ')}`); + core.info( + `Available assets: ${latestReleaseResponse.data.assets.map((a: any) => a.name).join(', ')}`, + ); return { version: latestReleaseResponse.data.tag_name,