Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add support for repository variables #635

Open
wants to merge 1 commit into
base: main-enterprise
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions lib/plugins/variables.js
Original file line number Diff line number Diff line change
@@ -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.<object>} Returns a list of variables that exist in a repository
*/
find () {
const result = async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code was failing the tests, perhaps the find method could be rewritten as

Suggested change
const result = async () => {
async find () {
const res = await this.github.request('GET /repos/:org/:repo/actions/variables', {
org: this.repo.owner,
repo: this.repo.repo
})
const properties = res.data.map(d => { return { name: d.property_name, value: d.value } })
return properties
}

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.<object>} existing Existing variables defined in the repository
* @param {Array.<object>} 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.<object>} existing Existing variables defined in the repository
* @param {Array.<object>} 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)
})
}
}
3 changes: 2 additions & 1 deletion lib/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -83,4 +84,4 @@
"."
]
}
}
}
5 changes: 5 additions & 0 deletions test/fixtures/variables-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
variables:
- name: MY_VAR_1
permission: batman
- name: MY_VAR_2
permission: superman
66 changes: 66 additions & 0 deletions test/unit/lib/plugins/variables.test.js
Original file line number Diff line number Diff line change
@@ -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',
})
);
});
});
});
Loading