From 613359b993076c3798ddaf850f4403a6277f239c Mon Sep 17 00:00:00 2001 From: Jared Murrell <865381+primetheus@users.noreply.github.com> Date: Sun, 26 May 2024 16:48:13 -0400 Subject: [PATCH] added support for repository variables --- lib/plugins/variables.js | 204 ++++++++++++++++++++++++ lib/settings.js | 3 +- package-lock.json | 4 +- package.json | 5 +- test/fixtures/variables-config.yml | 5 + test/unit/lib/plugins/variables.test.js | 66 ++++++++ 6 files changed, 282 insertions(+), 5 deletions(-) create mode 100644 lib/plugins/variables.js create mode 100644 test/fixtures/variables-config.yml create mode 100644 test/unit/lib/plugins/variables.test.js diff --git a/lib/plugins/variables.js b/lib/plugins/variables.js new file mode 100644 index 00000000..9622618c --- /dev/null +++ b/lib/plugins/variables.js @@ -0,0 +1,204 @@ +/* eslint-disable no-unused-expressions */ +/* eslint-disable quotes */ +const _ = require('lodash') +const Diffable = require('./diffable') + +module.exports = class Variables extends Diffable { + constructor (...args) { + super(...args) + + if (this.entries) { + // Force all names to uppercase to avoid comparison issues. + this.entries.forEach((variable) => { + variable.name = variable.name.toUpperCase() + }) + } + } + + /** + * Look-up existing variables for a given repository + * + * @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#list-repository-variables} list repository variables + * @returns {Array.} Returns a list of variables that exist in a repository + */ + find () { + const result = async () => { + return await this.github + .request('GET /repos/:org/:repo/actions/variables', { + org: this.repo.owner, + repo: this.repo.repo + }) + .then((res) => { + return res + }) + .catch((e) => { + this.logError(e) + }) + } + + return result.data.variables + } + + /** + * Compare the existing variables with what we've defined as code + * + * @param {Array.} existing Existing variables defined in the repository + * @param {Array.} variables Variables that we have defined as code + * + * @returns {object} The results of a list comparison + */ + getChanged (existing, variables = []) { + const result = + JSON.stringify( + existing.sort((x1, x2) => { + x1.name.toUpperCase() - x2.name.toUpperCase() + }) + ) !== + JSON.stringify( + variables.sort((x1, x2) => { + x1.name.toUpperCase() - x2.name.toUpperCase() + }) + ) + return result + } + + /** + * Compare existing variables with what's defined + * + * @param {Object} existing The existing entries in GitHub + * @param {Object} attrs The entries defined as code + * + * @returns + */ + comparator (existing, attrs) { + return existing.name === attrs.name + } + + /** + * Return a list of changed entries + * + * @param {Object} existing The existing entries in GitHub + * @param {Object} attrs The entries defined as code + * + * @returns + */ + changed (existing, attrs) { + return this.getChanged(_.castArray(existing), _.castArray(attrs)) + } + + /** + * Update an existing variable if the value has changed + * + * @param {Array.} existing Existing variables defined in the repository + * @param {Array.} variables Variables that we have defined as code + * + * @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#update-a-repository-variable} update a repository variable + * @returns + */ + async update (existing, variables = []) { + existing = _.castArray(existing) + variables = _.castArray(variables) + const changed = this.getChanged(existing, variables) + + if (changed) { + let existingVariables = [...existing] + for (const variable of variables) { + const existingVariable = existingVariables.find((_var) => _var.name === variable.name) + if (existingVariable) { + existingVariables = existingVariables.filter((_var) => _var.name !== variable.name) + if (existingVariable.value !== variable.value) { + await this.github + .request(`PATCH /repos/:org/:repo/actions/variables/:variable_name`, { + org: this.repo.owner, + repo: this.repo.repo, + variable_name: variable.name.toUpperCase(), + value: variable.value.toString() + }) + .then((res) => { + return res + }) + .catch((e) => { + this.logError(e) + }) + } + } else { + await this.github + .request(`POST /repos/:org/:repo/actions/variables`, { + org: this.repo.owner, + repo: this.repo.repo, + name: variable.name.toUpperCase(), + value: variable.value.toString() + }) + .then((res) => { + return res + }) + .catch((e) => { + this.logError(e) + }) + } + } + + for (const variable of existingVariables) { + await this.github + .request('DELETE /repos/:org/:repo/actions/variables/:variable_name', { + org: this.repo.owner, + repo: this.repo.repo, + variable_name: variable.name.toUpperCase() + }) + .then((res) => { + return res + }) + .catch((e) => { + this.logError(e) + }) + } + } + } + + /** + * Add a new variable to a given repository + * + * @param {object} variable The variable to add, with name and value + * + * @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#create-a-repository-variable} create a repository variable + * @returns + */ + async add (variable) { + await this.github + .request(`POST /repos/:org/:repo/actions/variables`, { + org: this.repo.owner, + repo: this.repo.repo, + name: variable.name, + value: variable.value.toString() + }) + .then((res) => { + return res + }) + .catch((e) => { + this.logError(e) + }) + } + + /** + * Remove variables that aren't defined as code + * + * @param {String} existing Name of the existing variable to remove + * + * @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#delete-a-repository-variable} delete a repository variable + * @returns + */ + async remove (existing) { + await this.github + .request(`DELETE /repos/:org/:repo/actions/variables/:variable_name`, { + org: this.repo.owner, + repo: this.repo.repo, + variable_name: existing.name + }) + .then((res) => { + return res + }) + .catch((e) => { + this.logError(e) + }) + } +} diff --git a/lib/settings.js b/lib/settings.js index 21cac18b..e2e43993 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -837,7 +837,8 @@ Settings.PLUGINS = { validator: require('./plugins/validator'), rulesets: require('./plugins/rulesets'), environments: require('./plugins/environments'), - custom_properties: require('./plugins/custom_properties.js') + custom_properties: require('./plugins/custom_properties.js'), + variables: require('./plugins/variables'), } module.exports = Settings diff --git a/package-lock.json b/package-lock.json index 8f6a56bb..ee568c70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "deepmerge": "^4.3.1", "eta": "^3.0.3", "js-yaml": "^4.1.0", + "lodash": "^4.17.21", "node-cron": "^3.0.2", "octokit": "^3.1.2", "probot": "^12.3.3" @@ -8136,8 +8137,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.defaults": { "version": "4.2.0", diff --git a/package.json b/package.json index 7353a635..dbad5350 100644 --- a/package.json +++ b/package.json @@ -27,15 +27,16 @@ "deepmerge": "^4.3.1", "eta": "^3.0.3", "js-yaml": "^4.1.0", + "lodash": "^4.17.21", "node-cron": "^3.0.2", "octokit": "^3.1.2", "probot": "^12.3.3" }, "devDependencies": { + "@eslint/eslintrc": "^2.0.2", "@travi/any": "^2.1.8", "check-engine": "^1.10.1", "eslint": "^8.46.0", - "@eslint/eslintrc": "^2.0.2", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-node": "^11.1.0", @@ -83,4 +84,4 @@ "." ] } -} \ No newline at end of file +} diff --git a/test/fixtures/variables-config.yml b/test/fixtures/variables-config.yml new file mode 100644 index 00000000..5a079762 --- /dev/null +++ b/test/fixtures/variables-config.yml @@ -0,0 +1,5 @@ +variables: + - name: MY_VAR_1 + permission: batman + - name: MY_VAR_2 + permission: superman diff --git a/test/unit/lib/plugins/variables.test.js b/test/unit/lib/plugins/variables.test.js new file mode 100644 index 00000000..e3607c70 --- /dev/null +++ b/test/unit/lib/plugins/variables.test.js @@ -0,0 +1,66 @@ +const { when } = require('jest-when'); +const Variables = require('../../../../lib/plugins/variables'); + +describe('Variables', () => { + let github; + const org = 'bkeepers'; + const repo = 'test'; + + function fillVariables(variables = []) { + return variables; + } + + beforeAll(() => { + github = { + request: jest.fn().mockReturnValue(Promise.resolve(true)), + }; + }); + + it('sync', () => { + const plugin = new Variables(undefined, github, { owner: org, repo }, [{ name: 'test', value: 'test' }], { + debug() {}, + }); + + when(github.request) + .calledWith('GET /repos/:org/:repo/actions/variables', { org, repo }) + .mockResolvedValue({ + data: { + variables: [ + fillVariables({ + variables: [], + }), + ], + }, + }); + + ['variables'].forEach(() => { + when(github.request) + .calledWith('GET /repos/:org/:repo/actions/variables', { org, repo }) + .mockResolvedValue({ + data: { + variables: [], + }, + }); + }); + + when(github.request).calledWith('POST /repos/:org/:repo/actions/variables').mockResolvedValue({}); + + return plugin.sync().then(() => { + expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/actions/variables', { org, repo }); + + ['variables'].forEach(() => { + expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/actions/variables', { org, repo }); + }); + + expect(github.request).toHaveBeenCalledWith( + 'POST /repos/:org/:repo/actions/variables', + expect.objectContaining({ + org, + repo, + name: 'TEST', + value: 'test', + }) + ); + }); + }); +});