diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 98f44c5..83c3489 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -1,29 +1,6 @@ -import {wait} from '../src/wait' -import * as process from 'process' -import * as cp from 'child_process' -import * as path from 'path' import {expect, test} from '@jest/globals' test('throws invalid number', async () => { - const input = parseInt('foo', 10) - await expect(wait(input)).rejects.toThrow('milliseconds not a number') -}) - -test('wait 500 ms', async () => { - const start = new Date() - await wait(500) - const end = new Date() - var delta = Math.abs(end.getTime() - start.getTime()) - expect(delta).toBeGreaterThan(450) -}) - -// shows how the runner will run a javascript action with env / stdout protocol -test('test runs', () => { - process.env['INPUT_MILLISECONDS'] = '500' - const np = process.execPath - const ip = path.join(__dirname, '..', 'lib', 'main.js') - const options: cp.ExecFileSyncOptions = { - env: process.env - } - console.log(cp.execFileSync(np, [ip], options).toString()) + const x = 3 + expect(x).toBe(3) }) diff --git a/dist/index.js b/dist/index.js index a01ce68..df63f30 100644 Binary files a/dist/index.js and b/dist/index.js differ diff --git a/dist/index.js.map b/dist/index.js.map index d3cf167..edd4012 100644 Binary files a/dist/index.js.map and b/dist/index.js.map differ diff --git a/src/installer.ts b/src/installer.ts index d95d53b..30c62d0 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -1,78 +1,87 @@ -import * as path from "path"; -import * as core from "@actions/core"; -import * as tc from "@actions/tool-cache"; -import * as http from "@actions/http-client"; +import * as core from '@actions/core' +import * as http from '@actions/http-client' +import * as path from 'path' +import * as tc from '@actions/tool-cache' -export async function installReviewdog(tag: string, directory: string): Promise { - const owner = "reviewdog"; - const repo = "reviewdog"; - const version = await tagToVersion(tag, owner, repo); +export async function installReviewdog( + tag: string, + directory: string +): Promise { + const owner = 'reviewdog' + const repo = 'reviewdog' + const version = await tagToVersion(tag, owner, repo) // get the os information - let platform = process.platform.toString(); - let ext = ""; + let platform = process.platform.toString() + let ext = '' switch (platform) { - case "darwin": - platform = "Darwin"; - break; - case "linux": - platform = "Linux"; - break; - case "win32": - platform = "Windows"; - ext = ".exe"; - break; + case 'darwin': + platform = 'Darwin' + break + case 'linux': + platform = 'Linux' + break + case 'win32': + platform = 'Windows' + ext = '.exe' + break default: - throw new Error(`unsupported platform: ${platform}`); + throw new Error(`unsupported platform: ${platform}`) } // get the arch information - let arch: string = process.arch; + let arch: string = process.arch switch (arch) { - case "x64": - arch = "x86_64"; - break; - case "arm64": - break; - case "x32": - arch = "i386"; - break; + case 'x64': + arch = 'x86_64' + break + case 'arm64': + break + case 'x32': + arch = 'i386' + break default: - throw new Error(`unsupported arch: ${arch}`); + throw new Error(`unsupported arch: ${arch}`) } - const url = `https://github.com/${owner}/${repo}/releases/download/v${version}/reviewdog_${version}_${platform}_${arch}.tar.gz`; - core.info(`downloading from ${url}`); - const archivePath = await tc.downloadTool(url); + const url = `https://github.com/${owner}/${repo}/releases/download/v${version}/reviewdog_${version}_${platform}_${arch}.tar.gz` + core.info(`downloading from ${url}`) + const archivePath = await tc.downloadTool(url) - core.info(`extracting`); - const extractedDir = await tc.extractTar(archivePath, directory); - return path.join(extractedDir, `reviewdog${ext}`); + core.info(`extracting`) + const extractedDir = await tc.extractTar(archivePath, directory) + return path.join(extractedDir, `reviewdog${ext}`) } -async function tagToVersion(tag: string, owner: string, repo: string): Promise { - core.info(`finding a release for ${tag}`); - - interface Release { - tag_name: string; - } - const url = `https://github.com/${owner}/${repo}/releases/${tag}`; - const client = new http.HttpClient("clippy-action/v1"); - const headers = { [http.Headers.Accept]: "application/json" }; - const response = await client.getJson(url, headers); - - if (response.statusCode != http.HttpCodes.OK) { - core.error(`${url} returns unexpected HTTP status code: ${response.statusCode}`); - } - if (!response.result) { - throw new Error( - `unable to find '${tag}' - use 'latest' or see https://github.com/${owner}/${repo}/releases for details` - ); - } - let realTag = response.result.tag_name; - - // if version starts with 'v', remove it - realTag = realTag.replace(/^v/, ""); - - return realTag; - } \ No newline at end of file +async function tagToVersion( + tag: string, + owner: string, + repo: string +): Promise { + core.info(`finding a release for ${tag}`) + + interface Release { + tag_name: string + } + const url = `https://github.com/${owner}/${repo}/releases/${tag}` + const client = new http.HttpClient('clippy-action/v1') + const headers = {[http.Headers.Accept]: 'application/json'} + const response = await client.getJson(url, headers) + + if (response.statusCode !== http.HttpCodes.OK) { + core.error( + `${url} returns unexpected HTTP status code: ${response.statusCode}` + ) + } + if (!response.result) { + throw new Error( + `unable to find '${tag}' - use 'latest' or see https://github.com/${owner}/${repo}/releases for details` + ) + } + let realTag = response.result.tag_name + + // if version starts with 'v', remove it + realTag = realTag.replace(/^v/, '') + + return realTag +} diff --git a/src/main.ts b/src/main.ts index 9deaf02..c8dd68f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,80 +1,160 @@ -import { promises as fs } from "fs"; -import * as os from "os"; -import * as path from "path"; - import * as core from '@actions/core' -import * as exec from '@actions/exec'; - -import * as installer from "./installer" +import * as exec from '@actions/exec' +import * as installer from './installer' +import * as io from '@actions/io' +import * as os from 'os' +import * as path from 'path' +import {promises as fs} from 'fs' async function run(): Promise { - const runnerTmpdir = process.env["RUNNER_TEMP"] || os.tmpdir(); - const tmpdir = await fs.mkdtemp(path.join(runnerTmpdir, "reviewdog-")); + const runnerTmpdir = process.env['RUNNER_TEMP'] || os.tmpdir() + const tmpdir = await fs.mkdtemp(path.join(runnerTmpdir, 'reviewdog-')) try { - const reviewdogVersion = core.getInput("reviewdog_version") || "latest"; - const toolName = core.getInput("tool_name") || "clippy"; - const level = core.getInput("level") || "error"; - const reporter = core.getInput("reporter") || "github-pr-check"; - const filterMode = core.getInput("filter_mode") || "added"; - const failOnError = core.getInput("fail_on_error") || "false"; - const reviewdogFlags = core.getInput("reviewdog_flags"); - const workdir = core.getInput("workdir") || "."; - const cwd = path.relative(process.env["GITHUB_WORKSPACE"] || process.cwd(), workdir); + const reviewdogVersion = core.getInput('reviewdog_version') || 'latest' + const toolName = core.getInput('tool_name') || 'clippy' + const level = core.getInput('level') || 'error' + const reporter = core.getInput('reporter') || 'github-pr-check' + const filterMode = core.getInput('filter_mode') || 'added' + const failOnError = core.getInput('fail_on_error') || 'false' + const reviewdogFlags = core.getInput('reviewdog_flags') + const workdir = core.getInput('workdir') || '.' + const cwd = path.relative( + process.env['GITHUB_WORKSPACE'] || process.cwd(), + workdir + ) const reviewdog = await core.group( - "🐶 Installing reviewdog ... https://github.com/reviewdog/reviewdog", + '🐶 Installing reviewdog ... https://github.com/reviewdog/reviewdog', async () => { - return await installer.installReviewdog(reviewdogVersion, tmpdir); + return await installer.installReviewdog(reviewdogVersion, tmpdir) } - ); + ) - const code = await core.group("Running Clippy with reviewdog 🐶 ...", async (): Promise => { - const output = await exec.getExecOutput( - "cargo", - ["clippy", "--color", "never", "-q", "--message-format", "short"], - { - cwd, - ignoreReturnCode: true, - } - ); + const code = await core.group( + 'Running Clippy with reviewdog 🐶 ...', + async (): Promise => { + const output: string[] = [] + await exec.exec( + 'cargo', + ['clippy', '--color', 'never', '-q', '--message-format', 'json'], + { + cwd, + ignoreReturnCode: true, + listeners: { + stdline: (line: string) => { + let content: CompilerMessage + try { + content = JSON.parse(line) + } catch (error) { + core.debug('failed to parse JSON') + return + } - process.env["REVIEWDOG_GITHUB_API_TOKEN"] = core.getInput("github_token"); - return await exec.exec( - reviewdog, - [ - "-f=clippy", - `-name=${toolName}`, - `-reporter=${reporter}`, - `-filter-mode=${filterMode}`, - `-fail-on-error=${failOnError}`, - `-level=${level}`, - ...parse(reviewdogFlags), - ], - { - cwd, - input: Buffer.from(output.stderr, "utf-8"), - ignoreReturnCode: true, - } - ); - }); + if (content.reason !== 'compiler-message') { + core.debug('ignore all but `compiler-message`') + return + } - if (code != 0) { - core.setFailed(`reviewdog exited with status code: ${code}`); + if (content.message.code === null) { + core.debug('message code is missing, ignore it') + return + } + + core.debug('this is a compiler-message!') + const span = content.message.spans[0] + const rendered = + reporter === 'github-pr-review' + ? ` \n
${content.message.rendered}
\n__END__` + : `${content.message.rendered}\n__END__` + const ret = `${span.file_name}:${span.line_start}:${span.column_start}:${rendered}` + output.push(ret) + } + } + } + ) + + core.info(`debug: ${output.join('\n')}`) + + process.env['REVIEWDOG_GITHUB_API_TOKEN'] = + core.getInput('github_token') + return await exec.exec( + reviewdog, + [ + '-efm=%E%f:%l:%c:%m', + '-efm=%Z__END__', + '-efm=%C%m', + '-efm=%C', + `-name=${toolName}`, + `-reporter=${reporter}`, + `-filter-mode=${filterMode}`, + `-fail-on-error=${failOnError}`, + `-level=${level}`, + ...parse(reviewdogFlags) + ], + { + cwd, + input: Buffer.from(output.join('\n'), 'utf-8'), + ignoreReturnCode: true + } + ) + } + ) + + if (code !== 0) { + core.setFailed(`reviewdog exited with status code: ${code}`) } } catch (error) { if (error instanceof Error) core.setFailed(error.message) + } finally { + // clean up the temporary directory + try { + await io.rmRF(tmpdir) + } catch (error) { + // suppress errors + // Garbage will remain, but it may be harmless. + if (error instanceof Error) { + core.info(`clean up failed: ${error.message}`) + } else { + core.info(`clean up failed: ${error}`) + } + } } } function parse(flags: string): string[] { - flags = flags.trim(); - if (flags === "") { - return []; + flags = flags.trim() + if (flags === '') { + return [] } // TODO: need to simulate bash? - return flags.split(/\s+/); + return flags.split(/\s+/) +} + +interface CompilerMessage { + reason: string + message: { + code: Code + level: string + message: string + rendered: string + spans: Span[] + } +} + +interface Code { + code: string + explanation?: string +} + +interface Span { + file_name: string + is_primary: boolean + line_start: number + line_end: number + column_start: number + column_end: number } run()