From 74919bf9bed359ff6577213960f45f7ada4cc9c6 Mon Sep 17 00:00:00 2001 From: Matt Travi <126441+travi@users.noreply.github.com> Date: Tue, 17 Sep 2024 00:35:25 -0400 Subject: [PATCH] feat(rulesets): initial support repository rulesets (semi-dark release with the potential for breaking changes) (#1029) --- docs/plugins/rulesets.md | 59 ++++++++++ index.js | 4 +- lib/plugins/rulesets.js | 31 +++++ lib/settings.js | 4 +- package-lock.json | 83 +++++++------ package.json | 1 + test/integration/features/rulesets.feature | 19 +++ .../step_definitions/rulesets-steps.mjs | 109 ++++++++++++++++++ test/unit/lib/plugins/rulesets.test.js | 55 +++++++++ 9 files changed, 327 insertions(+), 38 deletions(-) create mode 100644 docs/plugins/rulesets.md create mode 100644 lib/plugins/rulesets.js create mode 100644 test/integration/features/rulesets.feature create mode 100644 test/integration/features/step_definitions/rulesets-steps.mjs create mode 100644 test/unit/lib/plugins/rulesets.test.js diff --git a/docs/plugins/rulesets.md b/docs/plugins/rulesets.md new file mode 100644 index 000000000..ef9c78123 --- /dev/null +++ b/docs/plugins/rulesets.md @@ -0,0 +1,59 @@ +# Repository Rulesets + +> [!WARNING] +> Support for Repository Rulesets is still under development. +> Details may still change, like how the configuration is defined in the `settings.yml`. +> Please follow [#732](https://github.com/repository-settings/app/issues/732) for progress updates. +> Feedback is appreciated, but adopt cautiously, with the expectation of breaking changes until support is fully released. + +See https://docs.github.com/en/rest/repos/rules#update-a-repository-ruleset for +all available ruleset properties and a description of each. + +```yaml +rulesets: + - name: prevent destruction of the default branch + target: branch + enforcement: active + conditions: + ref_name: + include: + - "~DEFAULT_BRANCH" + rules: + - type: deletion + - type: non_fast_forward + + - name: verification must pass + target: branch + enforcement: active + conditions: + ref_name: + include: + - "~DEFAULT_BRANCH" + rules: + - type: required_status_checks + parameters: + required_status_checks: + - context: test + integration_id: 123456 + bypass_actors: + - actor_id: 5 + actor_type: RepositoryRole + bypass_mode: pull_request + + - name: changes must be reviewed + target: branch + enforcement: active + conditions: + ref_name: + include: + - "~DEFAULT_BRANCH" + rules: + - type: pull_request + parameters: + required_approving_review_count: 1 + require_code_owner_review: true + bypass_actors: + - actor_id: 654321 + actor_type: Integration + bypass_mode: always +``` diff --git a/index.js b/index.js index eaa4e882f..e2bdc9952 100644 --- a/index.js +++ b/index.js @@ -46,7 +46,5 @@ export default (robot, _, Settings = SettingsApp) => { return syncSettings(context) }) - robot.on('repository.created', async context => { - return syncSettings(context) - }) + robot.on('repository.created', async context => syncSettings(context)) } diff --git a/lib/plugins/rulesets.js b/lib/plugins/rulesets.js new file mode 100644 index 000000000..8c47f20b4 --- /dev/null +++ b/lib/plugins/rulesets.js @@ -0,0 +1,31 @@ +import deepEqual from 'deep-equal' + +import Diffable from './diffable.js' + +export default class Rulesets extends Diffable { + async find () { + const { data: rulesets } = await this.github.repos.getRepoRulesets(this.repo) + + return rulesets + } + + comparator (existing, attrs) { + return existing.name === attrs.name + } + + changed (existing, attrs) { + return !deepEqual(existing, attrs) + } + + update (existing, attrs) { + return this.github.repos.updateRepoRuleset({ ...this.repo, ruleset_id: existing.ruleset_id, ...attrs }) + } + + remove (existing) { + return this.github.repos.deleteRepoRuleset({ ...this.repo, ruleset_id: existing.ruleset_id }) + } + + async add (attrs) { + await this.github.repos.createRepoRuleset({ ...this.repo, ...attrs }) + } +} diff --git a/lib/settings.js b/lib/settings.js index 54e2087f7..0e2b97acd 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -5,6 +5,7 @@ import Teams from './plugins/teams.js' import Milestones from './plugins/milestones.js' import Branches from './plugins/branches.js' import Environments from './plugins/environments.js' +import Rulesets from './plugins/rulesets.js' export default class Settings { static sync (github, repo, config) { @@ -49,5 +50,6 @@ Settings.PLUGINS = { teams: Teams, milestones: Milestones, branches: Branches, - environments: Environments + environments: Environments, + rulesets: Rulesets } diff --git a/package-lock.json b/package-lock.json index 5a20dd813..fce46543d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0-semantically-released", "license": "ISC", "dependencies": { + "deep-equal": "^2.2.3", "deepmerge": "4.3.1", "js-yaml": "4.1.0", "probot": "13.3.7" @@ -4225,7 +4226,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.5", @@ -4511,7 +4511,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -5819,6 +5818,38 @@ } } }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5856,7 +5887,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -6397,7 +6427,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -7864,7 +7893,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.1.3" @@ -7979,7 +8007,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8358,7 +8385,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8414,7 +8440,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -8815,7 +8840,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -8913,7 +8937,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -8930,7 +8953,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -8969,7 +8991,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, "license": "MIT", "dependencies": { "has-bigints": "^1.0.1" @@ -8995,7 +9016,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -9036,7 +9056,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9101,7 +9120,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -9252,7 +9270,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9295,7 +9312,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -9371,7 +9387,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -9398,7 +9413,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9411,7 +9425,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.7" @@ -9440,7 +9453,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -9456,7 +9468,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.2" @@ -9488,7 +9499,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9514,7 +9524,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -9553,7 +9562,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/isexe": { @@ -14356,11 +14364,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -14370,7 +14393,6 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.5", @@ -15175,7 +15197,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -16733,7 +16754,6 @@ "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.6", @@ -18252,7 +18272,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -19287,7 +19306,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, "license": "MIT", "dependencies": { "internal-slot": "^1.0.4" @@ -21406,7 +21424,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, "license": "MIT", "dependencies": { "is-bigint": "^1.0.1", @@ -21450,7 +21467,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, "license": "MIT", "dependencies": { "is-map": "^2.0.3", @@ -21469,7 +21485,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", diff --git a/package.json b/package.json index 553684482..425c815a5 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "author": "Brandon Keepers", "license": "ISC", "dependencies": { + "deep-equal": "^2.2.3", "deepmerge": "4.3.1", "js-yaml": "4.1.0", "probot": "13.3.7" diff --git a/test/integration/features/rulesets.feature b/test/integration/features/rulesets.feature new file mode 100644 index 000000000..41de57f73 --- /dev/null +++ b/test/integration/features/rulesets.feature @@ -0,0 +1,19 @@ +Feature: Repository Rulesets + + Scenario: Add a ruleset + Given no rulesets are defined for the repository + And a ruleset is defined in the config + When a settings sync is triggered + Then the ruleset is enabled for the repository + + Scenario: Update a ruleset + Given a ruleset exists for the repository + And the ruleset is modified in the config + When a settings sync is triggered + Then the ruleset is updated + + Scenario: Delete a ruleset + Given a ruleset exists for the repository + And the ruleset is removed from the config + When a settings sync is triggered + Then the ruleset is deleted diff --git a/test/integration/features/step_definitions/rulesets-steps.mjs b/test/integration/features/step_definitions/rulesets-steps.mjs new file mode 100644 index 000000000..6409cc991 --- /dev/null +++ b/test/integration/features/step_definitions/rulesets-steps.mjs @@ -0,0 +1,109 @@ +import { dump } from 'js-yaml' +import { StatusCodes } from 'http-status-codes' + +import { Given, Then } from '@cucumber/cucumber' +import assert from 'node:assert' +import { http, HttpResponse } from 'msw' + +import { repository } from './common-steps.mjs' +import settings from '../../../../lib/settings.js' +import any from '@travi/any' + +const rulesetId = any.integer() +const rulesetName = any.word() + +Given('no rulesets are defined for the repository', async function () { + this.server.use( + http.get(`https://api.github.com/repos/${repository.owner.name}/${repository.name}/rulesets`, ({ request }) => + HttpResponse.json([]) + ) + ) +}) + +Given('a ruleset exists for the repository', async function () { + this.server.use( + http.get(`https://api.github.com/repos/${repository.owner.name}/${repository.name}/rulesets`, ({ request }) => + HttpResponse.json([{ ruleset_id: rulesetId, name: rulesetName, enforcement: 'evaluate' }]) + ) + ) +}) + +Given('a ruleset is defined in the config', async function () { + this.ruleset = { name: any.word() } + + this.server.use( + http.get( + `https://api.github.com/repos/${repository.owner.name}/${repository.name}/contents/${encodeURIComponent( + settings.FILE_NAME + )}`, + ({ request }) => HttpResponse.arrayBuffer(Buffer.from(dump({ rulesets: [this.ruleset] }))) + ), + http.post( + `https://api.github.com/repos/${repository.owner.name}/${repository.name}/rulesets`, + async ({ request }) => { + this.createdRuleset = await request.json() + + return new HttpResponse(null, { status: StatusCodes.CREATED }) + } + ) + ) +}) + +Given('the ruleset is modified in the config', async function () { + this.ruleset = { name: rulesetName, enforcement: 'active' } + + this.server.use( + http.get( + `https://api.github.com/repos/${repository.owner.name}/${repository.name}/contents/${encodeURIComponent( + settings.FILE_NAME + )}`, + ({ request }) => + HttpResponse.arrayBuffer( + Buffer.from( + dump({ + rulesets: [{ ruleset_id: rulesetId, ...this.ruleset }] + }) + ) + ) + ), + http.put( + `https://api.github.com/repos/${repository.owner.name}/${repository.name}/rulesets/${rulesetId}`, + async ({ request }) => { + this.updatedRuleset = await request.json() + + return new HttpResponse(null, { status: StatusCodes.OK }) + } + ) + ) +}) + +Given('the ruleset is removed from the config', async function () { + this.server.use( + http.get( + `https://api.github.com/repos/${repository.owner.name}/${repository.name}/contents/${encodeURIComponent( + settings.FILE_NAME + )}`, + ({ request }) => HttpResponse.arrayBuffer(Buffer.from(dump({ rulesets: [] }))) + ), + http.delete( + `https://api.github.com/repos/${repository.owner.name}/${repository.name}/rulesets/:rulesetId`, + async ({ params }) => { + this.removedRuleset = params.rulesetId + + return new HttpResponse(null, { status: StatusCodes.NO_CONTENT }) + } + ) + ) +}) + +Then('the ruleset is enabled for the repository', async function () { + assert.deepEqual(this.createdRuleset, this.ruleset) +}) + +Then('the ruleset is updated', async function () { + assert.deepEqual(this.updatedRuleset, this.ruleset) +}) + +Then('the ruleset is deleted', async function () { + assert.equal(this.removedRuleset, rulesetId) +}) diff --git a/test/unit/lib/plugins/rulesets.test.js b/test/unit/lib/plugins/rulesets.test.js new file mode 100644 index 000000000..46b6cb566 --- /dev/null +++ b/test/unit/lib/plugins/rulesets.test.js @@ -0,0 +1,55 @@ +import { jest } from '@jest/globals' +import { when } from 'jest-when' +import any from '@travi/any' + +import Rulesets from '../../../../lib/plugins/rulesets' + +function configure (github, owner, repo, config) { + return new Rulesets(github, { owner, repo }, config) +} + +describe('rulesets', () => { + let github + const owner = any.word() + const repo = any.word() + + beforeEach(() => { + github = { + repos: { + createRepoRuleset: jest.fn(), + deleteRepoRuleset: jest.fn(), + getRepoRulesets: jest.fn(), + updateRepoRuleset: jest.fn() + } + } + }) + + it('should sync rulesets', async () => { + const updatedValue = any.word() + const existingRulesetId = any.integer() + const removedRulesetId = any.integer() + const existingRuleset = { name: any.word(), foo: updatedValue } + const newRuleset = { name: any.word() } + const plugin = configure(github, owner, repo, [newRuleset, existingRuleset]) + when(github.repos.getRepoRulesets) + .calledWith({ owner, repo }) + .mockResolvedValue({ + data: [ + { ruleset_id: existingRulesetId, ...existingRuleset, foo: any.word() }, + { ruleset_id: removedRulesetId, name: any.word() } + ] + }) + + await plugin.sync() + + expect(github.repos.createRepoRuleset).toHaveBeenCalledWith({ owner, repo, ...newRuleset }) + expect(github.repos.updateRepoRuleset).toHaveBeenCalledWith({ + owner, + repo, + ruleset_id: existingRulesetId, + ...existingRuleset, + foo: updatedValue + }) + expect(github.repos.deleteRepoRuleset).toHaveBeenCalledWith({ owner, repo, ruleset_id: removedRulesetId }) + }) +})