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.** + +[![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 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,