reimplement github action in typescript

This commit is contained in:
Denis Isaev
2020-05-04 16:01:25 +03:00
parent 666cc9164e
commit 853ade09ed
22 changed files with 67641 additions and 49 deletions

29
src/cache.ts Normal file
View File

@@ -0,0 +1,29 @@
import * as core from "@actions/core"
import restore from "cache/lib/restore"
import save from "cache/lib/save"
// TODO: ensure dir exists, have access, etc
const getCacheDir = (): string => `${process.env.HOME}/.cache/golangci-lint`
const setCacheInputs = (): void => {
process.env.INPUT_KEY = `golangci-lint.analysis-cache`
process.env.INPUT_PATH = getCacheDir()
}
export async function restoreCache(): Promise<void> {
const startedAt = Date.now()
setCacheInputs()
// Tell golangci-lint to use our cache directory.
process.env.GOLANGCI_LINT_CACHE = getCacheDir()
await restore()
core.info(`Restored golangci-lint analysis cache in ${Date.now() - startedAt}ms`)
}
export async function saveCache(): Promise<void> {
const startedAt = Date.now()
setCacheInputs()
await save()
core.info(`Saved golangci-lint analysis cache in ${Date.now() - startedAt}ms`)
}

13
src/deps.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
declare module "setup-go/lib/main" {
function run(): Promise<void>
}
declare module "cache/lib/restore" {
function run(): Promise<void>
export default run
}
declare module "cache/lib/save" {
function run(): Promise<void>
export default run
}

27
src/install.ts Normal file
View File

@@ -0,0 +1,27 @@
import * as core from "@actions/core"
import * as tc from "@actions/tool-cache"
import path from "path"
import { run as setupGo } from "setup-go/lib/main"
import { stringifyVersion, Version } from "./version"
// The installLint returns path to installed binary of golangci-lint.
export async function installLint(ver: Version): Promise<string> {
core.info(`Installing golangci-lint ${stringifyVersion(ver)}...`)
const startedAt = Date.now()
const dirName = `golangci-lint-${ver.major}.${ver.minor}.${ver.patch}-linux-amd64`
const assetUrl = `https://github.com/golangci/golangci-lint/releases/download/${stringifyVersion(ver)}/${dirName}.tar.gz`
const tarGzPath = await tc.downloadTool(assetUrl)
const extractedDir = await tc.extractTar(tarGzPath, process.env.HOME)
const lintPath = path.join(extractedDir, dirName, `golangci-lint`)
core.info(`Installed golangci-lint into ${lintPath} in ${Date.now() - startedAt}ms`)
return lintPath
}
export async function installGo(): Promise<void> {
const startedAt = Date.now()
process.env[`INPUT_GO-VERSION`] = `1`
await setupGo()
core.info(`Installed Go in ${Date.now() - startedAt}ms`)
}

3
src/main.ts Normal file
View File

@@ -0,0 +1,3 @@
import { run } from "./run"
run()

3
src/post_main.ts Normal file
View File

@@ -0,0 +1,3 @@
import { postRun } from "./run"
postRun()

97
src/run.ts Normal file
View File

@@ -0,0 +1,97 @@
import * as core from "@actions/core"
import { exec } from "child_process"
import { promisify } from "util"
import { restoreCache, saveCache } from "./cache"
import { installGo, installLint } from "./install"
import { findLintVersion } from "./version"
const execShellCommand = promisify(exec)
async function prepareLint(): Promise<string> {
const lintVersion = await findLintVersion()
return await installLint(lintVersion)
}
async function prepareEnv(): Promise<string> {
const startedAt = Date.now()
// Prepare cache, lint and go in parallel.
const restoreCachePromise = restoreCache()
const prepareLintPromise = prepareLint()
const installGoPromise = installGo()
const lintPath = await prepareLintPromise
await installGoPromise
await restoreCachePromise
core.info(`Prepared env in ${Date.now() - startedAt}ms`)
return lintPath
}
type ExecRes = {
stdout: string
stderr: string
}
const printOutput = (res: ExecRes): void => {
if (res.stdout) {
core.info(res.stdout)
}
if (res.stderr) {
core.info(res.stderr)
}
}
async function runLint(lintPath: string): Promise<void> {
const debug = core.getInput(`debug`)
if (debug.split(`,`).includes(`cache`)) {
const res = await execShellCommand(`${lintPath} cache status`)
printOutput(res)
}
const args = core.getInput(`args`)
if (args.includes(`-out-format`)) {
throw new Error(`please, don't change out-format for golangci-lint: it can be broken in a future`)
}
const cmd = `${lintPath} run ${args}`.trimRight()
core.info(`Running [${cmd}] ...`)
const startedAt = Date.now()
try {
const res = await execShellCommand(cmd)
printOutput(res)
core.info(`golangci-lint found no issues`)
} catch (exc) {
// This logging passes issues to GitHub annotations but comments can be more convenient for some users.
// TODO: support reviewdog or leaving comments by GitHub API.
printOutput(exc)
if (exc.code === 1) {
core.setFailed(`issues found`)
} else {
core.setFailed(`golangci-lint exit with code ${exc.code}`)
}
}
core.info(`Ran golangci-lint in ${Date.now() - startedAt}ms`)
}
export async function run(): Promise<void> {
try {
const lintPath = await core.group(`prepare environment`, prepareEnv)
await core.group(`run golangci-lint`, () => runLint(lintPath))
} catch (error) {
core.error(`Failed to run: ${error}, ${error.stack}`)
core.setFailed(error.message)
}
}
export async function postRun(): Promise<void> {
try {
await saveCache()
} catch (error) {
core.error(`Failed to post-run: ${error}, ${error.stack}`)
core.setFailed(error.message)
}
}

123
src/version.ts Normal file
View File

@@ -0,0 +1,123 @@
import * as core from "@actions/core"
import * as github from "@actions/github"
import { Octokit } from "@actions/github/node_modules/@octokit/rest"
async function performResilientGitHubRequest<T>(opName: string, execFn: () => Promise<Octokit.Response<T>>): Promise<T> {
let lastError = ``
for (let i = 0; i < 3; i++) {
// TODO: configurable params, timeouts, random jitters, exponential back-off, etc
try {
const res = await execFn()
if (res.status === 200) {
return res.data
}
lastError = `GitHub returned HTTP code ${res.status}`
} catch (exc) {
lastError = exc.message
}
}
throw new Error(`failed to execute github operation '${opName}': ${lastError}`)
}
// TODO: make a class
export type Version = {
major: number
minor: number
patch: number | null
}
const versionRe = /^v(\d+)\.(\d+)(?:\.(\d+))?$/
const parseVersion = (s: string): Version => {
const match = s.match(versionRe)
if (!match) {
throw new Error(`invalid version string '${s}', expected format v1.2 or v1.2.3`)
}
return {
major: parseInt(match[1]),
minor: parseInt(match[2]),
patch: match[3] === undefined ? null : parseInt(match[3]),
}
}
export const stringifyVersion = (v: Version): string => `v${v.major}.${v.minor}${v.patch !== null ? `.${v.patch}` : ``}`
const minVersion = {
major: 1,
minor: 14,
patch: 0,
}
const isLessVersion = (a: Version, b: Version): boolean => {
if (a.major != b.major) {
return a.major < b.major
}
if (a.minor != b.minor) {
return a.minor < b.minor
}
if (a.patch === null || b.patch === null) {
return true
}
return a.patch < b.patch
}
const getRequestedLintVersion = (): Version => {
const requestedLintVersion = core.getInput(`version`, { required: true })
const parsedRequestedLintVersion = parseVersion(requestedLintVersion)
if (parsedRequestedLintVersion.patch !== null) {
throw new Error(
`requested golangci-lint version '${requestedLintVersion}' was specified with the patch version, need specify only minor version`
)
}
if (isLessVersion(parsedRequestedLintVersion, minVersion)) {
throw new Error(
`requested golangci-lint version '${requestedLintVersion}' isn't supported: we support only ${stringifyVersion(
minVersion
)} and later versions`
)
}
return parsedRequestedLintVersion
}
export async function findLintVersion(): Promise<Version> {
core.info(`Finding needed golangci-lint version...`)
const startedAt = Date.now()
const reqLintVersion = getRequestedLintVersion()
const githubToken = core.getInput(`github-token`, { required: true })
const octokit = new github.GitHub(githubToken)
// TODO: fetch all pages, not only the first one.
const releasesPage = await performResilientGitHubRequest(`fetch releases of golangci-lint`, function() {
return octokit.repos.listReleases({ owner: `golangci`, repo: `golangci-lint`, [`per_page`]: 100 })
})
// TODO: use semver and semver.satisfies
let latestPatchVersion: number | null = null
for (const rel of releasesPage) {
const ver = parseVersion(rel.tag_name)
if (ver.patch === null) {
// < minVersion
continue
}
if (ver.major == reqLintVersion.major && ver.minor == reqLintVersion.minor) {
latestPatchVersion = latestPatchVersion !== null ? Math.max(latestPatchVersion, ver.patch) : ver.patch
}
}
if (latestPatchVersion === null) {
throw new Error(
`requested golangci-lint lint version ${stringifyVersion(reqLintVersion)} doesn't exist in list of golangci-lint releases`
)
}
const neededVersion = { ...reqLintVersion, patch: latestPatchVersion }
core.info(`Calculated needed golangci-lint version ${stringifyVersion(neededVersion)} in ${Date.now() - startedAt}ms`)
return neededVersion
}