Compare commits

...

20 Commits

Author SHA1 Message Date
10cbc929b3 Support only-new-issues (#19)
Fixes #16
2020-05-22 10:36:12 +03:00
64c208bfbc docs: add working-directory to README 2020-05-21 14:43:37 +03:00
20d5541dab Add working-directory support (#18)
Add working-directory support

Fixes #15
2020-05-21 14:36:02 +03:00
85a3a6abe4 docs: remove 'beta version' 2020-05-16 17:24:18 +03:00
b66692b61d mark the action as stable: v0 -> v1 2020-05-16 17:20:48 +03:00
348830fe4b docs: recommend distinct job 2020-05-09 18:48:24 +03:00
e4cc61e5b1 Merge pull request #13 from golangci/feature/action-config
use action config, don't require github token
2020-05-09 16:36:00 +03:00
27e14e0f3f use action config, don't require github token
fixes #11
2020-05-09 16:34:52 +03:00
6993abb7cd docs: use @v0.1.7 in README as the latest release in marketplace 2020-05-07 16:01:25 +03:00
ca150a071d docs: fix version, @v0 doesn't work 2020-05-07 15:59:32 +03:00
1ef31b642a Merge pull request #10 from golangci/feature/fix-github-token
docs: recommend using GITHUB_TOKEN
2020-05-07 15:44:41 +03:00
fc9d1728df docs: recommend using GITHUB_TOKEN
Fixes: #9
2020-05-07 15:42:54 +03:00
1ad27ad153 s/@v1/@v0 2020-05-07 11:26:37 +03:00
1778ebaefa dev: shorten description 2020-05-05 18:17:02 +03:00
1dd9e0522b dev: improve description 2020-05-05 18:11:57 +03:00
66883b5fcf dev: change icon and color on GitHub marketplace 2020-05-05 17:57:46 +03:00
8ea3043ee4 docs: improve info about performance 2020-05-05 17:51:32 +03:00
95f6eefffa docs: add perf stat into README.md 2020-05-05 17:36:44 +03:00
977a01f96c Merge pull request #8 from golangci/fix-go.mod-hash
fix go.mod hashsum
2020-05-05 17:20:28 +03:00
13e2c1f984 fix go.mod hashsum 2020-05-05 17:17:46 +03:00
13 changed files with 12296 additions and 5550 deletions

View File

@ -15,7 +15,6 @@ jobs:
npm install npm install
npm run prepare-deps npm run prepare-deps
npm run all npm run all
git diff && git diff --cached
test: # make sure the action works on a clean machine without building test: # make sure the action works on a clean machine without building
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -24,4 +23,4 @@ jobs:
with: with:
version: v1.26 version: v1.26
args: --issues-exit-code=0 ./sample/... args: --issues-exit-code=0 ./sample/...
github-token: ${{ secrets.GOLANGCI_LINT_GITHUB_TOKEN }} only-new-issues: true

View File

@ -2,15 +2,14 @@
[![Build Status](https://github.com/golangci/golangci-lint-action/workflows/build-and-test/badge.svg)](https://github.com/golangci/golangci-lint-action/actions) [![Build Status](https://github.com/golangci/golangci-lint-action/workflows/build-and-test/badge.svg)](https://github.com/golangci/golangci-lint-action/actions)
![GitHub Annotations](./static/annotations.png) It's the official GitHub action for [golangci-lint](https://github.com/golangci/golangci-lint) from it's authors.
The action runs [golangci-lint](https://github.com/golangci/golangci-lint) and reports issues from linters.
The action that runs [golangci-lint](https://github.com/golangci/golangci-lint) and reports issues from linters. ![GitHub Annotations](./static/annotations.png)
## How to use ## How to use
1. Create a [GitHub token](https://github.com/settings/tokens/new) with scope `repo.public_repo`. Add `.github/workflows/golangci-lint.yml` with the following contents:
2. Add it to a [repository secrets](https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets#creating-encrypted-secrets): repository -> `Settings` -> `Secrets`.
3. Add `.github/workflows/golangci-lint.yml` with the following contents:
```yaml ```yaml
name: golangci-lint name: golangci-lint
@ -32,14 +31,20 @@ jobs:
with: with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.26 version: v1.26
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: golangci-lint command line arguments. # Optional: golangci-lint command line arguments.
# args: ./the-only-dir-to-analyze/... # args: --issues-exit-code=0
# Required: GitHub token with scope `repo.public_repo`. Used for fetching a list of releases of golangci-lint. # Optional: show only new issues if it's a pull request. The default value is `false`.
github-token: ${{ secrets.GOLANGCI_LINT_GITHUB_TOKEN }} # only-new-issues: true
``` ```
We recommend running this action in a job separate from other jobs (`go test`, etc)
because different jobs [run in parallel](https://help.github.com/en/actions/getting-started-with-github-actions/core-concepts-for-github-actions#job).
## Comments and Annotations ## Comments and Annotations
Currently, GitHub parses the action's output and creates [annotations](https://github.community/t5/GitHub-Actions/What-are-annotations/td-p/30770). Currently, GitHub parses the action's output and creates [annotations](https://github.community/t5/GitHub-Actions/What-are-annotations/td-p/30770).
@ -57,6 +62,13 @@ The action was implemented with performance in mind:
2. We don't use Docker because image pulling is slow. 2. We don't use Docker because image pulling is slow.
3. We do as much as we can in parallel, e.g. we download cache, go and golangci-lint binary in parallel. 3. We do as much as we can in parallel, e.g. we download cache, go and golangci-lint binary in parallel.
For example, in a repository of [golangci-lint](https://github.com/golangci/golangci-lint) running this action without the cache takes 50s, but with cache takes 14s:
* in parallel:
* 13s to download Go
* 4s to restore 50 MB of cache
* 1s to find and install `golangci-lint`
* 1s to run `golangci-lint` (it takes 35s without cache)
## Internals ## Internals
We use JavaScript-based action. We don't use Docker-based action because: We use JavaScript-based action. We don't use Docker-based action because:
@ -68,8 +80,8 @@ Inside our action we perform 3 steps:
1. Setup environment running in parallel: 1. Setup environment running in parallel:
* restore [cache](https://github.com/actions/cache) of previous analyzes * restore [cache](https://github.com/actions/cache) of previous analyzes
* list [releases of golangci-lint](https://github.com/golangci/golangci-lint/releases) and find the latest patch version * fetch [action config](https://github.com/golangci/golangci-lint/blob/master/assets/github-action-config.json) and find the latest `golangci-lint` patch version
for needed version (users of this action can specify only minor version). After that install [golangci-lint](https://github.com/golangci/golangci-lint) using [@actions/tool-cache](https://github.com/actions/toolkit/tree/master/packages/tool-cache) for needed version (users of this action can specify only minor version of `golangci-lint`). After that install [golangci-lint](https://github.com/golangci/golangci-lint) using [@actions/tool-cache](https://github.com/actions/toolkit/tree/master/packages/tool-cache)
* install the latest Go 1.x version using [@actions/setup-go](https://github.com/actions/setup-go) * install the latest Go 1.x version using [@actions/setup-go](https://github.com/actions/setup-go)
2. Run `golangci-lint` with specified by user `args` 2. Run `golangci-lint` with specified by user `args`
3. Save cache for later builds 3. Save cache for later builds
@ -87,6 +99,5 @@ This scheme is basic and needs improvements. Pull requests and ideas are welcome
1. Install [act](https://github.com/nektos/act#installation) 1. Install [act](https://github.com/nektos/act#installation)
2. Make a symlink for `act` to work properly: `ln -s . golangci-lint-action` 2. Make a symlink for `act` to work properly: `ln -s . golangci-lint-action`
3. Get a [GitHub token](https://github.com/settings/tokens/new) with the scope `repo.public_repo`. Export it by `export GITHUB_TOKEN=YOUR_TOKEN`. 3. Prepare deps once: `npm run prepare-deps`
4. Prepare deps once: `npm run prepare-deps` 4. Run `npm run local` after any change to test it
5. Run `npm run local` after any change to test it

View File

@ -1,6 +1,6 @@
--- ---
name: 'Run golangci-lint' name: 'Run golangci-lint'
description: 'Run golangci-lint (WIP)' description: 'Official golangci-lint action with line-attached annotations for found issues, caching and parallel execution.'
author: 'golangci' author: 'golangci'
inputs: inputs:
version: version:
@ -10,8 +10,16 @@ inputs:
description: 'golangci-lint command line arguments' description: 'golangci-lint command line arguments'
default: '' default: ''
required: false required: false
working-directory:
description: 'golangci-lint working directory, default is project root'
required: false
github-token: github-token:
description: 'GitHub token with scope `repo.public_repo`. Used for fetching a list of releases of golangci-lint.' description: 'the token is used for fetching patch of a pull request to show only new issues'
default: ${{ github.token }}
required: true
only-new-issues:
description: 'if set to true and the action runs on a pull request - the action outputs only newly found issues'
default: false
required: true required: true
runs: runs:
@ -19,5 +27,5 @@ runs:
main: 'dist/run/index.js' main: 'dist/run/index.js'
post: 'dist/post_run/index.js' post: 'dist/post_run/index.js'
branding: branding:
icon: 'check-circle' icon: 'shield'
color: 'blue' color: 'yellow'

8740
dist/post_run/index.js vendored

File diff suppressed because it is too large Load Diff

8740
dist/run/index.js vendored

File diff suppressed because it is too large Load Diff

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/golangci/golangci-lint-action
go 1.14

46
package-lock.json generated
View File

@ -257,6 +257,11 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/tmp": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.0.tgz",
"integrity": "sha512-flgpHJjntpBAdJD43ShRosQvNC0ME97DCfGvZEDlAThQmnerRXrLbX6YgzRBQCZTthET9eAWFAMaYP0m0Y4HzQ=="
},
"@types/uuid": { "@types/uuid": {
"version": "3.4.9", "version": "3.4.9",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.9.tgz", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.9.tgz",
@ -923,6 +928,17 @@
"chardet": "^0.7.0", "chardet": "^0.7.0",
"iconv-lite": "^0.4.24", "iconv-lite": "^0.4.24",
"tmp": "^0.0.33" "tmp": "^0.0.33"
},
"dependencies": {
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dev": true,
"requires": {
"os-tmpdir": "~1.0.2"
}
}
} }
}, },
"fast-deep-equal": { "fast-deep-equal": {
@ -996,8 +1012,7 @@
"fs.realpath": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
"dev": true
}, },
"function-bind": { "function-bind": {
"version": "1.1.1", "version": "1.1.1",
@ -1029,7 +1044,6 @@
"version": "7.1.6", "version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": { "requires": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
"inflight": "^1.0.4", "inflight": "^1.0.4",
@ -1125,7 +1139,6 @@
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": { "requires": {
"once": "^1.3.0", "once": "^1.3.0",
"wrappy": "1" "wrappy": "1"
@ -1134,8 +1147,7 @@
"inherits": { "inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
"dev": true
}, },
"inquirer": { "inquirer": {
"version": "7.1.0", "version": "7.1.0",
@ -1625,8 +1637,7 @@
"path-is-absolute": { "path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
"dev": true
}, },
"path-key": { "path-key": {
"version": "2.0.1", "version": "2.0.1",
@ -2035,12 +2046,21 @@
"dev": true "dev": true
}, },
"tmp": { "tmp": {
"version": "0.0.33", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"dev": true,
"requires": { "requires": {
"os-tmpdir": "~1.0.2" "rimraf": "^3.0.0"
},
"dependencies": {
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"requires": {
"glob": "^7.1.3"
}
}
} }
}, },
"tslib": { "tslib": {

View File

@ -9,13 +9,15 @@
"prepare-cache": "cd node_modules/cache && npm run build", "prepare-cache": "cd node_modules/cache && npm run build",
"prepare-deps": "npm run prepare-setup-go && npm run prepare-cache", "prepare-deps": "npm run prepare-setup-go && npm run prepare-cache",
"build": "tsc && ncc build -o dist/run/ src/main.ts && ncc build -o dist/post_run/ src/post_main.ts", "build": "tsc && ncc build -o dist/run/ src/main.ts && ncc build -o dist/post_run/ src/post_main.ts",
"watched_build_main": "tsc && ncc build -w -o dist/run/ src/main.ts",
"watched_build_post_main": "tsc && ncc build -w -o dist/post_run/ src/post_main.ts",
"type-check": "tsc", "type-check": "tsc",
"lint": "eslint **/*.ts --cache", "lint": "eslint --max-warnings 1 **/*.ts --cache",
"lint-fix": "eslint **/*.ts --cache --fix", "lint-fix": "eslint **/*.ts --cache --fix",
"format": "prettier --write **/*.ts", "format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts", "format-check": "prettier --check **/*.ts",
"all": "npm run build && npm run format-check && npm run lint", "all": "npm run build && npm run format-check && npm run lint",
"local": "npm run build && act -j test -s GOLANGCI_LINT_GITHUB_TOKEN=$GITHUB_TOKEN" "local": "npm run build && act -j test -b"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,8 +31,10 @@
"@actions/github": "^2.1.1", "@actions/github": "^2.1.1",
"@actions/tool-cache": "^1.3.4", "@actions/tool-cache": "^1.3.4",
"@types/semver": "^7.1.0", "@types/semver": "^7.1.0",
"@types/tmp": "^0.2.0",
"cache": "git+https://github.com/golangci/cache.git", "cache": "git+https://github.com/golangci/cache.git",
"setup-go": "git+https://github.com/actions/setup-go.git" "setup-go": "git+https://github.com/actions/setup-go.git",
"tmp": "^0.2.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^12.0.4", "@types/node": "^12.0.4",

View File

@ -10,6 +10,7 @@ import (
// Hash~ // Hash~
func Hash(data string) string { func Hash(data string) string {
retError() retError()
retError2()
h := md5.New() h := md5.New()
h.Write([]byte(data)) h.Write([]byte(data))
@ -19,3 +20,7 @@ func Hash(data string) string {
func retError() error { func retError() error {
return errors.New("err") return errors.New("err")
} }
func retError2() error {
return errors.New("err2")
}

View File

@ -4,7 +4,7 @@ import save from "cache/lib/save"
import * as crypto from "crypto" import * as crypto from "crypto"
import * as fs from "fs" import * as fs from "fs"
function checksumFile(hashName: string, path: string) { function checksumFile(hashName: string, path: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const hash = crypto.createHash(hashName) const hash = crypto.createHash(hashName)
const stream = fs.createReadStream(path) const stream = fs.createReadStream(path)
@ -14,7 +14,7 @@ function checksumFile(hashName: string, path: string) {
}) })
} }
const pathExists = async (path: string) => !!(await fs.promises.stat(path).catch(e => false)) const pathExists = async (path: string): Promise<boolean> => !!(await fs.promises.stat(path).catch(() => false))
const getLintCacheDir = (): string => `${process.env.HOME}/.cache/golangci-lint` const getLintCacheDir = (): string => `${process.env.HOME}/.cache/golangci-lint`
@ -42,7 +42,7 @@ async function buildCacheKeys(): Promise<string[]> {
if (await pathExists(`go.mod`)) { if (await pathExists(`go.mod`)) {
// Add checksum to key to invalidate a cache when dependencies change. // Add checksum to key to invalidate a cache when dependencies change.
cacheKey += await checksumFile(`cache-key`, `go.mod`) cacheKey += await checksumFile(`sha1`, `go.mod`)
} else { } else {
cacheKey += `nogomod` cacheKey += `nogomod`
} }

View File

@ -3,17 +3,19 @@ import * as tc from "@actions/tool-cache"
import path from "path" import path from "path"
import { run as setupGo } from "setup-go/lib/main" import { run as setupGo } from "setup-go/lib/main"
import { stringifyVersion, Version } from "./version" import { VersionConfig } from "./version"
// The installLint returns path to installed binary of golangci-lint. // The installLint returns path to installed binary of golangci-lint.
export async function installLint(ver: Version): Promise<string> { export async function installLint(versionConfig: VersionConfig): Promise<string> {
core.info(`Installing golangci-lint ${stringifyVersion(ver)}...`) core.info(`Installing golangci-lint ${versionConfig.TargetVersion}...`)
const startedAt = Date.now() 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) core.info(`Downloading ${versionConfig.AssetURL} ...`)
const tarGzPath = await tc.downloadTool(versionConfig.AssetURL)
const extractedDir = await tc.extractTar(tarGzPath, process.env.HOME) const extractedDir = await tc.extractTar(tarGzPath, process.env.HOME)
const urlParts = versionConfig.AssetURL.split(`/`)
const dirName = urlParts[urlParts.length - 1].replace(/\.tar\.gz$/, ``)
const lintPath = path.join(extractedDir, dirName, `golangci-lint`) const lintPath = path.join(extractedDir, dirName, `golangci-lint`)
core.info(`Installed golangci-lint into ${lintPath} in ${Date.now() - startedAt}ms`) core.info(`Installed golangci-lint into ${lintPath} in ${Date.now() - startedAt}ms`)
return lintPath return lintPath

View File

@ -1,5 +1,9 @@
import * as core from "@actions/core" import * as core from "@actions/core"
import { exec } from "child_process" import * as github from "@actions/github"
import { exec, ExecOptions } from "child_process"
import * as fs from "fs"
import * as path from "path"
import { dir } from "tmp"
import { promisify } from "util" import { promisify } from "util"
import { restoreCache, saveCache } from "./cache" import { restoreCache, saveCache } from "./cache"
@ -7,26 +11,91 @@ import { installGo, installLint } from "./install"
import { findLintVersion } from "./version" import { findLintVersion } from "./version"
const execShellCommand = promisify(exec) const execShellCommand = promisify(exec)
const writeFile = promisify(fs.writeFile)
const createTempDir = promisify(dir)
async function prepareLint(): Promise<string> { async function prepareLint(): Promise<string> {
const lintVersion = await findLintVersion() const versionConfig = await findLintVersion()
return await installLint(lintVersion) return await installLint(versionConfig)
} }
async function prepareEnv(): Promise<string> { async function fetchPatch(): Promise<string> {
const onlyNewIssues = core.getInput(`only-new-issues`, { required: true }).trim()
if (onlyNewIssues !== `false` && onlyNewIssues !== `true`) {
throw new Error(`invalid value of "only-new-issues": "${onlyNewIssues}", expected "true" or "false"`)
}
if (onlyNewIssues === `false`) {
return ``
}
const ctx = github.context
if (ctx.eventName !== `pull_request`) {
core.info(`Not fetching patch for showing only new issues because it's not a pull request context: event name is ${ctx.eventName}`)
return ``
}
const pull = ctx.payload.pull_request
if (!pull) {
core.warning(`No pull request in context`)
return ``
}
const octokit = new github.GitHub(core.getInput(`github-token`, { required: true }))
let patch: string
try {
const patchResp = await octokit.pulls.get({
owner: ctx.repo.owner,
repo: ctx.repo.repo,
[`pull_number`]: pull.number,
mediaType: {
format: `diff`,
},
})
if (patchResp.status !== 200) {
core.warning(`failed to fetch pull request patch: response status is ${patchResp.status}`)
return `` // don't fail the action, but analyze without patch
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
patch = patchResp.data as any
} catch (err) {
console.warn(`failed to fetch pull request patch:`, err)
return `` // don't fail the action, but analyze without patch
}
try {
const tempDir = await createTempDir()
const patchPath = path.join(tempDir, "pull.patch")
core.info(`Writing patch to ${patchPath}`)
await writeFile(patchPath, patch)
return patchPath
} catch (err) {
console.warn(`failed to save pull request patch:`, err)
return `` // don't fail the action, but analyze without patch
}
}
type Env = {
lintPath: string
patchPath: string
}
async function prepareEnv(): Promise<Env> {
const startedAt = Date.now() const startedAt = Date.now()
// Prepare cache, lint and go in parallel. // Prepare cache, lint and go in parallel.
const restoreCachePromise = restoreCache() const restoreCachePromise = restoreCache()
const prepareLintPromise = prepareLint() const prepareLintPromise = prepareLint()
const installGoPromise = installGo() const installGoPromise = installGo()
const patchPromise = fetchPatch()
const lintPath = await prepareLintPromise const lintPath = await prepareLintPromise
await installGoPromise await installGoPromise
await restoreCachePromise await restoreCachePromise
const patchPath = await patchPromise
core.info(`Prepared env in ${Date.now() - startedAt}ms`) core.info(`Prepared env in ${Date.now() - startedAt}ms`)
return lintPath return { lintPath, patchPath }
} }
type ExecRes = { type ExecRes = {
@ -43,23 +112,58 @@ const printOutput = (res: ExecRes): void => {
} }
} }
async function runLint(lintPath: string): Promise<void> { async function runLint(lintPath: string, patchPath: string): Promise<void> {
const debug = core.getInput(`debug`) const debug = core.getInput(`debug`)
if (debug.split(`,`).includes(`cache`)) { if (debug.split(`,`).includes(`cache`)) {
const res = await execShellCommand(`${lintPath} cache status`) const res = await execShellCommand(`${lintPath} cache status`)
printOutput(res) printOutput(res)
} }
const args = core.getInput(`args`) const userArgs = core.getInput(`args`)
if (args.includes(`-out-format`)) { const addedArgs: string[] = []
const userArgNames = new Set<string>()
userArgs
.split(/\s/)
.map(arg => arg.split(`=`)[0])
.filter(arg => arg.startsWith(`-`))
.forEach(arg => {
userArgNames.add(arg.replace(`-`, ``))
})
if (userArgNames.has(`out-format`)) {
throw new Error(`please, don't change out-format for golangci-lint: it can be broken in a future`) throw new Error(`please, don't change out-format for golangci-lint: it can be broken in a future`)
} }
addedArgs.push(`--out-format=github-actions`)
const cmd = `${lintPath} run --out-format=github-actions ${args}`.trimRight() if (patchPath && (userArgNames.has(`new`) || userArgNames.has(`new-from-rev`) || userArgNames.has(`new-from-patch`))) {
core.info(`Running [${cmd}] ...`) throw new Error(`please, don't specify manually --new* args when requesting only new issues`)
}
addedArgs.push(`--new-from-patch=${patchPath}`)
// Override config values.
addedArgs.push(`--new=false`)
addedArgs.push(`--new-from-rev=`)
const workingDirectory = core.getInput(`working-directory`)
const cmdArgs: ExecOptions = {}
if (workingDirectory) {
if (patchPath) {
// TODO: make them compatible
throw new Error(`options working-directory and only-new-issues aren't compatible`)
}
if (!fs.existsSync(workingDirectory) || !fs.lstatSync(workingDirectory).isDirectory()) {
throw new Error(`working-directory (${workingDirectory}) was not a path`)
}
cmdArgs.cwd = path.resolve(workingDirectory)
}
const cmd = `${lintPath} run ${addedArgs.join(` `)} ${userArgs}`.trimRight()
core.info(`Running [${cmd}] in [${cmdArgs.cwd}] ...`)
const startedAt = Date.now() const startedAt = Date.now()
try { try {
const res = await execShellCommand(cmd) const res = await execShellCommand(cmd, cmdArgs)
printOutput(res) printOutput(res)
core.info(`golangci-lint found no issues`) core.info(`golangci-lint found no issues`)
} catch (exc) { } catch (exc) {
@ -79,8 +183,8 @@ async function runLint(lintPath: string): Promise<void> {
export async function run(): Promise<void> { export async function run(): Promise<void> {
try { try {
const lintPath = await core.group(`prepare environment`, prepareEnv) const { lintPath, patchPath } = await core.group(`prepare environment`, prepareEnv)
await core.group(`run golangci-lint`, () => runLint(lintPath)) await core.group(`run golangci-lint`, () => runLint(lintPath, patchPath))
} catch (error) { } catch (error) {
core.error(`Failed to run: ${error}, ${error.stack}`) core.error(`Failed to run: ${error}, ${error.stack}`)
core.setFailed(error.message) core.setFailed(error.message)

View File

@ -1,24 +1,5 @@
import * as core from "@actions/core" import * as core from "@actions/core"
import * as github from "@actions/github" import * as httpm from "@actions/http-client"
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 // TODO: make a class
export type Version = { export type Version = {
@ -84,40 +65,61 @@ const getRequestedLintVersion = (): Version => {
return parsedRequestedLintVersion return parsedRequestedLintVersion
} }
export async function findLintVersion(): Promise<Version> { export type VersionConfig = {
Error?: string
TargetVersion: string
AssetURL: string
}
type Config = {
MinorVersionToConfig: {
[minorVersion: string]: VersionConfig
}
}
const getConfig = async (): Promise<Config> => {
const http = new httpm.HttpClient(`golangci/golangci-lint-action`, [], {
allowRetries: true,
maxRetries: 5,
})
try {
const url = `https://raw.githubusercontent.com/golangci/golangci-lint/master/assets/github-action-config.json`
const response: httpm.HttpClientResponse = await http.get(url)
if (response.message.statusCode !== 200) {
throw new Error(`failed to download from "${url}". Code(${response.message.statusCode}) Message(${response.message.statusMessage})`)
}
const body = await response.readBody()
return JSON.parse(body)
} catch (exc) {
throw new Error(`failed to get action config: ${exc.message}`)
}
}
export async function findLintVersion(): Promise<VersionConfig> {
core.info(`Finding needed golangci-lint version...`) core.info(`Finding needed golangci-lint version...`)
const startedAt = Date.now() const startedAt = Date.now()
const reqLintVersion = getRequestedLintVersion() const reqLintVersion = getRequestedLintVersion()
const githubToken = core.getInput(`github-token`, { required: true }) const config = await getConfig()
const octokit = new github.GitHub(githubToken)
// TODO: fetch all pages, not only the first one. if (!config.MinorVersionToConfig) {
const releasesPage = await performResilientGitHubRequest(`fetch releases of golangci-lint`, function() { core.warning(JSON.stringify(config))
return octokit.repos.listReleases({ owner: `golangci`, repo: `golangci-lint`, [`per_page`]: 100 }) throw new Error(`invalid config: no MinorVersionToConfig field`)
})
// 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) { const versionConfig = config.MinorVersionToConfig[stringifyVersion(reqLintVersion)]
throw new Error( if (!versionConfig) {
`requested golangci-lint lint version ${stringifyVersion(reqLintVersion)} doesn't exist in list of golangci-lint releases` throw new Error(`requested golangci-lint version '${stringifyVersion(reqLintVersion)}' doesn't exist`)
)
} }
const neededVersion = { ...reqLintVersion, patch: latestPatchVersion } if (versionConfig.Error) {
core.info(`Calculated needed golangci-lint version ${stringifyVersion(neededVersion)} in ${Date.now() - startedAt}ms`) throw new Error(`failed to use requested golangci-lint version '${stringifyVersion(reqLintVersion)}': ${versionConfig.Error}`)
return neededVersion }
core.info(
`Requested golangci-lint '${stringifyVersion(reqLintVersion)}', using '${versionConfig.TargetVersion}', calculation took ${Date.now() -
startedAt}ms`
)
return versionConfig
} }