diff --git a/README.md b/README.md index d2b0c7bb..9ebd8860 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ your projects up-to-date to the latest release. Schedule an automatic daily or weekly workflow: as soon as a new Gradle release is available, the action will open a PR ready to be -merged. It's like [Dependabot](https://dependabot.com) for Gradle Wrapper. 🤖✨ +merged. It's like [Dependabot](https://github.com/dependabot) for Gradle Wrapper. 🤖✨ ![Pull Request](https://user-images.githubusercontent.com/316923/93274006-8922ef80-f7b9-11ea-8ec7-85c2704270eb.png @@ -25,10 +25,14 @@ Request](https://user-images.githubusercontent.com/316923/93274006-8922ef80-f7b9 - [`labels`](#labels) - [`base-branch`](#base-branch) - [`target-branch`](#target-branch) + - [`paths`](#paths) + - [`paths-ignore`](#paths-ignore) - [`set-distribution-checksum`](#set-distribution-checksum) - [Examples](#examples) - [Scheduling action execution](#scheduling-action-execution) - [Targeting a custom branch](#targeting-a-custom-branch) + - [Ignoring subprojects folders](#ignoring-subproject-folders) + - [Using `paths` and `paths-ignore` together](#using-paths-and-paths-ignore-together) - [FAQ](#faq) - [Running CI workflows in Pull Requests created by the action](#running-ci-workflows-in-pull-requests-created-by-the-action) - [Android Studio warning about `distributionSha256Sum`](#android-studio-warning-about-distributionsha256sum) @@ -128,9 +132,11 @@ This is the list of supported inputs: | [`repo-token`](#repo-token) | `GITHUB_TOKEN` or a Personal Access Token (PAT) with `repo` scope. | No | `GITHUB_TOKEN` | | [`reviewers`](#reviewers) | List of users to request a review from (comma or newline-separated). | No | (empty) | | [`team-reviewers`](#team-reviewers) | List of teams to request a review from (comma or newline-separated). | No | (empty) | -| [`labels`](#labels) | 'List of labels to set on the Pull Request (comma or newline-separated). | No | (empty) | +| [`labels`](#labels) | List of labels to set on the Pull Request (comma or newline-separated). | No | (empty) | | [`base-branch`](#base-branch) | Base branch where the action will run and update the Gradle Wrapper. | No | The default branch name of your repository. | | [`target-branch`](#target-branch) | Branch to create the Pull Request against. | No | The default branch name of your repository. | +| [`paths`](#paths) | List of paths where to search for Gradle Wrapper files (comma or newline-separated). | No | (empty) | +| [`paths-ignore`](#paths-ignore) | List of paths to be excluded when searching for Gradle Wrapper files (comma or newline-separated). | No | (empty) | | [`set-distribution-checksum`](#set-distribution-checksum) | Whether to set the `distributionSha256Sum` property. | No | `true` | --- @@ -293,6 +299,66 @@ with: --- +### `paths` + +| Name | Description | Required | Default | +| --- | --- | --- | --- | +| `paths` | List of paths where to search for Gradle Wrapper files (comma or newline-separated). | No | (empty) | + +By default all Gradle Wrapper files in the source tree will be autodiscovered and considered for update. Use `paths` to provide a specific list of paths where to look for `gradle-wrapper.jar`. + +For example, use a comma-separated list: + +```yaml +with: + paths: project-web/**, project-backend/** +``` + +or add each path on a different line (no comma needed): + +```yaml +with: + paths: | + project-web/** + project-backend/** +``` + +This input accepts glob patterns that use characters like `*` and `**`, for more information see [GitHub's cheat sheet](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet). + +`paths` and `paths-ignore` can be used together. `paths` is always evaluated before `paths-ignore`, look at [this example](#using-paths-and-paths-ignore-together). + +--- + +### `paths-ignore` + +| Name | Description | Required | Default | +| --- | --- | --- | --- | +| `paths-ignore` | List of paths to be excluded when searching for Gradle Wrapper files (comma or newline-separated). | No | (empty) | + +By default all Gradle Wrapper files in the source tree will be autodiscovered and considered for update. Use `paths-ignore` to specify paths that should be ignored during scan. + +For example, use a comma-separated list: + +```yaml +with: + paths-ignore: project-docs/**, project-examples/** +``` + +or add each path on a different line (no comma needed): + +```yaml +with: + paths-ignore: | + project-docs/** + project-examples/** +``` + +This input accepts glob patterns that use characters like `*` and `**`, for more information see [GitHub's cheat sheet](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet). + +`paths` and `paths-ignore` can be used together. `paths-ignore` is always evaluated after `paths`, look at [this example](#using-paths-and-paths-ignore-together). + +--- + ### `set-distribution-checksum` | Name | Description | Required | Default | @@ -362,6 +428,33 @@ with: target-branch: v2-dev ``` +### Ignoring subprojects folders + +There are cases where your repository contains folders for projects or subprojects that need to be kept at an older Gradle version. + +If you want to ignore such files when the action runs, use `paths-ignore` to configure project paths that contains Gradle Wrapper files that should not be updated. + +```yaml +with: + paths-ignore: examples/** +``` + +### Using `paths` and `paths-ignore` together + +`paths` and `paths-ignore` works as allowlist and blocklist systems. The evaluation rule is as follows: + +- the source tree is searched for all `gradle-wrapper.jar` files and the list is passed to the next step +- if `paths` is not empty, the paths that match the specified patterns are passed to the next step +- if `paths-ignore` is not empty, the paths that match the specified patterns are removed from the list + +For example, the following configuration will srarch for Gradle Wrapper files in the `sub-project` directory and its subdirectories, but not in the `sub-project/examples` directory. + +```yaml +with: + paths: sub-project/** + paths-ignore: sub-project/examples/** +``` + ## FAQ ### Running CI workflows in Pull Requests created by the action diff --git a/action.yml b/action.yml index bc61d2ad..c7a06ea6 100644 --- a/action.yml +++ b/action.yml @@ -33,6 +33,14 @@ inputs: description: 'Whether to set the `distributionSha256Sum` property in `gradle-wrapper.properties`.' required: false default: true + paths: + description: 'List of paths where to search for Gradle Wrapper files (comma or newline-separated).' + required: false + default: '' + paths-ignore: + description: 'List of paths to be excluded when searching for Gradle Wrapper files (comma or newline-separated).' + required: false + default: '' runs: using: 'node16' diff --git a/dist/index.js b/dist/index.js index a611cbfb..1c9f7d76 100644 --- a/dist/index.js +++ b/dist/index.js @@ -736,6 +736,16 @@ class ActionInputs { core .getInput('set-distribution-checksum', { required: false }) .toLowerCase() !== 'false'; + this.paths = core + .getInput('paths', { required: false }) + .split(/[\n,]/) + .map(r => r.trim()) + .filter(r => r.length); + this.pathsIgnore = core + .getInput('paths-ignore', { required: false }) + .split(/[\n,]/) + .map(r => r.trim()) + .filter(r => r.length); } } @@ -978,13 +988,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge Object.defineProperty(exports, "__esModule", ({ value: true })); exports.MainAction = void 0; const core = __importStar(__nccwpck_require__(2186)); -const glob = __importStar(__nccwpck_require__(8090)); const git = __importStar(__nccwpck_require__(8940)); const gitAuth = __importStar(__nccwpck_require__(1304)); const store = __importStar(__nccwpck_require__(5826)); const git_commit_1 = __nccwpck_require__(4779); const wrapperInfo_1 = __nccwpck_require__(6832); const wrapperUpdater_1 = __nccwpck_require__(7412); +const find_1 = __nccwpck_require__(2758); class MainAction { constructor(inputs, githubApi, githubOps, releases) { this.inputs = inputs; @@ -1008,8 +1018,7 @@ class MainAction { core.warning(`A pull request already exists that updates Gradle Wrapper to ${targetRelease.version}.`); return; } - const globber = yield glob.create('**/gradle/wrapper/gradle-wrapper.properties', { followSymbolicLinks: false }); - const wrappers = yield globber.glob(); + const wrappers = yield (0, find_1.findWrapperPropertiesFiles)(this.inputs.paths, this.inputs.pathsIgnore); core.debug(`Wrappers: ${JSON.stringify(wrappers, null, 2)}`); if (!wrappers.length) { core.warning('Unable to find Gradle Wrapper files in this project.'); @@ -1201,6 +1210,93 @@ and [\`team-reviewers\`](https://github.com/gradle-update/update-gradle-wrapper- exports.PostAction = PostAction; +/***/ }), + +/***/ 2758: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.findWrapperPropertiesFiles = void 0; +const core = __importStar(__nccwpck_require__(2186)); +const glob = __importStar(__nccwpck_require__(8090)); +const internal_match_kind_1 = __nccwpck_require__(1063); +const internal_pattern_1 = __nccwpck_require__(4536); +function findWrapperPropertiesFiles(pathsInclude, pathsIgnore) { + return __awaiter(this, void 0, void 0, function* () { + const globber = yield glob.create('**/gradle/wrapper/gradle-wrapper.properties', { followSymbolicLinks: false }); + let propertiesFiles = yield globber.glob(); + core.debug(`wrapper.properties found: ${JSON.stringify(propertiesFiles, null, 2)}`); + if (!propertiesFiles.length) { + return propertiesFiles; + } + if (pathsInclude.length) { + const toInclude = []; + for (const wrapperPath of propertiesFiles) { + let shouldInclude = false; + for (const searchPath of pathsInclude) { + const pattern = new internal_pattern_1.Pattern(searchPath); + const match = pattern.match(wrapperPath); + shouldInclude || (shouldInclude = match === internal_match_kind_1.MatchKind.All); + } + if (shouldInclude) { + toInclude.push(wrapperPath); + } + } + propertiesFiles = toInclude; + } + core.debug(`wrapper.properties after pathsInclude: ${JSON.stringify(propertiesFiles, null, 2)}`); + if (pathsIgnore.length) { + const toExclude = []; + for (const wrapperPath of propertiesFiles) { + let shouldExclude = false; + for (const searchPath of pathsIgnore) { + const pattern = new internal_pattern_1.Pattern(searchPath); + const match = pattern.match(wrapperPath); + shouldExclude || (shouldExclude = match === internal_match_kind_1.MatchKind.All); + } + if (shouldExclude) { + toExclude.push(wrapperPath); + } + } + propertiesFiles = propertiesFiles.filter(f => !toExclude.includes(f)); + } + core.debug(`wrapper.properties after pathsExclude: ${JSON.stringify(propertiesFiles, null, 2)}`); + return propertiesFiles; + }); +} +exports.findWrapperPropertiesFiles = findWrapperPropertiesFiles; + + /***/ }), /***/ 6832: diff --git a/src/inputs/index.ts b/src/inputs/index.ts index 7782ec9b..5df1edba 100644 --- a/src/inputs/index.ts +++ b/src/inputs/index.ts @@ -22,6 +22,8 @@ export interface Inputs { baseBranch: string; targetBranch: string; setDistributionChecksum: boolean; + paths: string[]; + pathsIgnore: string[]; } export function getInputs(): Inputs { @@ -36,6 +38,8 @@ class ActionInputs implements Inputs { baseBranch: string; targetBranch: string; setDistributionChecksum: boolean; + paths: string[]; + pathsIgnore: string[]; constructor() { this.repoToken = core.getInput('repo-token', {required: false}); @@ -69,5 +73,17 @@ class ActionInputs implements Inputs { core .getInput('set-distribution-checksum', {required: false}) .toLowerCase() !== 'false'; + + this.paths = core + .getInput('paths', {required: false}) + .split(/[\n,]/) + .map(r => r.trim()) + .filter(r => r.length); + + this.pathsIgnore = core + .getInput('paths-ignore', {required: false}) + .split(/[\n,]/) + .map(r => r.trim()) + .filter(r => r.length); } } diff --git a/src/tasks/main.ts b/src/tasks/main.ts index f1443f9a..4d0cbbb1 100644 --- a/src/tasks/main.ts +++ b/src/tasks/main.ts @@ -13,7 +13,6 @@ // limitations under the License. import * as core from '@actions/core'; -import * as glob from '@actions/glob'; import * as git from '../git/git-cmds'; import * as gitAuth from '../git/git-auth'; @@ -22,6 +21,7 @@ import * as store from '../store'; import {commit} from '../git/git-commit'; import {createWrapperInfo} from '../wrapperInfo'; import {createWrapperUpdater} from '../wrapperUpdater'; +import {findWrapperPropertiesFiles} from '../wrapper/find'; import {GitHubOps} from '../github/gh-ops'; import {IGitHubApi} from '../github/gh-api'; import {Inputs} from '../inputs'; @@ -68,11 +68,10 @@ export class MainAction { return; } - const globber = await glob.create( - '**/gradle/wrapper/gradle-wrapper.properties', - {followSymbolicLinks: false} + const wrappers = await findWrapperPropertiesFiles( + this.inputs.paths, + this.inputs.pathsIgnore ); - const wrappers = await globber.glob(); core.debug(`Wrappers: ${JSON.stringify(wrappers, null, 2)}`); if (!wrappers.length) { diff --git a/src/wrapper/find.ts b/src/wrapper/find.ts new file mode 100644 index 00000000..6ef32efc --- /dev/null +++ b/src/wrapper/find.ts @@ -0,0 +1,99 @@ +// Copyright 2020-2021 Cristian Greco +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as core from '@actions/core'; +import * as glob from '@actions/glob'; + +import {MatchKind} from '@actions/glob/lib/internal-match-kind'; +import {Pattern} from '@actions/glob/lib/internal-pattern'; + +export async function findWrapperPropertiesFiles( + pathsInclude: string[], + pathsIgnore: string[] +): Promise { + const globber = await glob.create( + '**/gradle/wrapper/gradle-wrapper.properties', + {followSymbolicLinks: false} + ); + + let propertiesFiles = await globber.glob(); + + core.debug( + `wrapper.properties found: ${JSON.stringify(propertiesFiles, null, 2)}` + ); + + if (!propertiesFiles.length) { + return propertiesFiles; + } + + if (pathsInclude.length) { + const toInclude: string[] = []; + + for (const wrapperPath of propertiesFiles) { + let shouldInclude = false; + + for (const searchPath of pathsInclude) { + const pattern = new Pattern(searchPath); + const match = pattern.match(wrapperPath); + + shouldInclude ||= match === MatchKind.All; + } + + if (shouldInclude) { + toInclude.push(wrapperPath); + } + } + + propertiesFiles = toInclude; + } + + core.debug( + `wrapper.properties after pathsInclude: ${JSON.stringify( + propertiesFiles, + null, + 2 + )}` + ); + + if (pathsIgnore.length) { + const toExclude: string[] = []; + + for (const wrapperPath of propertiesFiles) { + let shouldExclude = false; + + for (const searchPath of pathsIgnore) { + const pattern = new Pattern(searchPath); + const match = pattern.match(wrapperPath); + + shouldExclude ||= match === MatchKind.All; + } + + if (shouldExclude) { + toExclude.push(wrapperPath); + } + } + + propertiesFiles = propertiesFiles.filter(f => !toExclude.includes(f)); + } + + core.debug( + `wrapper.properties after pathsExclude: ${JSON.stringify( + propertiesFiles, + null, + 2 + )}` + ); + + return propertiesFiles; +} diff --git a/tests/github/gh-ops.test.ts b/tests/github/gh-ops.test.ts index bedf702d..91e0d11f 100644 --- a/tests/github/gh-ops.test.ts +++ b/tests/github/gh-ops.test.ts @@ -32,7 +32,9 @@ const defaultMockInputs: Inputs = { labels: [], baseBranch: '', targetBranch: '', - setDistributionChecksum: true + setDistributionChecksum: true, + paths: [], + pathsIgnore: [] }; const defaultMockGitHubApi: IGitHubApi = { diff --git a/tests/inputs/inputs.test.ts b/tests/inputs/inputs.test.ts index f59b560d..70261e6c 100644 --- a/tests/inputs/inputs.test.ts +++ b/tests/inputs/inputs.test.ts @@ -45,6 +45,8 @@ describe('getInputs', () => { ActionInputs { "baseBranch": "", "labels": Array [], + "paths": Array [], + "pathsIgnore": Array [], "repoToken": "s3cr3t", "reviewers": Array [], "setDistributionChecksum": true, diff --git a/tests/tasks/main.test.ts b/tests/tasks/main.test.ts index 234f5a67..7c12d079 100644 --- a/tests/tasks/main.test.ts +++ b/tests/tasks/main.test.ts @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as glob from '@actions/glob'; - import * as commit from '../../src/git/git-commit'; import * as git from '../../src/git/git-cmds'; import * as gitAuth from '../../src/git/git-auth'; import * as store from '../../src/store'; import * as wrapper from '../../src/wrapperInfo'; +import * as wrapperFind from '../../src/wrapper/find'; import * as wrapperUpdater from '../../src/wrapperUpdater'; import {GitHubOps} from '../../src/github/gh-ops'; @@ -40,7 +39,9 @@ const defaultMockInputs: Inputs = { labels: [], baseBranch: '', targetBranch: '', - setDistributionChecksum: true + setDistributionChecksum: true, + paths: [], + pathsIgnore: [] }; const defaultMockGitHubApi: IGitHubApi = { @@ -88,13 +89,9 @@ describe('run', () => { mockGitHubOps.findMatchingRef = jest.fn().mockReturnValue(undefined); - jest.spyOn(glob, 'create').mockResolvedValue({ - getSearchPaths: jest.fn(), - glob: jest - .fn() - .mockReturnValue(['/path/to/gradle/wrapper/gradle-wrapper.properties']), - globGenerator: jest.fn() - }); + jest + .spyOn(wrapperFind, 'findWrapperPropertiesFiles') + .mockResolvedValue(['/path/to/gradle/wrapper/gradle-wrapper.properties']); jest.spyOn(wrapper, 'createWrapperInfo').mockReturnValue({ version: '1.0.0', diff --git a/tests/wrapper/find.test.ts b/tests/wrapper/find.test.ts new file mode 100644 index 00000000..9608e21e --- /dev/null +++ b/tests/wrapper/find.test.ts @@ -0,0 +1,89 @@ +// Copyright 2020-2021 Cristian Greco +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as glob from '@actions/glob'; +import * as path from 'path'; + +import {findWrapperPropertiesFiles} from '../../src/wrapper/find'; + +describe('findWrapperPropertiesFiles', () => { + it('filters paths based on include and ignore lists', async () => { + const pathPrefix = path.dirname(__filename); + + jest.spyOn(glob, 'create').mockResolvedValue({ + getSearchPaths: jest.fn(), + glob: jest + .fn() + .mockReturnValue([ + `${pathPrefix}/path_a/gradle/wrapper/gradle-wrapper.properties`, + `${pathPrefix}/path_b/gradle/wrapper/gradle-wrapper.properties`, + `${pathPrefix}/path_b/subpath_c/gradle/wrapper/gradle-wrapper.properties` + ]), + globGenerator: jest.fn() + }); + + const tests: { + pathsInclude: string[]; + pathsIgnore: string[]; + pathsExpected: string[]; + }[] = [ + { + pathsInclude: [], + pathsIgnore: [], + pathsExpected: [ + `${pathPrefix}/path_a/gradle/wrapper/gradle-wrapper.properties`, + `${pathPrefix}/path_b/gradle/wrapper/gradle-wrapper.properties`, + `${pathPrefix}/path_b/subpath_c/gradle/wrapper/gradle-wrapper.properties` + ] + }, + { + pathsInclude: [`${pathPrefix}/path_a/**`], + pathsIgnore: [], + pathsExpected: [ + `${pathPrefix}/path_a/gradle/wrapper/gradle-wrapper.properties` + ] + }, + { + pathsInclude: [], + pathsIgnore: [`${pathPrefix}/path_a/**`], + pathsExpected: [ + `${pathPrefix}/path_b/gradle/wrapper/gradle-wrapper.properties`, + `${pathPrefix}/path_b/subpath_c/gradle/wrapper/gradle-wrapper.properties` + ] + }, + { + pathsInclude: [`${pathPrefix}/path_a/**`], + pathsIgnore: [`${pathPrefix}/path_a/**`], + pathsExpected: [] + }, + { + pathsInclude: [`${pathPrefix}/path_b/**`], + pathsIgnore: [`${pathPrefix}/path_b/subpath_c/**`], + pathsExpected: [ + `${pathPrefix}/path_b/gradle/wrapper/gradle-wrapper.properties` + ] + } + ]; + + for (const t of tests) { + const files = await findWrapperPropertiesFiles( + t.pathsInclude, + t.pathsIgnore + ); + expect(files).toEqual(t.pathsExpected); + } + + expect(glob.create).toHaveBeenCalledTimes(tests.length); + }); +});