Skip to content

Commit c078aee

Browse files
committed
add support for repository variables (#798)
1 parent e61cf05 commit c078aee

File tree

4 files changed

+282
-1
lines changed

4 files changed

+282
-1
lines changed

lib/plugins/variables.js

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
const _ = require('lodash')
2+
const Diffable = require('./diffable')
3+
4+
module.exports = class Variables extends Diffable {
5+
constructor (...args) {
6+
super(...args)
7+
8+
if (this.entries) {
9+
// Force all names to uppercase to avoid comparison issues.
10+
this.entries.forEach((variable) => {
11+
variable.name = variable.name.toUpperCase()
12+
})
13+
}
14+
}
15+
16+
/**
17+
* Look-up existing variables for a given repository
18+
*
19+
* @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#list-repository-variables} list repository variables
20+
* @returns {Array.<object>} Returns a list of variables that exist in a repository
21+
*/
22+
async find () {
23+
this.log.debug(`Finding repo vars for ${this.repo.owner}/${this.repo.repo}`)
24+
const { data: { variables } } = await this.github.request('GET /repos/:org/:repo/actions/variables', {
25+
org: this.repo.owner,
26+
repo: this.repo.repo
27+
})
28+
return variables
29+
}
30+
31+
/**
32+
* Compare the existing variables with what we've defined as code
33+
*
34+
* @param {Array.<object>} existing Existing variables defined in the repository
35+
* @param {Array.<object>} variables Variables that we have defined as code
36+
*
37+
* @returns {object} The results of a list comparison
38+
*/
39+
getChanged (existing, variables = []) {
40+
const result =
41+
JSON.stringify(
42+
existing.sort((x1, x2) => {
43+
x1.name.toUpperCase() - x2.name.toUpperCase()
44+
})
45+
) !==
46+
JSON.stringify(
47+
variables.sort((x1, x2) => {
48+
x1.name.toUpperCase() - x2.name.toUpperCase()
49+
})
50+
)
51+
return result
52+
}
53+
54+
/**
55+
* Compare existing variables with what's defined
56+
*
57+
* @param {Object} existing The existing entries in GitHub
58+
* @param {Object} attrs The entries defined as code
59+
*
60+
* @returns
61+
*/
62+
comparator (existing, attrs) {
63+
return existing.name === attrs.name
64+
}
65+
66+
/**
67+
* Return a list of changed entries
68+
*
69+
* @param {Object} existing The existing entries in GitHub
70+
* @param {Object} attrs The entries defined as code
71+
*
72+
* @returns
73+
*/
74+
changed (existing, attrs) {
75+
return this.getChanged(_.castArray(existing), _.castArray(attrs))
76+
}
77+
78+
/**
79+
* Update an existing variable if the value has changed
80+
*
81+
* @param {Array.<object>} existing Existing variables defined in the repository
82+
* @param {Array.<object>} variables Variables that we have defined as code
83+
*
84+
* @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#update-a-repository-variable} update a repository variable
85+
* @returns
86+
*/
87+
async update (existing, variables = []) {
88+
this.log.debug(`Updating a repo var existing params ${JSON.stringify(existing)} and new ${JSON.stringify(variables)}`)
89+
existing = _.castArray(existing)
90+
variables = _.castArray(variables)
91+
const changed = this.getChanged(existing, variables)
92+
93+
if (changed) {
94+
let existingVariables = [...existing]
95+
for (const variable of variables) {
96+
const existingVariable = existingVariables.find((_var) => _var.name === variable.name)
97+
if (existingVariable) {
98+
existingVariables = existingVariables.filter((_var) => _var.name !== variable.name)
99+
if (existingVariable.value !== variable.value) {
100+
await this.github
101+
.request('PATCH /repos/:org/:repo/actions/variables/:variable_name', {
102+
org: this.repo.owner,
103+
repo: this.repo.repo,
104+
variable_name: variable.name.toUpperCase(),
105+
value: variable.value.toString()
106+
})
107+
.then((res) => {
108+
return res
109+
})
110+
.catch((e) => {
111+
this.logError(e)
112+
})
113+
}
114+
} else {
115+
await this.github
116+
.request('POST /repos/:org/:repo/actions/variables', {
117+
org: this.repo.owner,
118+
repo: this.repo.repo,
119+
name: variable.name.toUpperCase(),
120+
value: variable.value.toString()
121+
})
122+
.then((res) => {
123+
return res
124+
})
125+
.catch((e) => {
126+
this.logError(e)
127+
})
128+
}
129+
}
130+
131+
for (const variable of existingVariables) {
132+
await this.github
133+
.request('DELETE /repos/:org/:repo/actions/variables/:variable_name', {
134+
org: this.repo.owner,
135+
repo: this.repo.repo,
136+
variable_name: variable.name.toUpperCase()
137+
})
138+
.then((res) => {
139+
return res
140+
})
141+
.catch((e) => {
142+
this.logError(e)
143+
})
144+
}
145+
}
146+
}
147+
148+
/**
149+
* Add a new variable to a given repository
150+
*
151+
* @param {object} variable The variable to add, with name and value
152+
*
153+
* @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#create-a-repository-variable} create a repository variable
154+
* @returns
155+
*/
156+
async add (variable) {
157+
this.log.debug(`Adding a repo var with the parms ${JSON.stringify(variable)}`)
158+
await this.github
159+
.request('POST /repos/:org/:repo/actions/variables', {
160+
org: this.repo.owner,
161+
repo: this.repo.repo,
162+
name: variable.name,
163+
value: variable.value.toString()
164+
})
165+
.then((res) => {
166+
return res
167+
})
168+
.catch((e) => {
169+
this.logError(e)
170+
})
171+
}
172+
173+
/**
174+
* Remove variables that aren't defined as code
175+
*
176+
* @param {String} existing Name of the existing variable to remove
177+
*
178+
* @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#delete-a-repository-variable} delete a repository variable
179+
* @returns
180+
*/
181+
async remove (existing) {
182+
this.log.debug(`Removing a repo var with the params ${JSON.stringify(existing)}`)
183+
await this.github
184+
.request('DELETE /repos/:org/:repo/actions/variables/:variable_name', {
185+
org: this.repo.owner,
186+
repo: this.repo.repo,
187+
variable_name: existing.name
188+
})
189+
.then((res) => {
190+
return res
191+
})
192+
.catch((e) => {
193+
this.logError(e)
194+
})
195+
}
196+
}

lib/settings.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,8 @@ Settings.PLUGINS = {
925925
validator: require('./plugins/validator'),
926926
rulesets: require('./plugins/rulesets'),
927927
environments: require('./plugins/environments'),
928-
custom_properties: require('./plugins/custom_properties.js')
928+
custom_properties: require('./plugins/custom_properties.js'),
929+
variables: require('./plugins/variables')
929930
}
930931

931932
module.exports = Settings

test/fixtures/variables-config.yml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
variables:
2+
- name: MY_VAR_1
3+
permission: batman
4+
- name: MY_VAR_2
5+
permission: superman
+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
const { when } = require('jest-when')
2+
const Variables = require('../../../../lib/plugins/variables')
3+
4+
describe('Variables', () => {
5+
let github
6+
const org = 'bkeepers'
7+
const repo = 'test'
8+
9+
function fillVariables (variables = []) {
10+
return variables
11+
}
12+
13+
function configure (config) {
14+
const log = { debug: console.debug, error: console.error }
15+
const errors = []
16+
return new Variables(undefined, github, { owner: org, repo }, [{ name: 'test', value: 'test' }], log, errors)
17+
}
18+
19+
beforeAll(() => {
20+
github = {
21+
request: jest.fn().mockReturnValue(Promise.resolve(true))
22+
}
23+
})
24+
25+
it('sync', () => {
26+
const plugin = configure()
27+
28+
when(github.request)
29+
.calledWith('GET /repos/:org/:repo/actions/variables', { org, repo })
30+
.mockResolvedValue({
31+
data: {
32+
variables: [
33+
fillVariables({
34+
variables: []
35+
})
36+
]
37+
}
38+
});
39+
40+
['variables'].forEach(() => {
41+
when(github.request)
42+
.calledWith('GET /repos/:org/:repo/actions/variables', { org, repo })
43+
.mockResolvedValue({
44+
data: {
45+
variables: [{ name: 'DELETE_me', value: 'test' }]
46+
}
47+
})
48+
})
49+
50+
when(github.request).calledWith('POST /repos/:org/:repo/actions/variables').mockResolvedValue({})
51+
52+
return plugin.sync().then(() => {
53+
expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/actions/variables', { org, repo });
54+
55+
['variables'].forEach(() => {
56+
expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/actions/variables', { org, repo })
57+
})
58+
59+
expect(github.request).toHaveBeenCalledWith(
60+
'DELETE /repos/:org/:repo/actions/variables/:variable_name',
61+
expect.objectContaining({
62+
org,
63+
repo,
64+
variable_name: 'DELETE_me'
65+
})
66+
)
67+
68+
expect(github.request).toHaveBeenCalledWith(
69+
'POST /repos/:org/:repo/actions/variables',
70+
expect.objectContaining({
71+
org,
72+
repo,
73+
name: 'TEST',
74+
value: 'test'
75+
})
76+
)
77+
})
78+
})
79+
})

0 commit comments

Comments
 (0)