reimplement github action in typescript
This commit is contained in:
29
src/cache.ts
Normal file
29
src/cache.ts
Normal 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
13
src/deps.d.ts
vendored
Normal 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
27
src/install.ts
Normal 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
3
src/main.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { run } from "./run"
|
||||
|
||||
run()
|
3
src/post_main.ts
Normal file
3
src/post_main.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { postRun } from "./run"
|
||||
|
||||
postRun()
|
97
src/run.ts
Normal file
97
src/run.ts
Normal 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
123
src/version.ts
Normal 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
|
||||
}
|
Reference in New Issue
Block a user