diff --git a/.github/.cache_version b/.github/.cache_version index 1e7c7a7364..7262faa78c 100644 --- a/.github/.cache_version +++ b/.github/.cache_version @@ -1 +1 @@ -8.0.5.1 +8.0.5.2 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..78a740046e --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +./scripts/ci/husky/pre-commit.js diff --git a/config/generation.config.js b/config/generation.config.js new file mode 100644 index 0000000000..d325f85548 --- /dev/null +++ b/config/generation.config.js @@ -0,0 +1,30 @@ +// eslint-disable-next-line import/no-commonjs +module.exports = { + patterns: [ + // Ignore the roots and go down the tree by negating hand written files + 'clients/**', + '!clients/README.md', + '!clients/**/.openapi-generator-ignore', + + // Java + '!clients/algoliasearch-client-java-2/algoliasearch-core/src/com/algolia/exceptions/*', + '!clients/algoliasearch-client-java-2/algoliasearch-core/src/com/algolia/utils/*', + 'clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/echo/EchoResponse*.java', + '!clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/echo/EchoResponseInterface.java', + + // JavaScript + '!clients/algoliasearch-client-javascript/*', + '!clients/algoliasearch-client-javascript/.github/**', + '!clients/algoliasearch-client-javascript/.yarn/**', + '!clients/algoliasearch-client-javascript/scripts/**', + '!clients/algoliasearch-client-javascript/packages/algoliasearch/**', + '!clients/algoliasearch-client-javascript/packages/requester-*/**', + '!clients/algoliasearch-client-javascript/packages/client-common/**', + + // PHP + '!clients/algoliasearch-client-php/lib/Configuration/*', + 'clients/algoliasearch-client-php/lib/*.php', + 'clients/algoliasearch-client-php/lib/Api/*', + 'clients/algoliasearch-client-php/lib/Configuration/Configuration.php', + ], +}; diff --git a/package.json b/package.json index 503d874f16..a6ac06e8ea 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "docker:setup": "yarn docker:clean && yarn docker:build && yarn docker:mount", "fix:json": "eslint --ext=json . --fix", "github-actions:lint": "eslint --ext=yml .github/", - "postinstall": "yarn workspace eslint-plugin-automation-custom build", + "postinstall": "husky install && yarn workspace eslint-plugin-automation-custom build", "playground:browser": "yarn workspace javascript-browser-playground start", "release": "yarn workspace scripts createReleaseIssue", "scripts:lint": "eslint --ext=ts scripts/", @@ -50,6 +50,7 @@ "eslint-plugin-prettier": "4.0.0", "eslint-plugin-unused-imports": "2.0.0", "eslint-plugin-yml": "0.14.0", + "husky": "7.0.4", "json": "11.0.0", "mustache": "4.2.0", "prettier": "2.6.2", diff --git a/scripts/ci/husky/__tests__/pre-commit.test.js b/scripts/ci/husky/__tests__/pre-commit.test.js new file mode 100644 index 0000000000..bad3774922 --- /dev/null +++ b/scripts/ci/husky/__tests__/pre-commit.test.js @@ -0,0 +1,24 @@ +/* eslint-disable import/no-commonjs */ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { createMemoizedMicromatchMatcher } = require('../pre-commit'); + +describe('createMemoizedMicromatchMatcher', () => { + it('matches correctly', () => { + const matcher = createMemoizedMicromatchMatcher([ + 'clients/**', + '!clients/README.md', + ]); + + expect(matcher('clients/README.md')).toEqual(false); + expect(matcher('clients/CONTRIBUTING.md')).toEqual(true); + }); + + it('prioritizes the exact match when two patterns conflict', () => { + const matcher = createMemoizedMicromatchMatcher([ + '!lib/Configuration/*', + 'lib/Configuration/Configuration.php', + ]); + + expect(matcher('lib/Configuration/Configuration.php')).toEqual(true); + }); +}); diff --git a/scripts/ci/husky/pre-commit.js b/scripts/ci/husky/pre-commit.js new file mode 100755 index 0000000000..4004b4cb96 --- /dev/null +++ b/scripts/ci/husky/pre-commit.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ +/* eslint-disable import/no-commonjs */ +/* eslint-disable @typescript-eslint/no-var-requires */ +const chalk = require('chalk'); +const execa = require('execa'); +const micromatch = require('micromatch'); + +const GENERATED_FILE_PATTERNS = + require('../../../config/generation.config').patterns; + +const run = async (command, { cwd } = {}) => { + return ( + (await execa.command(command, { shell: 'bash', all: true, cwd })).all ?? '' + ); +}; + +function createMemoizedMicromatchMatcher(patterns = []) { + const exactMatchers = []; + const positiveMatchers = []; + const negativeMatchers = []; + + patterns.forEach((pattern) => { + if (pattern.startsWith('!')) { + // Patterns starting with `!` are negated + negativeMatchers.push(micromatch.matcher(pattern.slice(1))); + } else if (!pattern.includes('*')) { + exactMatchers.push(micromatch.matcher(pattern)); + } else { + positiveMatchers.push(micromatch.matcher(pattern)); + } + }); + + return function matcher(str) { + if (exactMatchers.some((match) => match(str))) { + return true; + } + + // As `some` returns false on empty array, test will always fail if we only + // provide `negativeMatchers`. We fallback to `true` is it's the case. + const hasPositiveMatchers = + Boolean(positiveMatchers.length === 0 && negativeMatchers.length) || + positiveMatchers.some((match) => match(str)); + + return hasPositiveMatchers && !negativeMatchers.some((match) => match(str)); + }; +} + +async function preCommit() { + const stagedFiles = (await run(`git diff --name-only --cached`)).split('\n'); + const deletedFiles = new Set( + (await run(`git ls-files --deleted`)).split('\n') + ); + const matcher = createMemoizedMicromatchMatcher(GENERATED_FILE_PATTERNS); + + for (const stagedFile of stagedFiles) { + // keep the deleted files staged even if they were generated before. + if (deletedFiles.has(stagedFile)) { + continue; + } + + if (!matcher(stagedFile)) { + continue; + } + + console.log( + chalk.bgYellow('[INFO]'), + `Generated file found, unstaging: ${stagedFile}` + ); + + await run(`git restore --staged ${stagedFile}`); + } +} + +if (require.main === module && !process.env.CI) { + preCommit(); +} + +module.exports = { + createMemoizedMicromatchMatcher, +}; diff --git a/scripts/package.json b/scripts/package.json index d29c13ac83..ff0262be5c 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -5,6 +5,7 @@ "cleanGeneratedBranch": "ts-node ci/codegen/cleanGeneratedBranch.ts", "createMatrix": "ts-node ci/createMatrix.ts", "createReleaseIssue": "ts-node release/create-release-issue.ts", + "pre-commit": "./ci/husky/pre-commit.js", "processRelease": "ts-node release/process-release.ts", "pushGeneratedCode": "ts-node ci/codegen/pushGeneratedCode.ts", "setRunVariables": "ts-node ci/setRunVariables.ts", @@ -19,9 +20,11 @@ "@types/inquirer": "8.2.1", "@types/jest": "27.4.1", "@types/js-yaml": "4.0.5", + "@types/micromatch": "4.0.2", "@types/mustache": "4.1.2", "@types/node": "16.11.26", "@types/semver": "7.3.9", + "chalk": "4.1.2", "commander": "9.1.0", "dotenv": "16.0.0", "eslint": "8.12.0", @@ -31,6 +34,7 @@ "inquirer": "8.2.2", "jest": "27.5.1", "js-yaml": "4.1.0", + "micromatch": "4.0.5", "mustache": "4.2.0", "openapi-types": "10.0.0", "ora-classic": "5.4.2", diff --git a/website/docs/commitAndPullRequest.md b/website/docs/commitAndPullRequest.md new file mode 100644 index 0000000000..a30e4e5198 --- /dev/null +++ b/website/docs/commitAndPullRequest.md @@ -0,0 +1,17 @@ +--- +title: Commit and Pull-request +--- + +# Commit and Pull-request + +## Commit + +If you accidentally include generated files in your commit, the `pre-commit` hook will automatically unstage them. + +We create commits on the CI as well, and in that case, we skip this unstaging behavior with the environment variable `CI=true` given. + +If you want to change the patterns of generated file paths, see [config/generation.config.js](https://github.com/algolia/api-clients-automation/blob/main/config/generation.config.js). + +## Pull-request + +Semantic title is required. It's validated by [GitHub Action](https://github.com/deepakputhraya/action-pr-title). See [pr-title.yml](https://github.com/algolia/api-clients-automation/blob/main/.github/workflows/pr-title.yml) for the complete regular expressions. diff --git a/website/docs/introduction.md b/website/docs/introduction.md index a260fa4b47..2f94b3cc2c 100644 --- a/website/docs/introduction.md +++ b/website/docs/introduction.md @@ -13,7 +13,7 @@ To contribute to the repository, make sure to take a look at our guidelines and - [Setup the repository tooling](/docs/setupRepository): to install our tooling. - [Add a new client](/docs/addNewClient): to add a new client spec to generate. - [Support a new language](/docs/addNewLanguage): to add a new supported language to the API clients. -- [Pull-request](/docs/pullRequest): to see how to send pull-requests. +- [Commit and Pull-request](/docs/commitAndPullRequest): to see how to commit and send pull-requests. - [Release process](/docs/releaseProcess): to see how to release API clients. CLI commands can be found at [CLI > specs commands](/docs/specsCommands) and [CLI > generation commands](/docs/generationCommands) diff --git a/website/docs/pullRequest.md b/website/docs/pullRequest.md deleted file mode 100644 index efc315396a..0000000000 --- a/website/docs/pullRequest.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Pull-request ---- - -# Pull-request - -Semantic title is required. It's validated by [GitHub Action](https://github.com/deepakputhraya/action-pr-title). See [pr-title.yml](https://github.com/algolia/api-clients-automation/blob/main/.github/workflows/pr-title.yml) for the complete regular expressions. diff --git a/website/sidebars.js b/website/sidebars.js index 8f3e3ed422..26e86508d4 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -19,7 +19,7 @@ const sidebars = { }, 'addNewClient', 'addNewLanguage', - 'pullRequest', + 'commitAndPullRequest', 'releaseProcess', ], }, diff --git a/yarn.lock b/yarn.lock index 4deaf783ac..5f8597aae7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,6 +26,7 @@ __metadata: eslint-plugin-prettier: 4.0.0 eslint-plugin-unused-imports: 2.0.0 eslint-plugin-yml: 0.14.0 + husky: 7.0.4 json: 11.0.0 mustache: 4.2.0 prettier: 2.6.2 @@ -5528,6 +5529,13 @@ __metadata: languageName: node linkType: hard +"@types/braces@npm:*": + version: 3.0.1 + resolution: "@types/braces@npm:3.0.1" + checksum: 3749f7673a03d498ddb2f199b222bb403e53e78ac05a599c757c2049703ece802602c78640af0ff826be0fd2ea8b03daff04ce18806ed739592302195b7a569b + languageName: node + linkType: hard + "@types/connect-history-api-fallback@npm:^1.3.5": version: 1.3.5 resolution: "@types/connect-history-api-fallback@npm:1.3.5" @@ -5746,6 +5754,15 @@ __metadata: languageName: node linkType: hard +"@types/micromatch@npm:4.0.2": + version: 4.0.2 + resolution: "@types/micromatch@npm:4.0.2" + dependencies: + "@types/braces": "*" + checksum: 6c678e9c625d5b422c6d2c1001da1c502ecc4457248343bbd324b79fd798a6563e336a4d79630d80e8202312013dd7cf8b4440afa644d04477abd26fde6fba24 + languageName: node + linkType: hard + "@types/mime@npm:^1": version: 1.3.2 resolution: "@types/mime@npm:1.3.2" @@ -12248,6 +12265,15 @@ __metadata: languageName: node linkType: hard +"husky@npm:7.0.4": + version: 7.0.4 + resolution: "husky@npm:7.0.4" + bin: + husky: lib/bin.js + checksum: c6ec4af63da2c9522da8674a20ad9b48362cc92704896cc8a58c6a2a39d797feb2b806f93fbd83a6d653fbdceb2c3b6e0b602c6b2e8565206ffc2882ef7db9e9 + languageName: node + linkType: hard + "iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" @@ -14731,6 +14757,16 @@ __metadata: languageName: node linkType: hard +"micromatch@npm:4.0.5, micromatch@npm:^4.0.5": + version: 4.0.5 + resolution: "micromatch@npm:4.0.5" + dependencies: + braces: ^3.0.2 + picomatch: ^2.3.1 + checksum: 02a17b671c06e8fefeeb6ef996119c1e597c942e632a21ef589154f23898c9c6a9858526246abb14f8bca6e77734aa9dcf65476fca47cedfb80d9577d52843fc + languageName: node + linkType: hard + "micromatch@npm:^4.0.2, micromatch@npm:^4.0.4": version: 4.0.4 resolution: "micromatch@npm:4.0.4" @@ -14741,16 +14777,6 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.5": - version: 4.0.5 - resolution: "micromatch@npm:4.0.5" - dependencies: - braces: ^3.0.2 - picomatch: ^2.3.1 - checksum: 02a17b671c06e8fefeeb6ef996119c1e597c942e632a21ef589154f23898c9c6a9858526246abb14f8bca6e77734aa9dcf65476fca47cedfb80d9577d52843fc - languageName: node - linkType: hard - "miller-rabin@npm:^4.0.0": version: 4.0.1 resolution: "miller-rabin@npm:4.0.1" @@ -19032,9 +19058,11 @@ __metadata: "@types/inquirer": 8.2.1 "@types/jest": 27.4.1 "@types/js-yaml": 4.0.5 + "@types/micromatch": 4.0.2 "@types/mustache": 4.1.2 "@types/node": 16.11.26 "@types/semver": 7.3.9 + chalk: 4.1.2 commander: 9.1.0 dotenv: 16.0.0 eslint: 8.12.0 @@ -19044,6 +19072,7 @@ __metadata: inquirer: 8.2.2 jest: 27.5.1 js-yaml: 4.1.0 + micromatch: 4.0.5 mustache: 4.2.0 openapi-types: 10.0.0 ora-classic: 5.4.2