diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9a16cb3069..46092cbc3b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,8 +52,8 @@ jobs: version: - "" - "latest" - - "v1.56" - - "v1.56.1" + - "v1.58" + - "v1.58.0" runs-on: ${{ matrix.os }} permissions: contents: read @@ -63,7 +63,6 @@ jobs: - uses: actions/setup-go@v5 with: go-version: oldstable - cache: false # setup-go v4 caches by default - uses: ./ with: version: ${{ matrix.version }} @@ -81,8 +80,8 @@ jobs: version: - "" - "latest" - - "v1.56.1" - - "bf5008a11acf2da5fe76716eb21d808499e079fa" + - "v1.58.0" + - "4bf574a12bb61234e28e3d6172be6ed95b0e8baf" runs-on: ${{ matrix.os }} permissions: contents: read @@ -92,7 +91,6 @@ jobs: - uses: actions/setup-go@v5 with: go-version: oldstable - cache: false # setup-go v4 caches by default - uses: ./ with: version: ${{ matrix.version }} @@ -116,7 +114,6 @@ jobs: - uses: actions/setup-go@v5 with: go-version: oldstable - cache: false # setup-go v4 caches by default - uses: ./ with: working-directory: sample-go-mod diff --git a/README.md b/README.md index 14b77caa4c..5e498a32a7 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,15 @@ [![Build Status](https://github.com/golangci/golangci-lint-action/workflows/build-and-test/badge.svg)](https://github.com/golangci/golangci-lint-action/actions) It's the official GitHub action for [golangci-lint](https://github.com/golangci/golangci-lint) from its authors. + The action runs [golangci-lint](https://github.com/golangci/golangci-lint) and reports issues from linters. ![GitHub Annotations](./static/annotations.png) ## Compatibility -* `v4.0.0+` requires an explicit setup-go installation step before using this action: `uses: actions/setup-go@v5`. +* `v5.0.0+` removes `skip-pkg-cache` and `skip-build-cache` because the cache related to Go itself is already handled by `actions/setup-go`. +* `v4.0.0+` requires an explicit `actions/setup-go` installation step before using this action: `uses: actions/setup-go@v5`. The `skip-go-installation` option has been removed. * `v2.0.0+` works with `golangci-lint` version >= `v1.28.3` * `v1.2.2` is deprecated due to we forgot to change the minimum version of `golangci-lint` to `v1.28.3` ([issue](https://github.com/golangci/golangci-lint-action/issues/39)) @@ -24,8 +26,8 @@ name: golangci-lint on: push: branches: - - master - main + - master pull_request: permissions: @@ -41,56 +43,29 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.21' - cache: false + go-version: '1.22' - name: golangci-lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v5 with: - # Require: The version of golangci-lint to use. - # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. - # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. - version: v1.54 - - # Optional: working directory, useful for monorepos - # working-directory: somedir - - # Optional: golangci-lint command line arguments. - # - # Note: By default, the `.golangci.yml` file should be at the root of the repository. - # The location of the configuration file can be changed by using `--config=` - # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 - - # Optional: show only new issues if it's a pull request. The default value is `false`. - # only-new-issues: true - - # Optional: if set to true, then all caching functionality will be completely disabled, - # takes precedence over all other caching options. - # skip-cache: true - - # Optional: if set to true, then the action won't cache or restore ~/go/pkg. - # skip-pkg-cache: true - - # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build. - # skip-build-cache: true - - # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'. - # install-mode: "goinstall" + version: latest ``` -We recommend running this action in a job separate from other jobs (`go test`, etc) +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). ### Multiple OS Support -If you need to run linters for specific operating systems, you will need to use the action `>=v2`. Here is a sample configuration file: +If you need to run linters for specific operating systems, you will need to use the action `>=v2`. + +Here is a sample configuration file: ```yaml name: golangci-lint on: push: branches: - - master - main + - master pull_request: permissions: @@ -102,8 +77,8 @@ jobs: golangci: strategy: matrix: - go: ['1.21'] - os: [macos-latest, windows-latest] + go: ['1.22'] + os: [ubuntu-latest, macos-latest, windows-latest] name: lint runs-on: ${{ matrix.os }} steps: @@ -111,47 +86,180 @@ jobs: - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - cache: false - name: golangci-lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v5 with: - # Require: The version of golangci-lint to use. - # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. - # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. - version: v1.54 + version: latest +``` - # Optional: working directory, useful for monorepos - # working-directory: somedir +You will also likely need to add the following `.gitattributes` file to ensure that line endings for Windows builds are properly formatted: + +```.gitattributes +*.go text eol=lf +``` - # Optional: golangci-lint command line arguments. - # - # Note: by default the `.golangci.yml` file should be at the root of the repository. - # The location of the configuration file can be changed by using `--config=` - # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 +## Options - # Optional: show only new issues if it's a pull request. The default value is `false`. - # only-new-issues: true +### `version` - # Optional:The mode to install golangci-lint. It can be 'binary' or 'goinstall'. - # install-mode: "goinstall" +(required) + +The version of golangci-lint to use. + +* When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. +* When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. + +```yml +uses: golangci/golangci-lint-action@v5 +with: + version: latest + # ... ``` -You will also likely need to add the following `.gitattributes` file to ensure that line endings for Windows builds are properly formatted: +### `install-mode` -```.gitattributes -*.go text eol=lf +(optional) + +The mode to install golangci-lint: it can be `binary` or `goinstall`. + +The default value is `binary`. + +```yml +uses: golangci/golangci-lint-action@v5 +with: + install-mode: "goinstall" + # ... +``` + +### `only-new-issues` + +(optional) + +Show only new issues. + +The default value is `false`. + +```yml +uses: golangci/golangci-lint-action@v5 +with: + only-new-issues: true + # ... +``` + +* `pull_request` and `pull_request_target`: the action gets the diff of the PR content from the [GitHub API](https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request) and use it with `--new-from-patch`. +* `push`: the action gets the diff of the push content (difference between commits before and after the push) from the [GitHub API](https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#compare-two-commits) and use it with `--new-from-patch`. +* `merge_group`: the action gets the diff by using `--new-from-rev` option (relies on git). + You should add the option `fetch-depth: 0` to `actions/checkout` step. + +### `working-directory` + +(optional) + +Working directory, useful for monorepos. + +```yml +uses: golangci/golangci-lint-action@v5 +with: + working-directory: somedir + # ... +``` + +### `skip-cache` + +(optional) + +If set to `true`, then all caching functionality will be completely disabled, +takes precedence over all other caching options. + +The default value is `false`. + +```yml +uses: golangci/golangci-lint-action@v5 +with: + skip-cache: true + # ... +``` + +### `skip-save-cache` + +(optional) + +If set to `true`, caches will not be saved, but they may still be restored, required `skip-cache: false`. + +The default value is `false`. + +```yml +uses: golangci/golangci-lint-action@v5 +with: + skip-save-cache: true + # ... +``` + +### `cache-invalidation-interval` + +(optional) + +Periodically invalidate the cache every `cache-invalidation-interval` days to ensure that outdated data is removed and fresh data is loaded. + +The default value is `7`. + +```yml +uses: golangci/golangci-lint-action@v5 +with: + cache-invalidation-interval: 15 + # ... +``` + +If set the number is `<= 0`, the cache will be always invalidate (Not recommended). + +### `annotations` + +(optional) + +To enable/disable GitHub Action annotations. + +If disabled (`false`), the output format(s) will follow the golangci-lint configuration file (or CLI flags from `args`) +and use the same default as golangci-lint (i.e. `colored-line-number`). + +https://golangci-lint.run/usage/configuration/#output-configuration + +The default value is `true`. + +```yml +uses: golangci/golangci-lint-action@v5 +with: + annotations: false + # ... +``` + +### `args` + +(optional) + +golangci-lint command line arguments. + +Note: By default, the `.golangci.yml` file should be at the root of the repository. +The location of the configuration file can be changed by using `--config=` + +```yml +uses: golangci/golangci-lint-action@v5 +with: + args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 + # ... ``` -## Comments and Annotations +## Annotations Currently, GitHub parses the action's output and creates [annotations](https://github.blog/2018-12-14-introducing-check-runs-and-annotations/). The restrictions of annotations are the following: -1. Currently, they don't support markdown formatting (see the [feature request](https://github.community/t5/GitHub-API-Development-and/Checks-Ability-to-include-Markdown-in-line-annotations/m-p/56704)) -2. They aren't shown in the list of comments like it was with [golangci.com](https://golangci.com). If you would like to have comments - please, up-vote [the issue](https://github.com/golangci/golangci-lint-action/issues/5). +1. Currently, they don't support Markdown formatting (see the [feature request](https://github.community/t5/GitHub-API-Development-and/Checks-Ability-to-include-Markdown-in-line-annotations/m-p/56704)) +2. They aren't shown in the list of comments. + If you would like to have comments - please, up-vote [the issue](https://github.com/golangci/golangci-lint-action/issues/5). +3. The number of annotations is [limited](https://github.com/actions/toolkit/blob/main/docs/problem-matchers.md#limitations). -To enable annotations, you need to add the `checks' permission to your action. +To enable annotations, you need to add the `checks` permission to your action. ```yaml annotate permissions: @@ -159,7 +267,7 @@ permissions: contents: read # Optional: allow read access to pull request. Use with `only-new-issues` option. pull-requests: read - # Optional: Allow write access to checks to allow the action to annotate code in the PR. + # Optional: allow write access to checks to allow the action to annotate code in the PR. checks: write ``` @@ -167,9 +275,9 @@ permissions: The action was implemented with performance in mind: -1. We cache data by [@actions/cache](https://github.com/actions/toolkit/tree/master/packages/cache) between builds: Go build cache, Go modules cache, golangci-lint analysis cache. +1. We cache data from golangci-lint analysis between builds by using [@actions/cache](https://github.com/actions/toolkit/tree/master/packages/cache). 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, 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: @@ -179,9 +287,10 @@ For example, in a repository of [golangci-lint](https://github.com/golangci/gola ## 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: -1. docker pulling is slow currently +1. Docker pulling is slow currently 2. it's easier to use caching from [@actions/cache](https://github.com/actions/toolkit/tree/master/packages/cache) We support different platforms, such as `ubuntu`, `macos`, and `windows` with `x32` and `x64` archs. @@ -189,18 +298,22 @@ We support different platforms, such as `ubuntu`, `macos`, and `windows` with `x Inside our action, we perform 3 steps: 1. Setup environment running in parallel: - * restore [cache](https://github.com/actions/cache) of previous analyses - * 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 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) + * restore [cache](https://github.com/actions/cache) of previous analyses + * 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 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) 2. Run `golangci-lint` with specified by user `args` 3. Save cache for later builds ### Caching internals -1. We save and restore the following directories: `~/.cache/golangci-lint`, `~/.cache/go-build`, `~/go/pkg`. -2. The primary caching key looks like `golangci-lint.cache-{interval_number}-{go.mod_hash}`. Interval number ensures that we periodically invalidate - our cache (every 7 days). `go.mod` hash ensures that we invalidate the cache early - as soon as dependencies have changed. -3. We use [restore keys](https://help.github.com/en/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows#matching-a-cache-key): `golangci-lint.cache-{interval_number}-`. GitHub matches keys by prefix if we have no exact match for the primary cache. +1. We save and restore the following directory: `~/.cache/golangci-lint`. +2. The primary caching key looks like `golangci-lint.cache-{runner_os}-{working_directory}-{interval_number}-{go.mod_hash}`. + Interval number ensures that we periodically invalidate our cache (every 7 days). + `go.mod` hash ensures that we invalidate the cache early - as soon as dependencies have changed. +3. We use [restore keys](https://help.github.com/en/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows#matching-a-cache-key): + `golangci-lint.cache-{runner_os}-{working_directory}-{interval_number}-`. + GitHub matches keys by prefix if we have no exact match for the primary cache. This scheme is basic and needs improvements. Pull requests and ideas are welcome. diff --git a/action.yml b/action.yml index 3b3ea21c27..bb4a50870e 100644 --- a/action.yml +++ b/action.yml @@ -1,4 +1,5 @@ -name: "Run golangci-lint" +# https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions +name: "Golangci-lint" description: "Official golangci-lint action with line-attached annotations for found issues, caching and parallel execution." author: "golangci" inputs: @@ -8,9 +9,9 @@ inputs: When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. required: false - args: - description: "golangci-lint command line arguments" - default: "" + install-mode: + description: "The mode to install golangci-lint. It can be 'binary' or 'goinstall'." + default: "binary" required: false working-directory: description: "golangci-lint working directory, default is project root" @@ -29,17 +30,23 @@ inputs: takes precedence over all other caching options. default: 'false' required: false - skip-pkg-cache: - description: "if set to true then the action doesn't cache or restore ~/go/pkg." + skip-save-cache: + description: | + if set to true then the action will not save any caches, but it may still + restore existing caches, subject to other options. default: 'false' required: false - skip-build-cache: - description: "if set to true then the action doesn't cache or restore ~/.cache/go-build." - default: 'false' + annotations: + description: "To Enable/disable GitHub Action annotations" + default: 'true' required: false - install-mode: - description: "The mode to install golangci-lint. It can be 'binary' or 'goinstall'." - default: "binary" + args: + description: "golangci-lint command line arguments" + default: "" + required: false + cache-invalidation-interval: + description: "Periodically invalidate a cache because a new code being added. (number of days)" + default: '7' required: false runs: using: "node20" @@ -47,4 +54,4 @@ runs: post: "dist/post_run/index.js" branding: icon: "shield" - color: "yellow" + color: "white" diff --git a/dist/post_run/index.js b/dist/post_run/index.js index e51711ce03..87f67815a6 100644 --- a/dist/post_run/index.js +++ b/dist/post_run/index.js @@ -88813,39 +88813,28 @@ const pathExists = async (path) => !!(await fs.promises.stat(path).catch(() => f const getLintCacheDir = () => { return path_1.default.resolve(`${process.env.HOME}/.cache/golangci-lint`); }; -const getCacheDirs = () => { - // Not existing dirs are ok here: it works. - const skipPkgCache = core.getInput(`skip-pkg-cache`, { required: true }).trim(); - const skipBuildCache = core.getInput(`skip-build-cache`, { required: true }).trim(); - const dirs = [getLintCacheDir()]; - if (skipBuildCache.toLowerCase() == "true") { - core.info(`Omitting ~/.cache/go-build from cache directories`); - } - else { - dirs.push(path_1.default.resolve(`${process.env.HOME}/.cache/go-build`)); - } - if (skipPkgCache.toLowerCase() == "true") { - core.info(`Omitting ~/go/pkg from cache directories`); - } - else { - dirs.push(path_1.default.resolve(`${process.env.HOME}/go/pkg`)); - } - return dirs; -}; const getIntervalKey = (invalidationIntervalDays) => { const now = new Date(); + if (invalidationIntervalDays <= 0) { + return `${now.getTime()}`; + } const secondsSinceEpoch = now.getTime() / 1000; const intervalNumber = Math.floor(secondsSinceEpoch / (invalidationIntervalDays * 86400)); return intervalNumber.toString(); }; async function buildCacheKeys() { const keys = []; - // Periodically invalidate a cache because a new code being added. - // TODO: configure it via inputs. - let cacheKey = `golangci-lint.cache-${getIntervalKey(7)}-`; - keys.push(cacheKey); + // Cache by OS. + let cacheKey = `golangci-lint.cache-${process.env?.RUNNER_OS}-`; // Get working directory from input const workingDirectory = core.getInput(`working-directory`); + if (workingDirectory) { + cacheKey += `${workingDirectory}-`; + } + // Periodically invalidate a cache because a new code being added. + const invalidationIntervalDays = parseInt(core.getInput(`cache-invalidation-interval`, { required: true }).trim()); + cacheKey += `${getIntervalKey(invalidationIntervalDays)}-`; + keys.push(cacheKey); // create path to go.mod prepending the workingDirectory if it exists const goModPath = path_1.default.join(workingDirectory, `go.mod`); core.info(`Checking for go.mod: ${goModPath}`); @@ -88860,7 +88849,7 @@ async function buildCacheKeys() { return keys; } async function restoreCache() { - if (core.getInput(`skip-cache`, { required: true }).trim() == "true") + if (core.getBooleanInput(`skip-cache`, { required: true })) return; if (!utils.isValidEvent()) { utils.logWarning(`Event Validation Error: The event type ${process.env[constants_1.Events.Key]} is not supported because it's not tied to a branch or tag ref.`); @@ -88878,7 +88867,7 @@ async function restoreCache() { } core.saveState(constants_1.State.CachePrimaryKey, primaryKey); try { - const cacheKey = await cache.restoreCache(getCacheDirs(), primaryKey, restoreKeys); + const cacheKey = await cache.restoreCache([getLintCacheDir()], primaryKey, restoreKeys); if (!cacheKey) { core.info(`Cache not found for input keys: ${[primaryKey, ...restoreKeys].join(", ")}`); return; @@ -88898,7 +88887,9 @@ async function restoreCache() { } exports.restoreCache = restoreCache; async function saveCache() { - if (core.getInput(`skip-cache`, { required: true }).trim() == "true") + if (core.getBooleanInput(`skip-cache`, { required: true })) + return; + if (core.getBooleanInput(`skip-save-cache`, { required: true })) return; // Validate inputs, this can cause task failure if (!utils.isValidEvent()) { @@ -88906,7 +88897,7 @@ async function saveCache() { return; } const startedAt = Date.now(); - const cacheDirs = getCacheDirs(); + const cacheDirs = [getLintCacheDir()]; const primaryKey = core.getState(constants_1.State.CachePrimaryKey); if (!primaryKey) { utils.logWarning(`Error retrieving key from state.`); @@ -89163,26 +89154,36 @@ const version_1 = __nccwpck_require__(1946); const execShellCommand = (0, util_1.promisify)(child_process_1.exec); const writeFile = (0, util_1.promisify)(fs.writeFile); const createTempDir = (0, util_1.promisify)(tmp_1.dir); +function isOnlyNewIssues() { + return core.getBooleanInput(`only-new-issues`, { required: true }); +} async function prepareLint() { const mode = core.getInput("install-mode").toLowerCase(); const versionConfig = await (0, version_1.findLintVersion)(mode); return await (0, install_1.installLint)(versionConfig, mode); } async function fetchPatch() { - 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`) { + if (!isOnlyNewIssues()) { 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 ``; + switch (ctx.eventName) { + case `pull_request`: + case `pull_request_target`: + return await fetchPullRequestPatch(ctx); + case `push`: + return await fetchPushPatch(ctx); + case `merge_group`: + core.info(JSON.stringify(ctx.payload)); + return ``; + default: + 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) { +} +async function fetchPullRequestPatch(ctx) { + const pr = ctx.payload.pull_request; + if (!pr) { core.warning(`No pull request in context`); return ``; } @@ -89192,7 +89193,7 @@ async function fetchPatch() { const patchResp = await octokit.rest.pulls.get({ owner: ctx.repo.owner, repo: ctx.repo.repo, - [`pull_number`]: pull.number, + [`pull_number`]: pr.number, mediaType: { format: `diff`, }, @@ -89220,14 +89221,47 @@ async function fetchPatch() { return ``; // don't fail the action, but analyze without patch } } +async function fetchPushPatch(ctx) { + const octokit = github.getOctokit(core.getInput(`github-token`, { required: true })); + let patch; + try { + const patchResp = await octokit.rest.repos.compareCommitsWithBasehead({ + owner: ctx.repo.owner, + repo: ctx.repo.repo, + basehead: `${ctx.payload.before}..${ctx.payload.after}`, + mediaType: { + format: `diff`, + }, + }); + if (patchResp.status !== 200) { + core.warning(`failed to fetch push 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; + } + catch (err) { + console.warn(`failed to fetch push patch:`, err); + return ``; // don't fail the action, but analyze without patch + } + try { + const tempDir = await createTempDir(); + const patchPath = path.join(tempDir, "push.patch"); + core.info(`Writing patch to ${patchPath}`); + await writeFile(patchPath, (0, diffUtils_1.alterDiffPatch)(patch)); + return patchPath; + } + catch (err) { + console.warn(`failed to save pull request patch:`, err); + return ``; // don't fail the action, but analyze without patch + } +} async function prepareEnv() { const startedAt = Date.now(); // Prepare cache, lint and go in parallel. await (0, cache_1.restoreCache)(); - const prepareLintPromise = prepareLint(); - const patchPromise = fetchPatch(); - const lintPath = await prepareLintPromise; - const patchPath = await patchPromise; + const lintPath = await prepareLint(); + const patchPath = await fetchPatch(); core.info(`Prepared env in ${Date.now() - startedAt}ms`); return { lintPath, patchPath }; } @@ -89256,26 +89290,47 @@ async function runLint(lintPath, patchPath) { .map(([key, value]) => [key.toLowerCase(), value ?? ""]); const userArgsMap = new Map(userArgsList); const userArgNames = new Set(userArgsList.map(([key]) => key)); - const formats = (userArgsMap.get("out-format") || "") - .trim() - .split(",") - .filter((f) => f.length > 0) - .filter((f) => !f.startsWith(`github-actions`)) - .concat("github-actions") - .join(","); - addedArgs.push(`--out-format=${formats}`); - userArgs = userArgs.replace(/--out-format=\S*/gi, "").trim(); - if (patchPath) { + const annotations = core.getBooleanInput(`annotations`); + if (annotations) { + const formats = (userArgsMap.get("out-format") || "") + .trim() + .split(",") + .filter((f) => f.length > 0) + .filter((f) => !f.startsWith(`github-actions`)) + .concat("github-actions") + .join(","); + addedArgs.push(`--out-format=${formats}`); + userArgs = userArgs.replace(/--out-format=\S*/gi, "").trim(); + } + if (isOnlyNewIssues()) { if (userArgNames.has(`new`) || userArgNames.has(`new-from-rev`) || userArgNames.has(`new-from-patch`)) { 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 ctx = github.context; + core.info(`only new issues on ${ctx.eventName}: ${patchPath}`); + switch (ctx.eventName) { + case `pull_request`: + case `pull_request_target`: + case `push`: + if (patchPath) { + addedArgs.push(`--new-from-patch=${patchPath}`); + // Override config values. + addedArgs.push(`--new=false`); + addedArgs.push(`--new-from-rev=`); + } + break; + case `merge_group`: + addedArgs.push(`--new-from-rev=${ctx.payload.merge_group.base_sha}`); + // Override config values. + addedArgs.push(`--new=false`); + addedArgs.push(`--new-from-patch=`); + break; + default: + break; + } } - const workingDirectory = core.getInput(`working-directory`); const cmdArgs = {}; + const workingDirectory = core.getInput(`working-directory`); if (workingDirectory) { if (!fs.existsSync(workingDirectory) || !fs.lstatSync(workingDirectory).isDirectory()) { throw new Error(`working-directory (${workingDirectory}) was not a path`); @@ -89286,7 +89341,7 @@ async function runLint(lintPath, patchPath) { cmdArgs.cwd = path.resolve(workingDirectory); } const cmd = `${lintPath} run ${addedArgs.join(` `)} ${userArgs}`.trimEnd(); - core.info(`Running [${cmd}] in [${cmdArgs.cwd || ``}] ...`); + core.info(`Running [${cmd}] in [${cmdArgs.cwd || process.cwd()}] ...`); const startedAt = Date.now(); try { const res = await execShellCommand(cmd, cmdArgs); diff --git a/dist/run/index.js b/dist/run/index.js index 7cc8c9a706..11ef531598 100644 --- a/dist/run/index.js +++ b/dist/run/index.js @@ -88813,39 +88813,28 @@ const pathExists = async (path) => !!(await fs.promises.stat(path).catch(() => f const getLintCacheDir = () => { return path_1.default.resolve(`${process.env.HOME}/.cache/golangci-lint`); }; -const getCacheDirs = () => { - // Not existing dirs are ok here: it works. - const skipPkgCache = core.getInput(`skip-pkg-cache`, { required: true }).trim(); - const skipBuildCache = core.getInput(`skip-build-cache`, { required: true }).trim(); - const dirs = [getLintCacheDir()]; - if (skipBuildCache.toLowerCase() == "true") { - core.info(`Omitting ~/.cache/go-build from cache directories`); - } - else { - dirs.push(path_1.default.resolve(`${process.env.HOME}/.cache/go-build`)); - } - if (skipPkgCache.toLowerCase() == "true") { - core.info(`Omitting ~/go/pkg from cache directories`); - } - else { - dirs.push(path_1.default.resolve(`${process.env.HOME}/go/pkg`)); - } - return dirs; -}; const getIntervalKey = (invalidationIntervalDays) => { const now = new Date(); + if (invalidationIntervalDays <= 0) { + return `${now.getTime()}`; + } const secondsSinceEpoch = now.getTime() / 1000; const intervalNumber = Math.floor(secondsSinceEpoch / (invalidationIntervalDays * 86400)); return intervalNumber.toString(); }; async function buildCacheKeys() { const keys = []; - // Periodically invalidate a cache because a new code being added. - // TODO: configure it via inputs. - let cacheKey = `golangci-lint.cache-${getIntervalKey(7)}-`; - keys.push(cacheKey); + // Cache by OS. + let cacheKey = `golangci-lint.cache-${process.env?.RUNNER_OS}-`; // Get working directory from input const workingDirectory = core.getInput(`working-directory`); + if (workingDirectory) { + cacheKey += `${workingDirectory}-`; + } + // Periodically invalidate a cache because a new code being added. + const invalidationIntervalDays = parseInt(core.getInput(`cache-invalidation-interval`, { required: true }).trim()); + cacheKey += `${getIntervalKey(invalidationIntervalDays)}-`; + keys.push(cacheKey); // create path to go.mod prepending the workingDirectory if it exists const goModPath = path_1.default.join(workingDirectory, `go.mod`); core.info(`Checking for go.mod: ${goModPath}`); @@ -88860,7 +88849,7 @@ async function buildCacheKeys() { return keys; } async function restoreCache() { - if (core.getInput(`skip-cache`, { required: true }).trim() == "true") + if (core.getBooleanInput(`skip-cache`, { required: true })) return; if (!utils.isValidEvent()) { utils.logWarning(`Event Validation Error: The event type ${process.env[constants_1.Events.Key]} is not supported because it's not tied to a branch or tag ref.`); @@ -88878,7 +88867,7 @@ async function restoreCache() { } core.saveState(constants_1.State.CachePrimaryKey, primaryKey); try { - const cacheKey = await cache.restoreCache(getCacheDirs(), primaryKey, restoreKeys); + const cacheKey = await cache.restoreCache([getLintCacheDir()], primaryKey, restoreKeys); if (!cacheKey) { core.info(`Cache not found for input keys: ${[primaryKey, ...restoreKeys].join(", ")}`); return; @@ -88898,7 +88887,9 @@ async function restoreCache() { } exports.restoreCache = restoreCache; async function saveCache() { - if (core.getInput(`skip-cache`, { required: true }).trim() == "true") + if (core.getBooleanInput(`skip-cache`, { required: true })) + return; + if (core.getBooleanInput(`skip-save-cache`, { required: true })) return; // Validate inputs, this can cause task failure if (!utils.isValidEvent()) { @@ -88906,7 +88897,7 @@ async function saveCache() { return; } const startedAt = Date.now(); - const cacheDirs = getCacheDirs(); + const cacheDirs = [getLintCacheDir()]; const primaryKey = core.getState(constants_1.State.CachePrimaryKey); if (!primaryKey) { utils.logWarning(`Error retrieving key from state.`); @@ -89163,26 +89154,36 @@ const version_1 = __nccwpck_require__(1946); const execShellCommand = (0, util_1.promisify)(child_process_1.exec); const writeFile = (0, util_1.promisify)(fs.writeFile); const createTempDir = (0, util_1.promisify)(tmp_1.dir); +function isOnlyNewIssues() { + return core.getBooleanInput(`only-new-issues`, { required: true }); +} async function prepareLint() { const mode = core.getInput("install-mode").toLowerCase(); const versionConfig = await (0, version_1.findLintVersion)(mode); return await (0, install_1.installLint)(versionConfig, mode); } async function fetchPatch() { - 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`) { + if (!isOnlyNewIssues()) { 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 ``; + switch (ctx.eventName) { + case `pull_request`: + case `pull_request_target`: + return await fetchPullRequestPatch(ctx); + case `push`: + return await fetchPushPatch(ctx); + case `merge_group`: + core.info(JSON.stringify(ctx.payload)); + return ``; + default: + 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) { +} +async function fetchPullRequestPatch(ctx) { + const pr = ctx.payload.pull_request; + if (!pr) { core.warning(`No pull request in context`); return ``; } @@ -89192,7 +89193,7 @@ async function fetchPatch() { const patchResp = await octokit.rest.pulls.get({ owner: ctx.repo.owner, repo: ctx.repo.repo, - [`pull_number`]: pull.number, + [`pull_number`]: pr.number, mediaType: { format: `diff`, }, @@ -89220,14 +89221,47 @@ async function fetchPatch() { return ``; // don't fail the action, but analyze without patch } } +async function fetchPushPatch(ctx) { + const octokit = github.getOctokit(core.getInput(`github-token`, { required: true })); + let patch; + try { + const patchResp = await octokit.rest.repos.compareCommitsWithBasehead({ + owner: ctx.repo.owner, + repo: ctx.repo.repo, + basehead: `${ctx.payload.before}..${ctx.payload.after}`, + mediaType: { + format: `diff`, + }, + }); + if (patchResp.status !== 200) { + core.warning(`failed to fetch push 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; + } + catch (err) { + console.warn(`failed to fetch push patch:`, err); + return ``; // don't fail the action, but analyze without patch + } + try { + const tempDir = await createTempDir(); + const patchPath = path.join(tempDir, "push.patch"); + core.info(`Writing patch to ${patchPath}`); + await writeFile(patchPath, (0, diffUtils_1.alterDiffPatch)(patch)); + return patchPath; + } + catch (err) { + console.warn(`failed to save pull request patch:`, err); + return ``; // don't fail the action, but analyze without patch + } +} async function prepareEnv() { const startedAt = Date.now(); // Prepare cache, lint and go in parallel. await (0, cache_1.restoreCache)(); - const prepareLintPromise = prepareLint(); - const patchPromise = fetchPatch(); - const lintPath = await prepareLintPromise; - const patchPath = await patchPromise; + const lintPath = await prepareLint(); + const patchPath = await fetchPatch(); core.info(`Prepared env in ${Date.now() - startedAt}ms`); return { lintPath, patchPath }; } @@ -89256,26 +89290,47 @@ async function runLint(lintPath, patchPath) { .map(([key, value]) => [key.toLowerCase(), value ?? ""]); const userArgsMap = new Map(userArgsList); const userArgNames = new Set(userArgsList.map(([key]) => key)); - const formats = (userArgsMap.get("out-format") || "") - .trim() - .split(",") - .filter((f) => f.length > 0) - .filter((f) => !f.startsWith(`github-actions`)) - .concat("github-actions") - .join(","); - addedArgs.push(`--out-format=${formats}`); - userArgs = userArgs.replace(/--out-format=\S*/gi, "").trim(); - if (patchPath) { + const annotations = core.getBooleanInput(`annotations`); + if (annotations) { + const formats = (userArgsMap.get("out-format") || "") + .trim() + .split(",") + .filter((f) => f.length > 0) + .filter((f) => !f.startsWith(`github-actions`)) + .concat("github-actions") + .join(","); + addedArgs.push(`--out-format=${formats}`); + userArgs = userArgs.replace(/--out-format=\S*/gi, "").trim(); + } + if (isOnlyNewIssues()) { if (userArgNames.has(`new`) || userArgNames.has(`new-from-rev`) || userArgNames.has(`new-from-patch`)) { 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 ctx = github.context; + core.info(`only new issues on ${ctx.eventName}: ${patchPath}`); + switch (ctx.eventName) { + case `pull_request`: + case `pull_request_target`: + case `push`: + if (patchPath) { + addedArgs.push(`--new-from-patch=${patchPath}`); + // Override config values. + addedArgs.push(`--new=false`); + addedArgs.push(`--new-from-rev=`); + } + break; + case `merge_group`: + addedArgs.push(`--new-from-rev=${ctx.payload.merge_group.base_sha}`); + // Override config values. + addedArgs.push(`--new=false`); + addedArgs.push(`--new-from-patch=`); + break; + default: + break; + } } - const workingDirectory = core.getInput(`working-directory`); const cmdArgs = {}; + const workingDirectory = core.getInput(`working-directory`); if (workingDirectory) { if (!fs.existsSync(workingDirectory) || !fs.lstatSync(workingDirectory).isDirectory()) { throw new Error(`working-directory (${workingDirectory}) was not a path`); @@ -89286,7 +89341,7 @@ async function runLint(lintPath, patchPath) { cmdArgs.cwd = path.resolve(workingDirectory); } const cmd = `${lintPath} run ${addedArgs.join(` `)} ${userArgs}`.trimEnd(); - core.info(`Running [${cmd}] in [${cmdArgs.cwd || ``}] ...`); + core.info(`Running [${cmd}] in [${cmdArgs.cwd || process.cwd()}] ...`); const startedAt = Date.now(); try { const res = await execShellCommand(cmd, cmdArgs); diff --git a/package-lock.json b/package-lock.json index 41e4bc4099..315e57cd3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,8 +21,8 @@ "tmp": "^0.2.3" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^7.7.0", - "@typescript-eslint/parser": "^7.7.0", + "@typescript-eslint/eslint-plugin": "^7.7.1", + "@typescript-eslint/parser": "^7.7.1", "@vercel/ncc": "^0.38.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", @@ -691,16 +691,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.0.tgz", - "integrity": "sha512-GJWR0YnfrKnsRoluVO3PRb9r5aMZriiMMM/RHj5nnTrBy1/wIgk76XCtCKcnXGjpZQJQRFtGV9/0JJ6n30uwpQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz", + "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.7.0", - "@typescript-eslint/type-utils": "7.7.0", - "@typescript-eslint/utils": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/type-utils": "7.7.1", + "@typescript-eslint/utils": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.3.1", @@ -741,15 +741,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.0.tgz", - "integrity": "sha512-fNcDm3wSwVM8QYL4HKVBggdIPAy9Q41vcvC/GtDobw3c4ndVT3K6cqudUmjHPw8EAp4ufax0o58/xvWaP2FmTg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz", + "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.7.0", - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/typescript-estree": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4" }, "engines": { @@ -769,13 +769,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.0.tgz", - "integrity": "sha512-/8INDn0YLInbe9Wt7dK4cXLDYp0fNHP5xKLHvZl3mOT5X17rK/YShXaiNmorl+/U4VKCVIjJnx4Ri5b0y+HClw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz", + "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0" + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -786,13 +786,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.0.tgz", - "integrity": "sha512-bOp3ejoRYrhAlnT/bozNQi3nio9tIgv3U5C0mVDdZC7cpcQEDZXvq8inrHYghLVwuNABRqrMW5tzAv88Vy77Sg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz", + "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.7.0", - "@typescript-eslint/utils": "7.7.0", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/utils": "7.7.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -813,9 +813,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.0.tgz", - "integrity": "sha512-G01YPZ1Bd2hn+KPpIbrAhEWOn5lQBrjxkzHkWvP6NucMXFtfXoevK82hzQdpfuQYuhkvFDeQYbzXCjR1z9Z03w==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz", + "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -826,13 +826,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.0.tgz", - "integrity": "sha512-8p71HQPE6CbxIBy2kWHqM1KGrC07pk6RJn40n0DSc6bMOBBREZxSDJ+BmRzc8B5OdaMh1ty3mkuWRg4sCFiDQQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz", + "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -893,17 +893,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.0.tgz", - "integrity": "sha512-LKGAXMPQs8U/zMRFXDZOzmMKgFv3COlxUQ+2NMPhbqgVm6R1w+nU1i4836Pmxu9jZAuIeyySNrN/6Rc657ggig==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz", + "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.15", "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.7.0", - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/typescript-estree": "7.7.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", "semver": "^7.6.0" }, "engines": { @@ -933,12 +933,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.0.tgz", - "integrity": "sha512-h0WHOj8MhdhY8YWkzIF30R379y0NqyOHExI9N9KCzvmu05EgG4FumeYa3ccfKUSphyWkWQE1ybVrgz/Pbam6YA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz", + "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.0", + "@typescript-eslint/types": "7.7.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -4610,16 +4610,16 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.0.tgz", - "integrity": "sha512-GJWR0YnfrKnsRoluVO3PRb9r5aMZriiMMM/RHj5nnTrBy1/wIgk76XCtCKcnXGjpZQJQRFtGV9/0JJ6n30uwpQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz", + "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.7.0", - "@typescript-eslint/type-utils": "7.7.0", - "@typescript-eslint/utils": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/type-utils": "7.7.1", + "@typescript-eslint/utils": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.3.1", @@ -4640,54 +4640,54 @@ } }, "@typescript-eslint/parser": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.0.tgz", - "integrity": "sha512-fNcDm3wSwVM8QYL4HKVBggdIPAy9Q41vcvC/GtDobw3c4ndVT3K6cqudUmjHPw8EAp4ufax0o58/xvWaP2FmTg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz", + "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "7.7.0", - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/typescript-estree": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.0.tgz", - "integrity": "sha512-/8INDn0YLInbe9Wt7dK4cXLDYp0fNHP5xKLHvZl3mOT5X17rK/YShXaiNmorl+/U4VKCVIjJnx4Ri5b0y+HClw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz", + "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0" + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1" } }, "@typescript-eslint/type-utils": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.0.tgz", - "integrity": "sha512-bOp3ejoRYrhAlnT/bozNQi3nio9tIgv3U5C0mVDdZC7cpcQEDZXvq8inrHYghLVwuNABRqrMW5tzAv88Vy77Sg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz", + "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "7.7.0", - "@typescript-eslint/utils": "7.7.0", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/utils": "7.7.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/types": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.0.tgz", - "integrity": "sha512-G01YPZ1Bd2hn+KPpIbrAhEWOn5lQBrjxkzHkWvP6NucMXFtfXoevK82hzQdpfuQYuhkvFDeQYbzXCjR1z9Z03w==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz", + "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.0.tgz", - "integrity": "sha512-8p71HQPE6CbxIBy2kWHqM1KGrC07pk6RJn40n0DSc6bMOBBREZxSDJ+BmRzc8B5OdaMh1ty3mkuWRg4sCFiDQQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz", + "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==", "dev": true, "requires": { - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -4726,17 +4726,17 @@ } }, "@typescript-eslint/utils": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.0.tgz", - "integrity": "sha512-LKGAXMPQs8U/zMRFXDZOzmMKgFv3COlxUQ+2NMPhbqgVm6R1w+nU1i4836Pmxu9jZAuIeyySNrN/6Rc657ggig==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz", + "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.15", "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.7.0", - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/typescript-estree": "7.7.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", "semver": "^7.6.0" }, "dependencies": { @@ -4752,12 +4752,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.0.tgz", - "integrity": "sha512-h0WHOj8MhdhY8YWkzIF30R379y0NqyOHExI9N9KCzvmu05EgG4FumeYa3ccfKUSphyWkWQE1ybVrgz/Pbam6YA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz", + "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==", "dev": true, "requires": { - "@typescript-eslint/types": "7.7.0", + "@typescript-eslint/types": "7.7.1", "eslint-visitor-keys": "^3.4.3" } }, diff --git a/package.json b/package.json index 2f5d88798f..50fd789d8d 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,8 @@ "tmp": "^0.2.3" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^7.7.0", - "@typescript-eslint/parser": "^7.7.0", + "@typescript-eslint/eslint-plugin": "^7.7.1", + "@typescript-eslint/parser": "^7.7.1", "@vercel/ncc": "^0.38.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", diff --git a/src/cache.ts b/src/cache.ts index cb8f828ac9..dc6aa06b14 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -23,28 +23,13 @@ const getLintCacheDir = (): string => { return path.resolve(`${process.env.HOME}/.cache/golangci-lint`) } -const getCacheDirs = (): string[] => { - // Not existing dirs are ok here: it works. - const skipPkgCache = core.getInput(`skip-pkg-cache`, { required: true }).trim() - const skipBuildCache = core.getInput(`skip-build-cache`, { required: true }).trim() - const dirs = [getLintCacheDir()] - - if (skipBuildCache.toLowerCase() == "true") { - core.info(`Omitting ~/.cache/go-build from cache directories`) - } else { - dirs.push(path.resolve(`${process.env.HOME}/.cache/go-build`)) - } - if (skipPkgCache.toLowerCase() == "true") { - core.info(`Omitting ~/go/pkg from cache directories`) - } else { - dirs.push(path.resolve(`${process.env.HOME}/go/pkg`)) - } - - return dirs -} - const getIntervalKey = (invalidationIntervalDays: number): string => { const now = new Date() + + if (invalidationIntervalDays <= 0) { + return `${now.getTime()}` + } + const secondsSinceEpoch = now.getTime() / 1000 const intervalNumber = Math.floor(secondsSinceEpoch / (invalidationIntervalDays * 86400)) return intervalNumber.toString() @@ -52,28 +37,42 @@ const getIntervalKey = (invalidationIntervalDays: number): string => { async function buildCacheKeys(): Promise { const keys = [] - // Periodically invalidate a cache because a new code being added. - // TODO: configure it via inputs. - let cacheKey = `golangci-lint.cache-${getIntervalKey(7)}-` - keys.push(cacheKey) + + // Cache by OS. + let cacheKey = `golangci-lint.cache-${process.env?.RUNNER_OS}-` + // Get working directory from input const workingDirectory = core.getInput(`working-directory`) + + if (workingDirectory) { + cacheKey += `${workingDirectory}-` + } + + // Periodically invalidate a cache because a new code being added. + const invalidationIntervalDays = parseInt(core.getInput(`cache-invalidation-interval`, { required: true }).trim()) + cacheKey += `${getIntervalKey(invalidationIntervalDays)}-` + + keys.push(cacheKey) + // create path to go.mod prepending the workingDirectory if it exists const goModPath = path.join(workingDirectory, `go.mod`) + core.info(`Checking for go.mod: ${goModPath}`) + if (await pathExists(goModPath)) { // Add checksum to key to invalidate a cache when dependencies change. cacheKey += await checksumFile(`sha1`, goModPath) } else { cacheKey += `nogomod` } + keys.push(cacheKey) return keys } export async function restoreCache(): Promise { - if (core.getInput(`skip-cache`, { required: true }).trim() == "true") return + if (core.getBooleanInput(`skip-cache`, { required: true })) return if (!utils.isValidEvent()) { utils.logWarning( @@ -97,7 +96,7 @@ export async function restoreCache(): Promise { } core.saveState(State.CachePrimaryKey, primaryKey) try { - const cacheKey = await cache.restoreCache(getCacheDirs(), primaryKey, restoreKeys) + const cacheKey = await cache.restoreCache([getLintCacheDir()], primaryKey, restoreKeys) if (!cacheKey) { core.info(`Cache not found for input keys: ${[primaryKey, ...restoreKeys].join(", ")}`) return @@ -115,7 +114,8 @@ export async function restoreCache(): Promise { } export async function saveCache(): Promise { - if (core.getInput(`skip-cache`, { required: true }).trim() == "true") return + if (core.getBooleanInput(`skip-cache`, { required: true })) return + if (core.getBooleanInput(`skip-save-cache`, { required: true })) return // Validate inputs, this can cause task failure if (!utils.isValidEvent()) { @@ -127,7 +127,7 @@ export async function saveCache(): Promise { const startedAt = Date.now() - const cacheDirs = getCacheDirs() + const cacheDirs = [getLintCacheDir()] const primaryKey = core.getState(State.CachePrimaryKey) if (!primaryKey) { utils.logWarning(`Error retrieving key from state.`) diff --git a/src/run.ts b/src/run.ts index 64e4164618..9cffa9c690 100644 --- a/src/run.ts +++ b/src/run.ts @@ -1,5 +1,6 @@ import * as core from "@actions/core" import * as github from "@actions/github" +import { Context } from "@actions/github/lib/context" import { exec, ExecOptions } from "child_process" import * as fs from "fs" import * as path from "path" @@ -15,6 +16,10 @@ const execShellCommand = promisify(exec) const writeFile = promisify(fs.writeFile) const createTempDir = promisify(dir) +function isOnlyNewIssues(): boolean { + return core.getBooleanInput(`only-new-issues`, { required: true }) +} + async function prepareLint(): Promise { const mode = core.getInput("install-mode").toLowerCase() const versionConfig = await findLintVersion(mode) @@ -23,31 +28,42 @@ async function prepareLint(): Promise { } async function fetchPatch(): Promise { - 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`) { + if (!isOnlyNewIssues()) { 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 `` + + switch (ctx.eventName) { + case `pull_request`: + case `pull_request_target`: + return await fetchPullRequestPatch(ctx) + case `push`: + return await fetchPushPatch(ctx) + case `merge_group`: + core.info(JSON.stringify(ctx.payload)) + return `` + default: + 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) { +} + +async function fetchPullRequestPatch(ctx: Context): Promise { + const pr = ctx.payload.pull_request + if (!pr) { core.warning(`No pull request in context`) return `` } + const octokit = github.getOctokit(core.getInput(`github-token`, { required: true })) + let patch: string try { const patchResp = await octokit.rest.pulls.get({ owner: ctx.repo.owner, repo: ctx.repo.repo, - [`pull_number`]: pull.number, + [`pull_number`]: pr.number, mediaType: { format: `diff`, }, @@ -77,6 +93,44 @@ async function fetchPatch(): Promise { } } +async function fetchPushPatch(ctx: Context): Promise { + const octokit = github.getOctokit(core.getInput(`github-token`, { required: true })) + + let patch: string + try { + const patchResp = await octokit.rest.repos.compareCommitsWithBasehead({ + owner: ctx.repo.owner, + repo: ctx.repo.repo, + basehead: `${ctx.payload.before}..${ctx.payload.after}`, + mediaType: { + format: `diff`, + }, + }) + + if (patchResp.status !== 200) { + core.warning(`failed to fetch push 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 push patch:`, err) + return `` // don't fail the action, but analyze without patch + } + + try { + const tempDir = await createTempDir() + const patchPath = path.join(tempDir, "push.patch") + core.info(`Writing patch to ${patchPath}`) + await writeFile(patchPath, alterDiffPatch(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 @@ -87,11 +141,9 @@ async function prepareEnv(): Promise { // Prepare cache, lint and go in parallel. await restoreCache() - const prepareLintPromise = prepareLint() - const patchPromise = fetchPatch() - const lintPath = await prepareLintPromise - const patchPath = await patchPromise + const lintPath = await prepareLint() + const patchPath = await fetchPatch() core.info(`Prepared env in ${Date.now() - startedAt}ms`) @@ -133,30 +185,57 @@ async function runLint(lintPath: string, patchPath: string): Promise { const userArgsMap = new Map(userArgsList) const userArgNames = new Set(userArgsList.map(([key]) => key)) - const formats = (userArgsMap.get("out-format") || "") - .trim() - .split(",") - .filter((f) => f.length > 0) - .filter((f) => !f.startsWith(`github-actions`)) - .concat("github-actions") - .join(",") + const annotations = core.getBooleanInput(`annotations`) - addedArgs.push(`--out-format=${formats}`) - userArgs = userArgs.replace(/--out-format=\S*/gi, "").trim() + if (annotations) { + const formats = (userArgsMap.get("out-format") || "") + .trim() + .split(",") + .filter((f) => f.length > 0) + .filter((f) => !f.startsWith(`github-actions`)) + .concat("github-actions") + .join(",") - if (patchPath) { + addedArgs.push(`--out-format=${formats}`) + userArgs = userArgs.replace(/--out-format=\S*/gi, "").trim() + } + + if (isOnlyNewIssues()) { if (userArgNames.has(`new`) || userArgNames.has(`new-from-rev`) || userArgNames.has(`new-from-patch`)) { 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 ctx = github.context + + core.info(`only new issues on ${ctx.eventName}: ${patchPath}`) + + switch (ctx.eventName) { + case `pull_request`: + case `pull_request_target`: + case `push`: + if (patchPath) { + addedArgs.push(`--new-from-patch=${patchPath}`) + + // Override config values. + addedArgs.push(`--new=false`) + addedArgs.push(`--new-from-rev=`) + } + break + case `merge_group`: + addedArgs.push(`--new-from-rev=${ctx.payload.merge_group.base_sha}`) + + // Override config values. + addedArgs.push(`--new=false`) + addedArgs.push(`--new-from-patch=`) + break + default: + break + } } - const workingDirectory = core.getInput(`working-directory`) const cmdArgs: ExecOptions = {} + + const workingDirectory = core.getInput(`working-directory`) if (workingDirectory) { if (!fs.existsSync(workingDirectory) || !fs.lstatSync(workingDirectory).isDirectory()) { throw new Error(`working-directory (${workingDirectory}) was not a path`) @@ -169,7 +248,7 @@ async function runLint(lintPath: string, patchPath: string): Promise { const cmd = `${lintPath} run ${addedArgs.join(` `)} ${userArgs}`.trimEnd() - core.info(`Running [${cmd}] in [${cmdArgs.cwd || ``}] ...`) + core.info(`Running [${cmd}] in [${cmdArgs.cwd || process.cwd()}] ...`) const startedAt = Date.now() try {