From c078aee32cb8f9c7cd5e00a226bcaa5c51a0be86 Mon Sep 17 00:00:00 2001 From: tonpatel Date: Tue, 29 Apr 2025 20:55:39 -0400 Subject: [PATCH 1/2] add support for repository variables (#798) --- lib/plugins/variables.js | 196 ++++++++++++++++++++++++ lib/settings.js | 3 +- test/fixtures/variables-config.yml | 5 + test/unit/lib/plugins/variables.test.js | 79 ++++++++++ 4 files changed, 282 insertions(+), 1 deletion(-) 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 000000000..ce1c1ca12 --- /dev/null +++ b/lib/plugins/variables.js @@ -0,0 +1,196 @@ +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 + */ + async find () { + this.log.debug(`Finding repo vars for ${this.repo.owner}/${this.repo.repo}`) + const { data: { variables } } = await this.github.request('GET /repos/:org/:repo/actions/variables', { + org: this.repo.owner, + repo: this.repo.repo + }) + return 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 = []) { + this.log.debug(`Updating a repo var existing params ${JSON.stringify(existing)} and new ${JSON.stringify(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) { + this.log.debug(`Adding a repo var with the parms ${JSON.stringify(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) { + this.log.debug(`Removing a repo var with the params ${JSON.stringify(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 8f9022529..5abb2f6ef 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -925,7 +925,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/test/fixtures/variables-config.yml b/test/fixtures/variables-config.yml new file mode 100644 index 000000000..5a079762f --- /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 000000000..1953c0ede --- /dev/null +++ b/test/unit/lib/plugins/variables.test.js @@ -0,0 +1,79 @@ +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 + } + + function configure (config) { + const log = { debug: console.debug, error: console.error } + const errors = [] + return new Variables(undefined, github, { owner: org, repo }, [{ name: 'test', value: 'test' }], log, errors) + } + + beforeAll(() => { + github = { + request: jest.fn().mockReturnValue(Promise.resolve(true)) + } + }) + + it('sync', () => { + const plugin = configure() + + 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: [{ name: 'DELETE_me', value: 'test' }] + } + }) + }) + + 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( + 'DELETE /repos/:org/:repo/actions/variables/:variable_name', + expect.objectContaining({ + org, + repo, + variable_name: 'DELETE_me' + }) + ) + + expect(github.request).toHaveBeenCalledWith( + 'POST /repos/:org/:repo/actions/variables', + expect.objectContaining({ + org, + repo, + name: 'TEST', + value: 'test' + }) + ) + }) + }) +}) From 5905e152a43c2fb049b102f560262579148b9f0f Mon Sep 17 00:00:00 2001 From: tonpatel Date: Sat, 17 May 2025 18:01:40 -0400 Subject: [PATCH 2/2] fix: fix comparison operator and remove unused files for repository variables (#798) --- lib/plugins/variables.js | 6 +++--- test/fixtures/variables-config.yml | 5 ----- test/unit/lib/plugins/variables.test.js | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 test/fixtures/variables-config.yml diff --git a/lib/plugins/variables.js b/lib/plugins/variables.js index ce1c1ca12..25795c408 100644 --- a/lib/plugins/variables.js +++ b/lib/plugins/variables.js @@ -40,12 +40,12 @@ module.exports = class Variables extends Diffable { const result = JSON.stringify( existing.sort((x1, x2) => { - x1.name.toUpperCase() - x2.name.toUpperCase() + return x1.name.toUpperCase().localeCompare(x2.name.toUpperCase()) }) ) !== JSON.stringify( variables.sort((x1, x2) => { - x1.name.toUpperCase() - x2.name.toUpperCase() + return x1.name.toUpperCase().localeCompare(x2.name.toUpperCase()) }) ) return result @@ -154,7 +154,7 @@ module.exports = class Variables extends Diffable { * @returns */ async add (variable) { - this.log.debug(`Adding a repo var with the parms ${JSON.stringify(variable)}`) + this.log.debug(`Adding a repo var with the params ${JSON.stringify(variable)}`) await this.github .request('POST /repos/:org/:repo/actions/variables', { org: this.repo.owner, diff --git a/test/fixtures/variables-config.yml b/test/fixtures/variables-config.yml deleted file mode 100644 index 5a079762f..000000000 --- a/test/fixtures/variables-config.yml +++ /dev/null @@ -1,5 +0,0 @@ -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 index 1953c0ede..2784d7afd 100644 --- a/test/unit/lib/plugins/variables.test.js +++ b/test/unit/lib/plugins/variables.test.js @@ -10,7 +10,7 @@ describe('Variables', () => { return variables } - function configure (config) { + function configure () { const log = { debug: console.debug, error: console.error } const errors = [] return new Variables(undefined, github, { owner: org, repo }, [{ name: 'test', value: 'test' }], log, errors)