From 678b02e533bf513e1289e8758b0eff5929247456 Mon Sep 17 00:00:00 2001 From: Simon Boyer <36060258+Simon-Boyer@users.noreply.github.com> Date: Tue, 14 May 2024 21:22:35 -0400 Subject: [PATCH] feat: repository custom properties plugin (#626) * feat: repository custom properties plugin * docs: custom properties --- README.md | 6 + app.yml | 1 + docs/sample-settings/repo.yml | 8 +- lib/plugins/custom_properties.js | 67 +++++++++ lib/settings.js | 3 +- .../lib/plugins/custom_properties.test.js | 128 ++++++++++++++++++ 6 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 lib/plugins/custom_properties.js create mode 100644 test/unit/lib/plugins/custom_properties.test.js diff --git a/README.md b/README.md index 4a45fe74..fe8d58c8 100644 --- a/README.md +++ b/README.md @@ -498,6 +498,12 @@ branches: users: [] teams: [] +# Custom properties +# See https://docs.github.com/en/rest/repos/custom-properties?apiVersion=2022-11-28 +custom_properties: + - name: test + value: test + # See the docs (https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/configuring-autolinks-to-reference-external-resources) for a description of autolinks and replacement values. autolinks: - key_prefix: 'JIRA-' diff --git a/app.yml b/app.yml index a7206cf9..99403d76 100644 --- a/app.yml +++ b/app.yml @@ -30,6 +30,7 @@ default_events: # the value (for example, write). # Valid values are `read`, `write`, and `none` default_permissions: + repository_custom_properties: write organization_custom_properties: admin # Repository creation, deletion, settings, teams, and collaborators. diff --git a/docs/sample-settings/repo.yml b/docs/sample-settings/repo.yml index 3a71ebc4..a3d70914 100644 --- a/docs/sample-settings/repo.yml +++ b/docs/sample-settings/repo.yml @@ -196,7 +196,13 @@ branches: apps: [] users: [] teams: [] - + +# Custom properties +# See https://docs.github.com/en/rest/repos/custom-properties?apiVersion=2022-11-28 +custom_properties: + - name: test + value: test + validator: pattern: '[a-zA-Z0-9_-]+_[a-zA-Z0-9_-]+.*' diff --git a/lib/plugins/custom_properties.js b/lib/plugins/custom_properties.js new file mode 100644 index 00000000..cb0adf7d --- /dev/null +++ b/lib/plugins/custom_properties.js @@ -0,0 +1,67 @@ +const Diffable = require('./diffable') + +module.exports = class CustomProperties extends Diffable { + constructor (...args) { + super(...args) + + if (this.entries) { + // Force all names to lowercase to avoid comparison issues. + this.entries.forEach(prop => { + prop.name = prop.name.toLowerCase() + }) + } + } + + async find () { + const data = await this.github.request('GET /repos/:org/:repo/properties/values', { + org: this.repo.owner, + repo: this.repo.repo + }) + + const properties = data.data.map(d => { return { name: d.property_name, value: d.value } }) + return properties + } + + comparator (existing, attrs) { + return existing.name === attrs.name + } + + changed (existing, attrs) { + return attrs.value !== existing.value + } + + async update (existing, attrs) { + await this.github.request('PATCH /repos/:org/:repo/properties/values', { + org: this.repo.owner, + repo: this.repo.repo, + properties: [{ + property_name: attrs.name, + value: attrs.value + }] + }) + } + + async add (attrs) { + await this.github.request('PATCH /repos/:org/:repo/properties/values', { + org: this.repo.owner, + repo: this.repo.repo, + properties: [{ + property_name: attrs.name, + value: attrs.value + }] + }) + } + + async remove (existing) { + await this.github.request('PATCH /repos/:org/:repo/properties/values', { + org: this.repo.owner, + repo: this.repo.repo, + properties: [ + { + property_name: existing.name, + value: null + } + ] + }) + } +} diff --git a/lib/settings.js b/lib/settings.js index 5253a519..21cac18b 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -836,7 +836,8 @@ Settings.PLUGINS = { autolinks: require('./plugins/autolinks'), validator: require('./plugins/validator'), rulesets: require('./plugins/rulesets'), - environments: require('./plugins/environments') + environments: require('./plugins/environments'), + custom_properties: require('./plugins/custom_properties.js') } module.exports = Settings diff --git a/test/unit/lib/plugins/custom_properties.test.js b/test/unit/lib/plugins/custom_properties.test.js new file mode 100644 index 00000000..69568a28 --- /dev/null +++ b/test/unit/lib/plugins/custom_properties.test.js @@ -0,0 +1,128 @@ +const CustomProperties = require('../../../../lib/plugins/custom_properties') + +describe('CustomProperties', () => { + let github + const repo = { owner: 'owner', repo: 'repo' } + let log + + function configure (config) { + const nop = false; + const errors = [] + return new CustomProperties(nop, github, { owner: 'bkeepers', repo: 'test' }, config, log, errors) + } + + beforeEach(() => { + github = { + request: jest.fn() + // .mockResolvedValue({ + // data: [ + // { property_name: 'test', value: 'test' } + // ] + // }) + } + log = { debug: jest.fn(), error: console.error } + }) + + describe('sync', () => { + it('syncs custom properties', async () => { + const plugin = configure([ + { name: 'test', value: 'test' } + ]) + + github.request.mockResolvedValue({ + data: [ + { property_name: 'test', value: 'test' } + ] + }) + + return plugin.sync().then(() => { + expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/properties/values', { + org: 'bkeepers', + repo: 'test' + }) + }) + }) + }) + describe('sync', () => { + it('add custom properties', async () => { + const plugin = configure([ + { name: 'test', value: 'test' } + ]) + + github.request.mockResolvedValue({ + data: [] + }) + + return plugin.sync().then(() => { + expect(github.request).toHaveBeenNthCalledWith(1, 'GET /repos/:org/:repo/properties/values', { + org: 'bkeepers', + repo: 'test' + }) + expect(github.request).toHaveBeenNthCalledWith(2, 'PATCH /repos/:org/:repo/properties/values', { + org: 'bkeepers', + repo: 'test', + properties: [ + { + property_name: 'test', + value: 'test' + } + ] + }) + }) + }) + }) + describe('sync', () => { + it('remove custom properties', async () => { + const plugin = configure([]) + + github.request.mockResolvedValue({ + data: [{ property_name: 'test', value: 'test' }] + }) + + return plugin.sync().then(() => { + expect(github.request).toHaveBeenNthCalledWith(1, 'GET /repos/:org/:repo/properties/values', { + org: 'bkeepers', + repo: 'test' + }) + expect(github.request).toHaveBeenNthCalledWith(2, 'PATCH /repos/:org/:repo/properties/values', { + org: 'bkeepers', + repo: 'test', + properties: [ + { + property_name: 'test', + value: null + } + ] + }) + }) + }) + }) + describe('sync', () => { + it('update custom properties', async () => { + const plugin = configure([ + { name: 'test', value: 'foobar' } + ]) + + github.request.mockResolvedValue({ + data: [{ property_name: 'test', value: 'test' }] + }) + + return plugin.sync().then(() => { + expect(github.request).toHaveBeenNthCalledWith(1, 'GET /repos/:org/:repo/properties/values', { + org: 'bkeepers', + repo: 'test' + }) + expect(github.request).toHaveBeenNthCalledWith(2, 'PATCH /repos/:org/:repo/properties/values', { + org: 'bkeepers', + repo: 'test', + properties: [ + { + property_name: 'test', + value: 'foobar' + } + ] + }) + }) + }) + }) +}) \ No newline at end of file