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__/**"]
}
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.**
+
+[](https://github.com/harehare/setup-mq/actions/workflows/ci.yml)
+[](https://github.com/marketplace/actions/setup-mq)
+[](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
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}${tag}>`;
+ }
+ /**
+ * 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,