From 19e2691e2c1af133aed4939256e4c82ead1ebde9 Mon Sep 17 00:00:00 2001
From: Brad-Abrams <157060836+Brad-Abrams@users.noreply.github.com>
Date: Thu, 15 Aug 2024 12:01:39 -0400
Subject: [PATCH 1/6] Environments tolerate concise configuration (#649)
* decompose unit tests, patch sync for environments
* remove logging, combine loops as per review comments
* Add NopCommand, log.error, and errors
* Allow concise config for Environments
This commit combines PR [616](https://github.com/github/safe-settings/pull/616) and [646](https://github.com/github/safe-settings/pull/646)
environments.js
Add defensive code to prevent the GitHub API from being called with undefined data.
In the UI, and API an environment can be added with just an name.
Now, safe-settings permits this as well.
In the UI, and API an environment can be added without variables.
Now, safe-settings permits this as well.
In the UI, and API an environment can be added without deployment_protection_rules.
Now, safe-settings permits this as well.
environments.test.js
Add a test case for the scenario when there are zero existing environments
Add a test case for an environment name change
Add a test case inspired by PR 616 which adds 7 new environments with various attributes
Move expect statements out of aftereach() as there is now variability in what is expected across test cases.
Specifically, when there is no existing environment, that environment should NOT be queried for variables nor deployment_protection_rules
* Update documentation: Environments permissions.
Addresses issue: [Environments do not get provisioned for repositories set to internal or private #623](https://github.com/github/safe-settings/issues/623)
Adds documentation for permissions required for safe-settings when Environments are used
[List Environments](https://docs.github.com/en/rest/deployments/environments?apiVersion=2022-11-28#list-environments) API requires:
```
The fine-grained token must have the following permission set:
"Actions" repository permissions (read)
```
[Create an environment variable](https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#create-an-environment-variable) API requires:
```
The fine-grained token must have the following permission set:
"Variables" repository permissions (write) and "Environments" repository permissions (write)
```
With permissions added, issue 623 was resolved.
---
README.md | 5 +-
app.yml | 12 +
docs/deploy.md | 3 +
lib/plugins/environments.js | 136 ++-
test/unit/lib/plugins/environments.test.js | 1264 ++++++++++++++++----
5 files changed, 1160 insertions(+), 260 deletions(-)
diff --git a/README.md b/README.md
index 0c5f00bd..20a29bfe 100644
--- a/README.md
+++ b/README.md
@@ -266,9 +266,9 @@ And the `checkrun` page will look like this:
-### The Settings File
+### The Settings Files
-The settings file can be used to set the policies at the `org`, `suborg` or `repo` level.
+The settings files can be used to set the policies at the `org`, `suborg` or `repo` level.
The following can be configured:
@@ -284,6 +284,7 @@ The following can be configured:
- `Autolinks`
- `Repository name validation` using regex pattern
- `Rulesets`
+- `Environments` - wait timer, required reviewers, prevent self review, protected branches deployment branch policy, custom deployment branch policy, variables, deployment protection rules
It is possible to provide an `include` or `exclude` settings to restrict the `collaborators`, `teams`, `labels` to a list of repos or exclude a set of repos for a collaborator.
diff --git a/app.yml b/app.yml
index 24c28282..44dd0bdc 100644
--- a/app.yml
+++ b/app.yml
@@ -34,6 +34,10 @@ default_permissions:
repository_custom_properties: write
organization_custom_properties: admin
+ # Workflows, workflow runs and artifacts. (needed to read environments when repo is private or internal)
+ # https://developer.github.com/v3/apps/permissions/#repository-permissions-for-actions
+ actions: read
+
# Repository creation, deletion, settings, teams, and collaborators.
# https://developer.github.com/v3/apps/permissions/#permission-on-administration
administration: write
@@ -50,6 +54,10 @@ default_permissions:
# https://developer.github.com/v3/apps/permissions/#permission-on-deployments
# deployments: read
+ # Manage repository environments.
+ # https://developer.github.com/v3/apps/permissions/#repository-permissions-for-environments
+ environments: write
+
# Issues and related comments, assignees, labels, and milestones.
# https://developer.github.com/v3/apps/permissions/#permission-on-issues
issues: write
@@ -106,6 +114,10 @@ default_permissions:
# https://developer.github.com/v3/apps/permissions/
organization_administration: write
+ # Manage Actions repository variables.
+ # https://developer.github.com/v3/apps/permissions/#repository-permissions-for-variables
+ variables: write
+
# The name of the GitHub App. Defaults to the name specified in package.json
name: Safe Settings
diff --git a/docs/deploy.md b/docs/deploy.md
index 7e016777..ba3dff7f 100644
--- a/docs/deploy.md
+++ b/docs/deploy.md
@@ -255,14 +255,17 @@ Every deployment will need an [App](https://developer.github.com/apps/).
#### Repository Permissions
+- Actions: **Read-only**
- Administration: **Read & Write**
- Checks: **Read & Write**
- Commit statuses: **Read & Write**
- Contents: **Read & Write**
- Custom properties: **Read & Write**
+- Environments: **Read & Write**
- Issues: **Read & Write**
- Metadata: **Read-only**
- Pull requests: **Read & Write**
+- Variables: **Read & Write**
#### Organization Permissions
diff --git a/lib/plugins/environments.js b/lib/plugins/environments.js
index 3435388e..6d52b409 100644
--- a/lib/plugins/environments.js
+++ b/lib/plugins/environments.js
@@ -1,4 +1,6 @@
const Diffable = require('./diffable')
+const MergeDeep = require('../mergeDeep')
+const NopCommand = require('../nopcommand')
module.exports = class Environments extends Diffable {
constructor(...args) {
@@ -14,7 +16,11 @@ module.exports = class Environments extends Diffable {
});
}
})
- }
+ }
+
+ // Remove 'name' from filtering list so Environments with only a name defined are processed.
+ MergeDeep.NAME_FIELDS.splice(MergeDeep.NAME_FIELDS.indexOf('name'), 1)
+
}
async find() {
@@ -78,7 +84,7 @@ module.exports = class Environments extends Diffable {
const wait_timer = existing.wait_timer !== attrs.wait_timer;
const prevent_self_review = existing.prevent_self_review !== attrs.prevent_self_review;
const reviewers = JSON.stringify(existing.reviewers.sort((x1, x2) => x1.id - x2.id)) !== JSON.stringify(attrs.reviewers.sort((x1, x2) => x1.id - x2.id));
-
+
let existing_custom_branch_policies = existing.deployment_branch_policy === null ? null : existing.deployment_branch_policy.custom_branch_policies;
if(typeof(existing_custom_branch_policies) === 'object' && existing_custom_branch_policies !== null) {
existing_custom_branch_policies = existing_custom_branch_policies.sort();
@@ -158,6 +164,7 @@ module.exports = class Environments extends Diffable {
if(variables) {
let existingVariables = [...existing.variables];
+
for(let variable of attrs.variables) {
const existingVariable = existingVariables.find((_var) => _var.name === variable.name);
if(existingVariable) {
@@ -195,6 +202,7 @@ module.exports = class Environments extends Diffable {
if(deployment_protection_rules) {
let existingRules = [...existing.deployment_protection_rules];
+
for(let rule of attrs.deployment_protection_rules) {
const existingRule = existingRules.find((_rule) => _rule.id === rule.id);
@@ -227,13 +235,14 @@ module.exports = class Environments extends Diffable {
wait_timer: attrs.wait_timer,
prevent_self_review: attrs.prevent_self_review,
reviewers: attrs.reviewers,
- deployment_branch_policy: attrs.deployment_branch_policy === null ? null : {
- protected_branches: attrs.deployment_branch_policy.protected_branches,
+ deployment_branch_policy: attrs.deployment_branch_policy == null ? null : {
+ protected_branches: !!attrs.deployment_branch_policy.protected_branches,
custom_branch_policies: !!attrs.deployment_branch_policy.custom_branch_policies
}
});
if(attrs.deployment_branch_policy && attrs.deployment_branch_policy.custom_branch_policies) {
+
for(let policy of attrs.deployment_branch_policy.custom_branch_policies) {
await this.github.request('POST /repos/:org/:repo/environments/:environment_name/deployment-branch-policies', {
org: this.repo.owner,
@@ -242,26 +251,34 @@ module.exports = class Environments extends Diffable {
name: policy.name
});
}
- }
-
- for(let variable of attrs.variables) {
- await this.github.request(`POST /repos/:org/:repo/environments/:environment_name/variables`, {
- org: this.repo.owner,
- repo: this.repo.repo,
- environment_name: attrs.name,
- name: variable.name,
- value: variable.value
- });
}
- for(let rule of attrs.deployment_protection_rules) {
- await this.github.request(`POST /repos/:org/:repo/environments/:environment_name/deployment_protection_rules`, {
- org: this.repo.owner,
- repo: this.repo.repo,
- environment_name: attrs.name,
- integration_id: rule.app_id
- });
+ if(attrs.variables) {
+
+ for(let variable of attrs.variables) {
+ await this.github.request(`POST /repos/:org/:repo/environments/:environment_name/variables`, {
+ org: this.repo.owner,
+ repo: this.repo.repo,
+ environment_name: attrs.name,
+ name: variable.name,
+ value: variable.value
+ });
+ }
+
+ }
+
+ if(attrs.deployment_protection_rules) {
+
+ for(let rule of attrs.deployment_protection_rules) {
+ await this.github.request(`POST /repos/:org/:repo/environments/:environment_name/deployment_protection_rules`, {
+ org: this.repo.owner,
+ repo: this.repo.repo,
+ environment_name: attrs.name,
+ integration_id: rule.app_id
+ });
+ }
+
}
}
@@ -272,4 +289,79 @@ module.exports = class Environments extends Diffable {
environment_name: existing.name
});
}
-}
\ No newline at end of file
+
+ sync () {
+ const resArray = []
+ if (this.entries) {
+ let filteredEntries = this.filterEntries()
+ return this.find().then(existingRecords => {
+
+ // Filter out all empty entries (usually from repo override)
+ for (const entry of filteredEntries) {
+ for (const key of Object.keys(entry)) {
+ if (entry[key] === null || entry[key] === undefined) {
+ delete entry[key]
+ }
+ }
+ }
+ filteredEntries = filteredEntries.filter(entry => Object.keys(entry).filter(key => !MergeDeep.NAME_FIELDS.includes(key)).length !== 0)
+
+ const changes = []
+
+ existingRecords.forEach(x => {
+ if (!filteredEntries.find(y => this.comparator(x, y))) {
+ const change = this.remove(x).then(res => {
+ if (this.nop) {
+ return resArray.push(res)
+ }
+ return res
+ })
+ changes.push(change)
+ }
+ })
+
+ filteredEntries.forEach(attrs => {
+ const existing = existingRecords.find(record => {
+ return this.comparator(record, attrs)
+ })
+
+ if (!existing) {
+ const change = this.add(attrs).then(res => {
+ if (this.nop) {
+ return resArray.push(res)
+ }
+ return res
+ })
+ changes.push(change)
+ } else if (this.changed(existing, attrs)) {
+ const change = this.update(existing, attrs).then(res => {
+ if (this.nop) {
+ return resArray.push(res)
+ }
+ return res
+ })
+ changes.push(change)
+ }
+ })
+
+ if (this.nop) {
+ return Promise.resolve(resArray)
+ }
+ return Promise.all(changes)
+ }).catch(e => {
+ if (this.nop) {
+ if (e.status === 404) {
+ // Ignore 404s which can happen in dry-run as the repo may not exist.
+ return Promise.resolve(resArray)
+ } else {
+ resArray.push(new NopCommand(this.constructor.name, this.repo, null, `error ${e} in ${this.constructor.name} for repo: ${JSON.stringify(this.repo)} entries ${JSON.stringify(this.entries)}`, 'ERROR'))
+ return Promise.resolve(resArray)
+ }
+ } else {
+ this.logError(`Error ${e} in ${this.constructor.name} for repo: ${JSON.stringify(this.repo)} entries ${JSON.stringify(this.entries)}`)
+ }
+ })
+ }
+ }
+
+}
diff --git a/test/unit/lib/plugins/environments.test.js b/test/unit/lib/plugins/environments.test.js
index e826e005..276a82cd 100644
--- a/test/unit/lib/plugins/environments.test.js
+++ b/test/unit/lib/plugins/environments.test.js
@@ -1,274 +1,1066 @@
const { when } = require('jest-when')
const Environments = require('../../../../lib/plugins/environments')
-describe('Environments', () => {
- let github
- const org = 'bkeepers'
- const repo = 'test'
-
- function fillEnvironment(attrs) {
- if (!attrs.wait_timer) attrs.wait_timer = 0;
- if (!attrs.prevent_self_review) attrs.prevent_self_review = false;
- if (!attrs.reviewers) attrs.reviewers = [];
- if (!attrs.deployment_branch_policy) attrs.deployment_branch_policy = null;
- if(!attrs.variables) attrs.variables = [];
- if(!attrs.deployment_protection_rules) attrs.deployment_protection_rules = [];
- if(!attrs.protection_rules) attrs.protection_rules = [];
-
- return attrs;
+describe('Environments Plugin test suite', () => {
+ let github
+ let environment_name = ''
+ const org = 'bkeepers'
+ const repo = 'test'
+ const PrimaryEnvironmentNamesBeingTested = ['wait-timer_environment', 'wait-timer_2_environment', 'reviewers_environment', 'prevent-self-review_environment', 'deployment-branch-policy_environment', 'deployment-branch-policy-custom_environment', 'variables_environment', 'deployment-protection-rules_environment', 'new_environment', 'old_environment']
+ const EnvironmentNamesForTheNewEnvironmentsTest = ['new-wait-timer', 'new-reviewers', 'new-prevent-self-review', 'new-deployment-branch-policy', 'new-deployment-branch-policy-custom', 'new-variables', 'new-deployment-protection-rules']
+ const AllEnvironmentNamesBeingTested = PrimaryEnvironmentNamesBeingTested.concat(EnvironmentNamesForTheNewEnvironmentsTest)
+ const log = { debug: jest.fn(), error: console.error }
+ const errors = []
+
+ function fillEnvironment(attrs) {
+ if (!attrs.wait_timer) attrs.wait_timer = 0;
+ if (!attrs.prevent_self_review) attrs.prevent_self_review = false;
+ if (!attrs.reviewers) attrs.reviewers = [];
+ if (!attrs.deployment_branch_policy) attrs.deployment_branch_policy = null;
+ if (!attrs.variables) attrs.variables = [];
+ if (!attrs.deployment_protection_rules) attrs.deployment_protection_rules = [];
+ if (!attrs.protection_rules) attrs.protection_rules = [];
+
+ return attrs;
+ }
+
+ beforeEach(() => {
+ //arrange for all
+ github = {
+ request: jest.fn(() => Promise.resolve(true))
}
- beforeAll(() => {
- github = {
- request: jest.fn().mockReturnValue(Promise.resolve(true))
+ AllEnvironmentNamesBeingTested.forEach((environment_name) => {
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name })
+ .mockResolvedValue({
+ data: {
+ variables: []
+ }
+ })
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name })
+ .mockResolvedValue({
+ data: {
+ custom_deployment_protection_rules: []
+ }
+ })
+ }
+ );
+
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments/:environment_name/deployment-branch-policies', { org, repo, environment_name: 'deployment-branch-policy-custom_environment' })
+ .mockResolvedValue({
+ data: {
+ branch_policies: []
+ }
}
+ );
+
+ when(github.request)
+ .calledWith('DELETE /repos/:org/:repo/environments/:environment_name/deployment-branch-policies/:branch_policy_id')
+ .mockResolvedValue({});
+
+ when(github.request)
+ .calledWith('POST /repos/:org/:repo/environments/:environment_name/deployment-branch-policies')
+ .mockResolvedValue({});
+
+ when(github.request)
+ .calledWith('PUT /repos/:org/:repo/environments/:environment_name')
+ .mockResolvedValue({});
+
+ when(github.request)
+ .calledWith('POST /repos/:org/:repo/environments/:environment_name/variables')
+ .mockResolvedValue({});
+
+ when(github.request)
+ .calledWith('POST /repos/:org/:repo/environments/:environment_name/deployment_protection_rules')
+ .mockResolvedValue({});
+
+ when(github.request)
+ .calledWith('DELETE /repos/:org/:repo/environments/:environment_name/deployment_protection_rules/:rule_id')
+ .mockResolvedValue({});
+
+ })
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ // start individual tests
+
+ // wait-timer
+ describe('When the existing wait-timer is 0 and the config is set to 1', () => {
+ it('detect divergence and set wait-timer to 1', async () => {
+ //arrange
+ environment_name = 'wait-timer_environment'
+ // represent config with a wait timer of 1
+ const plugin = new Environments(undefined, github, { owner: org, repo }, [
+ {
+ name: environment_name,
+ wait_timer: 1
+ }
+ ], log, errors);
+
+ //model an existing environment with a wait timer of 0
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments', { org, repo })
+ .mockResolvedValue({
+ data: {
+ environments: [
+ fillEnvironment({
+ name: environment_name,
+ wait_timer: 0
+ })
+ ]
+ }
+ });
+
+ //act - run sync() in environments.js
+ await plugin.sync().then(() => {
+ //assert - update to the wait timer was requested with value 1
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments', { org, repo });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: environment_name,
+ wait_timer: 1
+ }));
+ })
})
+ })
- it('sync', () => {
- const plugin = new Environments(undefined, github, {owner: org, repo}, [
+ // add reviewers
+ describe('When there are no existing reviewers and config calls for a user and a team', () => {
+ it('detect divergence and set reviewers', async () => {
+ //arrange
+ environment_name = 'reviewers_environment'
+ // represent config with a reviewers being a user and a team
+ const plugin = new Environments(undefined, github, { owner: org, repo }, [
+ {
+ name: environment_name,
+ reviewers: [
{
- name: 'wait-timer',
- wait_timer: 1
- },
- {
- name: 'reviewers',
- reviewers: [
- {
+ type: 'User',
+ id: 1
+ }
+ ]
+ }
+ ], log, errors);
+
+ //model an existing environment with no reviewers
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments', { org, repo })
+ .mockResolvedValue({
+ data: {
+ environments: [
+ fillEnvironment({
+ name: environment_name,
+ protection_rules: [
+ {
+ type: 'required_reviewers',
+ reviewers: [
+ {
type: 'User',
- id: 1
- },
- {
- type: 'Team',
- id: 2
- }
+ reviewer: {
+ id: 56,
+ type: 'User'
+ }
+ }
+ ]
+ }
]
- },
- {
- name: 'prevent-self-review',
- prevent_self_review: true
- },
+ })
+ ]
+ }
+ });
+
+ //act - run sync() in environments.js
+ await plugin.sync().then(() => {
+ //assert - update the reviewers
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments', { org, repo });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: environment_name,
+ reviewers: [
{
- name: 'deployment-branch-policy',
- deployment_branch_policy: {
- protected_branches: true,
- custom_branch_policies: false
- }
- },
+ type: 'User',
+ id: 1
+ }
+ ]
+ }));
+ })
+ })
+ })
+
+ // prevent self review
+ describe('When prevent self review is false, and the config calls for it to be true', () => {
+ it('detect divergence and set prevent self review to true', async () => {
+ //arrange
+ environment_name = 'prevent-self-review_environment'
+ //
+ const plugin = new Environments(undefined, github, { owner: org, repo }, [
+ {
+ name: environment_name,
+ prevent_self_review: true
+ }
+ ], log, errors);
+
+ //model an existing environment with prevent self review false
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments', { org, repo })
+ .mockResolvedValue({
+ data: {
+ environments: [
+ fillEnvironment({
+ name: environment_name,
+ prevent_self_review: false
+ })
+ ]
+ }
+ });
+
+ //act - run sync() in environments.js
+ await plugin.sync().then(() => {
+ //assert - update the prevent self review boolean
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments', { org, repo });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: environment_name,
+ prevent_self_review: true
+ }));
+ })
+ })
+ })
+
+ // deployment branch policy
+ describe('When there is no existing deployment branch policy and the config sets a policy', () => {
+ it('detect divergence and set the deployment branch policy from the config', async () => {
+ //arrange
+ environment_name = 'deployment-branch-policy_environment'
+ // represent config with a reviewers being a user and a team
+ const plugin = new Environments(undefined, github, { owner: org, repo }, [
+ {
+ name: environment_name,
+ deployment_branch_policy: {
+ protected_branches: true,
+ custom_branch_policies: false
+ }
+ }
+ ], log, errors);
+
+ //model an existing environment with prevent self review false
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments', { org, repo })
+ .mockResolvedValue({
+ data: {
+ environments: [
+ fillEnvironment({
+ name: environment_name,
+ deployment_branch_policy: null
+ })
+ ]
+ }
+ });
+
+ //act - run sync() in environments.js
+ await plugin.sync().then(() => {
+ //assert - update branch policy
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments', { org, repo });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: environment_name,
+ deployment_branch_policy: {
+ protected_branches: true,
+ custom_branch_policies: false
+ }
+ }));
+ })
+ })
+ })
+
+ // custom deployment branch policy
+ describe('When there is no existing deployment branch policy and the config sets a custom policy', () => {
+ it('detect divergence and set the custom deployment branch policy from the config', async () => {
+ //arrange
+ environment_name = 'deployment-branch-policy-custom_environment'
+ // represent config with a custom branch policy
+ const plugin = new Environments(undefined, github, { owner: org, repo }, [
+ {
+ name: environment_name,
+ deployment_branch_policy: {
+ protected_branches: false,
+ custom_branch_policies: [
+ 'master',
+ 'dev'
+ ]
+ }
+ }
+ ], log, errors);
+
+ //model an existing environment with no branch policies
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments', { org, repo })
+ .mockResolvedValue({
+ data: {
+ environments: [
+ fillEnvironment({
+ name: environment_name,
+ deployment_branch_policy: null
+ })
+ ]
+ }
+ });
+
+ //act - run sync() in environments.js
+ await plugin.sync().then(() => {
+ //assert - update the custom branch policies
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments', { org, repo });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: environment_name,
+ deployment_branch_policy: {
+ protected_branches: false,
+ custom_branch_policies: true
+ }
+ }));
+ expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/deployment-branch-policies', expect.objectContaining({
+ org,
+ repo,
+ environment_name: environment_name,
+ name: 'master'
+ }));
+ expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/deployment-branch-policies', expect.objectContaining({
+ org,
+ repo,
+ environment_name: environment_name,
+ name: 'dev'
+ }));
+ })
+ })
+ })
+
+ // add variable
+ describe('When there are no existing variables and config calls for one', () => {
+ it('detect divergence and add the variable', async () => {
+ //arrange
+ environment_name = 'variables_environment'
+ // represent config with a reviewers being a user and a team
+ const plugin = new Environments(undefined, github, { owner: org, repo }, [
+ {
+ name: environment_name,
+ variables: [
{
- name: 'deployment-branch-policy-custom',
- deployment_branch_policy: {
- protected_branches: false,
- custom_branch_policies: [
- 'master',
- 'dev'
- ]
- }
- },
+ name: 'test',
+ value: 'test'
+ }
+ ]
+ }
+ ], log, errors);
+
+ //model an existing environment with no reviewers
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments', { org, repo })
+ .mockResolvedValue({
+ data: {
+ environments: [
+ fillEnvironment({
+ name: environment_name,
+ variables: []
+ })
+ ]
+ }
+ });
+
+ //act - run sync() in environments.js
+ await plugin.sync().then(() => {
+ //assert - update the variables
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments', { org, repo });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/variables', expect.objectContaining({
+ org,
+ repo,
+ environment_name: environment_name,
+ name: 'test',
+ value: 'test'
+ }));
+ })
+ })
+ })
+
+ // add deployment protection rules
+ describe('When there are no existing deployment protection rules, but config calls for one', () => {
+ it('detect divergence and add the deployment protection rule', async () => {
+ //arrange
+ environment_name = 'deployment-protection-rules_environment'
+ // represent config with a deployment protection rule
+ const plugin = new Environments(undefined, github, { owner: org, repo }, [
+ {
+ name: environment_name,
+ deployment_protection_rules: [
{
- name: 'variables',
- variables: [
- {
- name: 'test',
- value: 'test'
- }
+ app_id: 1
+ }
+ ]
+ }
+ ], log, errors);
+
+ //model an existing environment with no deployment protection rules
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments', { org, repo })
+ .mockResolvedValue({
+ data: {
+ environments: [
+ fillEnvironment({
+ name: environment_name,
+ deployment_protection_rules: []
+ })
+ ]
+ }
+ });
+
+ //act - run sync() in environments.js
+ await plugin.sync().then(() => {
+ //assert - update the deployment protection rules
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments', { org, repo });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', expect.objectContaining({
+ org,
+ repo,
+ environment_name: environment_name,
+ integration_id: 1 // weird that this is integration_id, but above it's app_id
+ }));
+ })
+ })
+ })
+
+ // wait-timer unchanged
+ describe('When the existing wait-timer is 2 and the config is set to 2', () => {
+ it('detect that the value is unchanged, and do nothing', async () => {
+ //arrange
+ environment_name = 'wait-timer_2_environment'
+ // represent config with a wait timer of 2
+ const plugin = new Environments(undefined, github, { owner: org, repo }, [
+ {
+ name: environment_name,
+ wait_timer: 2
+ }
+ ], log, errors);
+
+ //model an existing environment with no reviewers
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments', { org, repo })
+ .mockResolvedValue({
+ data: {
+ environments: [
+ fillEnvironment({
+ name: environment_name,
+ protection_rules: [
+ {
+ type: 'wait_timer',
+ wait_timer: 2
+ }
]
+ })
+ ]
+ }
+ });
+
+ //act - run sync() in environments.js
+ await plugin.sync().then(() => {
+ //assert - update to the wait timer was requested with value 2
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments', { org, repo });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name });
+ expect(github.request).not.toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: environment_name,
+ wait_timer: 2
+ }));
+ })
+ })
+ })
+
+ // Zero existing environments
+ describe('When there are no existing environments, and the config has one environment', () => {
+ it('detect that and environment needs to be added, and add it', async () => {
+ //arrange
+ environment_name = 'new_environment'
+ // represent a new environment
+ const plugin = new Environments(undefined, github, { owner: org, repo }, [
+ {
+ name: environment_name,
+ }
+ ], log, errors);
+
+ //model an existing state which has zero environments
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments', { org, repo })
+ .mockResolvedValue({
+ data: {
+ environments: [
+
+ ]
+ }
+ });
+
+ //act - run sync() in environments.js
+ await plugin.sync().then(() => {
+ //assert - the new environment was added
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments', { org, repo });
+ expect(github.request).not.toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name });
+ expect(github.request).not.toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: environment_name
+ }));
+ })
+ })
+ })
+
+ // Single environment name change
+ describe('When there is one existing environment with an old name, and the config has one environment with a new name', () => {
+ it('detect that an environment name has changed, add the new one, and delete the old one', async () => {
+ //arrange
+ environment_name = 'new_environment'
+ const old_environment_name = 'old_environment'
+ // represent a new environment
+ const plugin = new Environments(undefined, github, { owner: org, repo }, [
+ {
+ name: environment_name,
+ }
+ ], log, errors);
+
+ //model an existing environment with an old name
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments', { org, repo })
+ .mockResolvedValue({
+ data: {
+ environments: [
+ fillEnvironment({
+ name: old_environment_name
+ })
+ ]
+ }
+ });
+
+ //act - run sync() in environments.js
+ await plugin.sync().then(() => {
+ //assert - the new environment was added
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments', { org, repo });
+ expect(github.request).not.toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name });
+ expect(github.request).not.toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: environment_name
+ }));
+
+ //assert - the old environment was deleted
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments', { org, repo });
+ expect(github.request).not.toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, old_environment_name });
+ expect(github.request).not.toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, old_environment_name });
+ expect(github.request).toHaveBeenCalledWith('DELETE /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: old_environment_name
+ }));
+
+ })
+ })
+ })
+
+ // original 7 changes all combined together test
+ describe('When there are changes across 7 environments', () => {
+ it('detect and apply all changes', async () => {
+ //arrange
+ // represent 7 environments and their desired settings
+ const plugin = new Environments(undefined, github, { owner: org, repo }, [
+ {
+ name: 'wait-timer_environment',
+ wait_timer: 1
+ },
+ {
+ name: 'reviewers_environment',
+ reviewers: [
+ {
+ type: 'User',
+ id: 1
},
{
- name: 'deployment-protection-rules',
- deployment_protection_rules: [
- {
- app_id: 1
- }
- ]
+ type: 'Team',
+ id: 2
}
- ], {
- debug: function() {}
- });
+ ]
+ },
+ {
+ name: 'prevent-self-review_environment',
+ prevent_self_review: true
+ },
+ {
+ name: 'deployment-branch-policy_environment',
+ deployment_branch_policy: {
+ protected_branches: true,
+ custom_branch_policies: false
+ }
+ },
+ {
+ name: 'deployment-branch-policy-custom_environment',
+ deployment_branch_policy: {
+ protected_branches: false,
+ custom_branch_policies: [
+ 'master',
+ 'dev'
+ ]
+ }
+ },
+ {
+ name: 'variables_environment',
+ variables: [
+ {
+ name: 'test',
+ value: 'test'
+ }
+ ]
+ },
+ {
+ name: 'deployment-protection-rules_environment',
+ deployment_protection_rules: [
+ {
+ app_id: 1
+ }
+ ]
+ }
+ ], log, errors);
- when(github.request)
- .calledWith('GET /repos/:org/:repo/environments', { org, repo })
- .mockResolvedValue({
- data: {
- environments: [
- fillEnvironment({
- name: 'wait-timer',
- wait_timer: 0
- }),
- fillEnvironment({
- name: 'reviewers',
- reviewers: []
- }),
- fillEnvironment({
- name: 'prevent-self-review',
- prevent_self_review: false
- }),
- fillEnvironment({
- name: 'deployment-branch-policy',
- deployment_branch_policy: null
- }),
- fillEnvironment({
- name: 'deployment-branch-policy-custom',
- deployment_branch_policy: null
- }),
- fillEnvironment({
- name: 'variables',
- variables: []
- }),
- fillEnvironment({
- name: 'deployment-protection-rules',
- deployment_protection_rules: []
- })
- ]
- }
- });
-
- ['wait-timer', 'reviewers', 'prevent-self-review', 'deployment-branch-policy', 'deployment-branch-policy-custom', 'variables', 'deployment-protection-rules'].forEach((environment_name) => {
- when(github.request)
- .calledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name })
- .mockResolvedValue({
- data: {
- variables: []
- }
- })
+ // model 7 existing environments and their settings
+ // note: wait-timer, required_reviewers, and branch_policy are modeled incorrectly here as they are not wrapped by protection_rules[]
+ // the test succeeds anyway because it so happens that the defaults assigned for missing values, coincidentally match the values below
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments', { org, repo })
+ .mockResolvedValue({
+ data: {
+ environments: [
+ fillEnvironment({
+ name: 'wait-timer_environment',
+ wait_timer: 0
+ }),
+ fillEnvironment({
+ name: 'reviewers_environment',
+ reviewers: []
+ }),
+ fillEnvironment({
+ name: 'prevent-self-review_environment',
+ prevent_self_review: false
+ }),
+ fillEnvironment({
+ name: 'deployment-branch-policy_environment',
+ deployment_branch_policy: null
+ }),
+ fillEnvironment({
+ name: 'deployment-branch-policy-custom_environment',
+ deployment_branch_policy: null
+ }),
+ fillEnvironment({
+ name: 'variables_environment',
+ variables: []
+ }),
+ fillEnvironment({
+ name: 'deployment-protection-rules_environment',
+ deployment_protection_rules: []
+ })
+ ]
+ }
});
- ['wait-timer', 'reviewers', 'prevent-self-review', 'deployment-branch-policy', 'deployment-branch-policy-custom', 'variables', 'deployment-protection-rules'].forEach((environment_name) => {
- when(github.request)
- .calledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name })
- .mockResolvedValue({
- data: {
- custom_deployment_protection_rules: []
- }
- })
- });
+ //act - run sync() in environments.js
+ await plugin.sync().then(() => {
+ //assert - update to the wait timer was requested with value 1, etc.
- when(github.request)
- .calledWith('GET /repos/:org/:repo/environments/:environment_name/deployment-branch-policies', { org, repo, environment_name: 'deployment-branch-policy-custom' })
- .mockResolvedValue({
- data: {
- branch_policies: []
- }
- });
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments', { org, repo });
- when(github.request)
- .calledWith('DELETE /repos/:org/:repo/environments/:environment_name/deployment-branch-policies/:branch_policy_id')
- .mockResolvedValue({});
+ ['wait-timer_environment', 'reviewers_environment', 'prevent-self-review_environment', 'deployment-branch-policy_environment', 'deployment-branch-policy-custom_environment', 'variables_environment', 'deployment-protection-rules_environment'].forEach((environment_name) => {
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name });
- when(github.request)
- .calledWith('POST /repos/:org/:repo/environments/:environment_name/deployment-branch-policies')
- .mockResolvedValue({});
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name });
+ });
- when(github.request)
- .calledWith('PUT /repos/:org/:repo/environments/:environment_name')
- .mockResolvedValue({});
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'wait-timer_environment',
+ wait_timer: 1
+ }));
- when(github.request)
- .calledWith('POST /repos/:org/:repo/environments/:environment_name/variables')
- .mockResolvedValue({});
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'reviewers_environment',
+ reviewers: [
+ {
+ type: 'User',
+ id: 1
+ },
+ {
+ type: 'Team',
+ id: 2
+ }
+ ]
+ }));
- when(github.request)
- .calledWith('POST /repos/:org/:repo/environments/:environment_name/deployment_protection_rules')
- .mockResolvedValue({});
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'prevent-self-review_environment',
+ prevent_self_review: true
+ }));
- when(github.request)
- .calledWith('DELETE /repos/:org/:repo/environments/:environment_name/deployment_protection_rules/:rule_id')
- .mockResolvedValue({});
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'prevent-self-review_environment',
+ prevent_self_review: true
+ }));
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'deployment-branch-policy_environment',
+ deployment_branch_policy: {
+ protected_branches: true,
+ custom_branch_policies: false
+ }
+ }));
- return plugin.sync().then(() => {
- expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments', { org, repo });
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'deployment-branch-policy-custom_environment',
+ deployment_branch_policy: {
+ protected_branches: false,
+ custom_branch_policies: true
+ }
+ }));
- ['wait-timer', 'reviewers', 'prevent-self-review', 'deployment-branch-policy', 'deployment-branch-policy-custom', 'variables', 'deployment-protection-rules'].forEach((environment_name) => {
- expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name });
+ expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/deployment-branch-policies', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'deployment-branch-policy-custom_environment',
+ name: 'master'
+ }));
- expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name });
- });
+ expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/deployment-branch-policies', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'deployment-branch-policy-custom_environment',
+ name: 'dev'
+ }));
- expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
- org,
- repo,
- environment_name: 'wait-timer',
- wait_timer: 1
- }));
+ expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/variables', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'variables_environment',
+ name: 'test',
+ value: 'test'
+ }));
- expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
- org,
- repo,
- environment_name: 'reviewers',
- reviewers: [
- {
- type: 'User',
- id: 1
- },
- {
- type: 'Team',
- id: 2
- }
+ expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'deployment-protection-rules_environment',
+ integration_id: 1
+ }));
+ })
+ })
+ })
+
+ // Add 7 new environments, each with one environment attribute set
+ describe('When there are 7 existing environments and 7 new environments each with one environment attribute in the config', () => {
+ it('make changes in the existing environments and also add the 7 new environments', async () => {
+ //arrange
+ // represent 14 environments (7 new) and their desired settings
+ const plugin = new Environments(undefined, github, { owner: org, repo }, [
+ {
+ name: 'wait-timer_environment',
+ wait_timer: 1
+ },
+ {
+ name: 'reviewers_environment',
+ reviewers: [
+ {
+ type: 'User',
+ id: 1
+ },
+ {
+ type: 'Team',
+ id: 2
+ }
+ ]
+ },
+ {
+ name: 'prevent-self-review_environment',
+ prevent_self_review: true
+ },
+ {
+ name: 'deployment-branch-policy_environment',
+ deployment_branch_policy: {
+ protected_branches: true,
+ custom_branch_policies: false
+ }
+ },
+ {
+ name: 'deployment-branch-policy-custom_environment',
+ deployment_branch_policy: {
+ protected_branches: false,
+ custom_branch_policies: [
+ 'master',
+ 'dev'
+ ]
+ }
+ },
+ {
+ name: 'variables_environment',
+ variables: [
+ {
+ name: 'test',
+ value: 'test'
+ }
+ ]
+ },
+ {
+ name: 'deployment-protection-rules_environment',
+ deployment_protection_rules: [
+ {
+ app_id: 1
+ }
+ ]
+ },
+ {
+ name: 'new-wait-timer',
+ wait_timer: 1
+ },
+ {
+ name: 'new-reviewers',
+ reviewers: [
+ {
+ type: 'User',
+ id: 1
+ },
+ {
+ type: 'Team',
+ id: 2
+ }
+ ]
+ },
+ {
+ name: 'new-prevent-self-review',
+ prevent_self_review: true
+ },
+ {
+ name: 'new-deployment-branch-policy',
+ deployment_branch_policy: {
+ protected_branches: true,
+ custom_branch_policies: false
+ }
+ },
+ {
+ name: 'new-deployment-branch-policy-custom',
+ deployment_branch_policy: {
+ protected_branches: false,
+ custom_branch_policies: [
+ 'master',
+ 'dev'
]
- }));
-
- expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
- org,
- repo,
- environment_name: 'prevent-self-review',
- prevent_self_review: true
- }));
-
- expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
- org,
- repo,
- environment_name: 'prevent-self-review',
- prevent_self_review: true
- }));
-
- expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
- org,
- repo,
- environment_name: 'deployment-branch-policy',
- deployment_branch_policy: {
- protected_branches: true,
- custom_branch_policies: false
+ }
+ },
+ {
+ name: 'new-variables',
+ variables: [
+ {
+ name: 'test',
+ value: 'test'
}
- }));
-
- expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
- org,
- repo,
- environment_name: 'deployment-branch-policy-custom',
- deployment_branch_policy: {
- protected_branches: false,
- custom_branch_policies: true
+ ]
+ },
+ {
+ name: 'new-deployment-protection-rules',
+ deployment_protection_rules: [
+ {
+ app_id: 1
}
- }));
-
- expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/deployment-branch-policies', expect.objectContaining({
- org,
- repo,
- environment_name: 'deployment-branch-policy-custom',
- name: 'master'
- }));
-
- expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/deployment-branch-policies', expect.objectContaining({
- org,
- repo,
- environment_name: 'deployment-branch-policy-custom',
- name: 'dev'
- }));
-
- expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/variables', expect.objectContaining({
- org,
- repo,
- environment_name: 'variables',
- name: 'test',
- value: 'test'
- }));
-
- expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', expect.objectContaining({
- org,
- repo,
- environment_name: 'deployment-protection-rules',
- integration_id: 1
- }));
- })
+ ]
+ }
+ ], log, errors);
+
+ // model 7 existing environments and their settings
+ // note: wait-timer, required_reviewers, and branch_policy are modeled incorrectly here as they are not wrapped by protection_rules[]
+ // the test succeeds anyway because it so happens that the defaults assigned for missing values, coincidentally match the values below
+ when(github.request)
+ .calledWith('GET /repos/:org/:repo/environments', { org, repo })
+ .mockResolvedValue({
+ data: {
+ environments: [
+ fillEnvironment({
+ name: 'wait-timer_environment',
+ wait_timer: 0
+ }),
+ fillEnvironment({
+ name: 'reviewers_environment',
+ reviewers: []
+ }),
+ fillEnvironment({
+ name: 'prevent-self-review_environment',
+ prevent_self_review: false
+ }),
+ fillEnvironment({
+ name: 'deployment-branch-policy_environment',
+ deployment_branch_policy: null
+ }),
+ fillEnvironment({
+ name: 'deployment-branch-policy-custom_environment',
+ deployment_branch_policy: null
+ }),
+ fillEnvironment({
+ name: 'variables_environment',
+ variables: []
+ }),
+ fillEnvironment({
+ name: 'deployment-protection-rules_environment',
+ deployment_protection_rules: []
+ })
+ ]
+ }
+ });
+
+ //act - run sync() in environments.js
+ await plugin.sync().then(() => {
+ //assert - update to the wait timer was requested with value 1, etc.
+
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments', { org, repo });
+
+ ['wait-timer_environment', 'reviewers_environment', 'prevent-self-review_environment', 'deployment-branch-policy_environment', 'deployment-branch-policy-custom_environment', 'variables_environment', 'deployment-protection-rules_environment'].forEach((environment_name) => {
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, environment_name });
+
+ expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, environment_name });
+ });
+
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'wait-timer_environment',
+ wait_timer: 1
+ }));
+
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'reviewers_environment',
+ reviewers: [
+ {
+ type: 'User',
+ id: 1
+ },
+ {
+ type: 'Team',
+ id: 2
+ }
+ ]
+ }));
+
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'prevent-self-review_environment',
+ prevent_self_review: true
+ }));
+
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'prevent-self-review_environment',
+ prevent_self_review: true
+ }));
+
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'deployment-branch-policy_environment',
+ deployment_branch_policy: {
+ protected_branches: true,
+ custom_branch_policies: false
+ }
+ }));
+
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'deployment-branch-policy-custom_environment',
+ deployment_branch_policy: {
+ protected_branches: false,
+ custom_branch_policies: true
+ }
+ }));
+
+ expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/deployment-branch-policies', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'deployment-branch-policy-custom_environment',
+ name: 'master'
+ }));
+
+ expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/deployment-branch-policies', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'deployment-branch-policy-custom_environment',
+ name: 'dev'
+ }));
+
+ expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/variables', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'variables_environment',
+ name: 'test',
+ value: 'test'
+ }));
+
+ expect(github.request).toHaveBeenCalledWith('POST /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', expect.objectContaining({
+ org,
+ repo,
+ environment_name: 'deployment-protection-rules_environment',
+ integration_id: 1
+ }));
+
+ //assert - seven new environments were also added
+ EnvironmentNamesForTheNewEnvironmentsTest.forEach(new_environment_name => {
+ expect(github.request).not.toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/variables', { org, repo, new_environment_name });
+ expect(github.request).not.toHaveBeenCalledWith('GET /repos/:org/:repo/environments/:environment_name/deployment_protection_rules', { org, repo, new_environment_name });
+ expect(github.request).toHaveBeenCalledWith('PUT /repos/:org/:repo/environments/:environment_name', expect.objectContaining({
+ org,
+ repo,
+ environment_name: new_environment_name
+ }));
+ });
+
+ })
})
-})
\ No newline at end of file
+ })
+
+})
From 776b31d6304d9441c1863dd5e8c30039fb2f88d0 Mon Sep 17 00:00:00 2001
From: Luv Saxena
Date: Tue, 20 Aug 2024 09:12:14 -0500
Subject: [PATCH 2/6] fix: add or update fields in the existing environment or
rulesets (#665)
* feat: initial commit
* fix: merge deep code
* fix: lint
* fix: modification conditions
* fix: simplify conditions
---------
Co-authored-by: ls07667
---
lib/mergeDeep.js | 38 +-
test/unit/lib/mergeDeep.test.js | 1060 ++++++++++++++++++++-----------
2 files changed, 707 insertions(+), 391 deletions(-)
diff --git a/lib/mergeDeep.js b/lib/mergeDeep.js
index d47fd5ce..a685a048 100644
--- a/lib/mergeDeep.js
+++ b/lib/mergeDeep.js
@@ -211,18 +211,19 @@ class MergeDeep {
}
}
const combined = []
- for (const fields of Object.keys(visited)) {
- combined.push(visited[fields])
- }
- // Elements that are not in target are additions
- additions[key] = combined.filter(item => {
- if (this.isObjectNotArray(item)) {
- return !target.some(targetItem => GET_NAME_USERNAME_PROPERTY(item) === GET_NAME_USERNAME_PROPERTY(targetItem))
- } else {
- return !target.includes(item)
+ if (Object.keys(visited).length !== 0) {
+ for (const fields of Object.keys(visited)) {
+ combined.push(visited[fields])
}
- })
-
+ // Elements that are not in target are additions
+ additions[key] = combined.filter(item => {
+ if (this.isObjectNotArray(item)) {
+ return !target.some(targetItem => GET_NAME_USERNAME_PROPERTY(item) === GET_NAME_USERNAME_PROPERTY(targetItem))
+ } else {
+ return !target.includes(item)
+ }
+ })
+ }
// Elements that not in source are deletions
if (combined.length > 0) {
// Elements that not in source are deletions
@@ -247,8 +248,21 @@ class MergeDeep {
this.compareDeep(a, visited[id], additions[additions.length - 1], modifications[modifications.length - 1], deletions[deletions.length - 1])
}
// Any addtions for the matching key must be moved to modifications
+ const lastAddition = additions[additions.length - 1]
+ const lastModification = modifications[modifications.length - 1]
+
if (!this.isEmpty(additions)) {
- modifications = modifications.concat(additions)
+ for (const key in lastAddition) {
+ if (!lastModification[key]) {
+ lastModification[key] = Array.isArray(lastAddition[key]) ? [] : {}
+ }
+ if (!Array.isArray(lastAddition[key])) {
+ Object.assign(lastModification[key], lastAddition[key])
+ } else {
+ lastModification[key].push(...lastAddition[key])
+ }
+ }
+ additions.length = 0
}
// Add name attribute to the modifications to make it look better ; it won't be added otherwise as it would be the same
if (!this.isEmpty(modifications[modifications.length - 1])) {
diff --git a/test/unit/lib/mergeDeep.test.js b/test/unit/lib/mergeDeep.test.js
index 35d6aeed..c114377e 100644
--- a/test/unit/lib/mergeDeep.test.js
+++ b/test/unit/lib/mergeDeep.test.js
@@ -2,7 +2,7 @@
const MergeDeep = require('../../../lib/mergeDeep')
const YAML = require('js-yaml')
const log = require('pino')('test.log')
-
+
describe('MergeDeep Test', () => {
it('CompareDeep extensive test', () => {
const target = YAML.load(`
@@ -116,38 +116,38 @@ branches:
const expected = {
additions: {
repository: {
- name: "test",
- org: "decyjphr-org",
- homepage: "https://newhome.github.io/",
+ name: 'test',
+ org: 'decyjphr-org',
+ homepage: 'https://newhome.github.io/',
topics: [
- "red"
+ 'red'
],
auto_init: true,
has_issues: true,
has_projects: true,
has_downloads: true,
allow_squash_merge: true,
- default_branch: "develop"
+ default_branch: 'develop'
},
labels: [
- {
- name: "green",
- color: "#B60205",
- description: "An issue sswithss the system"
- }
+ {
+ name: 'green',
+ color: '#B60205',
+ description: 'An issue sswithss the system'
+ }
],
validator: {
- pattern: "[a-zA-Z0-9_-]+"
+ pattern: '[a-zA-Z0-9_-]+'
},
collaborators: [
- {
- username: "regpaco",
- permission: "pull"
- }
+ {
+ username: 'regpaco',
+ permission: 'pull'
+ }
],
branches: [
{
- name: "feature1",
+ name: 'feature1',
protection: {
required_pull_request_reviews: {
required_approving_review_count: 5,
@@ -174,8 +174,8 @@ branches:
},
modifications: {
repository: {
- description: "description of test repository",
- name: "test"
+ description: 'description of test repository',
+ name: 'test'
}
},
hasChanges: true
@@ -183,13 +183,13 @@ branches:
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const merged = mergeDeep.compareDeep(target, source)
console.log(`source ${JSON.stringify(source, null, 2)}`)
console.log(`target ${JSON.stringify(target, null, 2)}`)
@@ -221,9 +221,9 @@ branches:
contexts: []
enforce_admins: false
`)
-
+
const expected = {
- additions:
+ additions:
{
protection: {
required_pull_request_reviews: {
@@ -237,26 +237,25 @@ branches:
contexts: []
},
enforce_admins: false
- }
}
- ,
- modifications: {},
- hasChanges: true
- }
-
- const ignorableFields = []
- const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
- const mergeDeep = new MergeDeep(
- log,
- mockReturnGitHubContext,
- ignorableFields
- );
- const merged = mergeDeep.compareDeep(undefined, source)
- console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
- expect(merged.additions).toEqual(expected.additions)
- expect(merged.modifications.length).toEqual(expected.modifications.length)
+ },
+ modifications: {},
+ hasChanges: true
+ }
+
+ const ignorableFields = []
+ const mockReturnGitHubContext = jest.fn().mockReturnValue({
+ request: () => {}
+ })
+ const mergeDeep = new MergeDeep(
+ log,
+ mockReturnGitHubContext,
+ ignorableFields
+ )
+ const merged = mergeDeep.compareDeep(undefined, source)
+ console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
+ expect(merged.additions).toEqual(expected.additions)
+ expect(merged.modifications.length).toEqual(expected.modifications.length)
})
it('CompareDeep Empty target Works', () => {
@@ -272,45 +271,45 @@ branches:
contexts: []
enforce_admins: false
`)
-
+
const expected = {
additions:
{
- protection:{
- required_pull_request_reviews:{
- required_approving_review_count:2,
- dismiss_stale_reviews:false,
- require_code_owner_reviews:true,
- dismissal_restrictions:{}},
- required_status_checks:{
- strict:true,
- contexts:[]
- },
- enforce_admins:false
- }
+ protection: {
+ required_pull_request_reviews: {
+ required_approving_review_count: 2,
+ dismiss_stale_reviews: false,
+ require_code_owner_reviews: true,
+ dismissal_restrictions: {}
+ },
+ required_status_checks: {
+ strict: true,
+ contexts: []
+ },
+ enforce_admins: false
}
- ,
- modifications:{},
- hasChanges: true
- }
- const ignorableFields = []
- const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
- const mergeDeep = new MergeDeep(
- log,
- mockReturnGitHubContext,
- ignorableFields
- );
- const merged = mergeDeep.compareDeep({}, source)
- console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
- expect(merged.additions).toEqual(expected.additions)
- expect(merged.modifications.length).toEqual(expected.modifications.length)
-
- const overrideConfig = mergeDeep.mergeDeep({}, {}, source)
- const same = mergeDeep.compareDeep(overrideConfig, source)
- expect(same.additions).toEqual({})
- expect(same.modifications).toEqual({})
+ },
+ modifications: {},
+ hasChanges: true
+ }
+ const ignorableFields = []
+ const mockReturnGitHubContext = jest.fn().mockReturnValue({
+ request: () => {}
+ })
+ const mergeDeep = new MergeDeep(
+ log,
+ mockReturnGitHubContext,
+ ignorableFields
+ )
+ const merged = mergeDeep.compareDeep({}, source)
+ console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
+ expect(merged.additions).toEqual(expected.additions)
+ expect(merged.modifications.length).toEqual(expected.modifications.length)
+
+ const overrideConfig = mergeDeep.mergeDeep({}, {}, source)
+ const same = mergeDeep.compareDeep(overrideConfig, source)
+ expect(same.additions).toEqual({})
+ expect(same.modifications).toEqual({})
})
it('CompareDeep Test when target is from the api', () => {
@@ -378,7 +377,7 @@ branches:
branches: [
{
name: 'master',
- protection: protection
+ protection
}
]
}
@@ -408,7 +407,7 @@ branches:
dismiss_stale_reviews: false
}
},
- name: "master"
+ name: 'master'
}
]
},
@@ -417,13 +416,13 @@ branches:
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const merged = mergeDeep.compareDeep(target, source)
console.log(`source ${JSON.stringify(source, null, 2)}`)
console.log(`target ${JSON.stringify(target, null, 2)}`)
@@ -438,9 +437,9 @@ branches:
})
it('Merge labels with ignorable extra info Works', () => {
- const source = { entries: [{"id":3954990840,"node_id":"LA_kwDOHC6_Gc7rvF74","url":"https://api.github.com/repos/decyjphr-org/test2/labels/bug","name":"bug","color":"CC0000","default":true,"description":"An issue with the system"},{"id":4015763857,"node_id":"LA_kwDOHC6_Gc7vW7GR","url":"https://api.github.com/repos/decyjphr-org/test2/labels/feature","name":"feature","color":"336699","default":false,"description":"New functionality."},{"id":4015763934,"node_id":"LA_kwDOHC6_Gc7vW7He","url":"https://api.github.com/repos/decyjphr-org/test2/labels/first-timers-only","name":"first-timers-only","color":"326699","default":false,"description":null},{"id":4015763984,"node_id":"LA_kwDOHC6_Gc7vW7IQ","url":"https://api.github.com/repos/decyjphr-org/test2/labels/new-label","name":"new-label","color":"326699","default":false,"description":null}]}
- const target = { entries: [{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"326699"},{"name":"new-label","oldname":"Help Wanted","color":"326699"}]}
-
+ const source = { entries: [{ id: 3954990840, node_id: 'LA_kwDOHC6_Gc7rvF74', url: 'https://api.github.com/repos/decyjphr-org/test2/labels/bug', name: 'bug', color: 'CC0000', default: true, description: 'An issue with the system' }, { id: 4015763857, node_id: 'LA_kwDOHC6_Gc7vW7GR', url: 'https://api.github.com/repos/decyjphr-org/test2/labels/feature', name: 'feature', color: '336699', default: false, description: 'New functionality.' }, { id: 4015763934, node_id: 'LA_kwDOHC6_Gc7vW7He', url: 'https://api.github.com/repos/decyjphr-org/test2/labels/first-timers-only', name: 'first-timers-only', color: '326699', default: false, description: null }, { id: 4015763984, node_id: 'LA_kwDOHC6_Gc7vW7IQ', url: 'https://api.github.com/repos/decyjphr-org/test2/labels/new-label', name: 'new-label', color: '326699', default: false, description: null }] }
+ const target = { entries: [{ name: 'bug', color: 'CC0000', description: 'An issue with the system' }, { name: 'feature', color: '336699', description: 'New functionality.' }, { name: 'first-timers-only', oldname: 'Help Wanted', color: '326699' }, { name: 'new-label', oldname: 'Help Wanted', color: '326699' }] }
+
const expected = {
additions: {},
modifications: {}
@@ -448,28 +447,27 @@ branches:
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const merged = mergeDeep.compareDeep(target, source)
// console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
expect(merged.additions).toEqual(expected.additions)
expect(merged.modifications.length).toEqual(expected.modifications.length)
-
const overrideConfig = mergeDeep.mergeDeep({}, target, source)
// console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`)
const same = mergeDeep.compareDeep(overrideConfig, target)
// console.log(`new diffs ${JSON.stringify(same, null, 2)}`)
expect(same.additions).toEqual({})
expect(same.modifications).toEqual({})
- })
+ })
- it('Merge labels ', () => {
+ it('Merge labels ', () => {
const source = YAML.load(`
repository:
name: new
@@ -487,68 +485,66 @@ branches:
home: old home
`)
const expected = {
- additions: {
- labels: [
- {
- name: "green",
- color: "#B60205",
- description: "An issue sswithss the system"
- }
- ]
- },
- modifications: {},
- hasChanges: true
+ additions: {
+ labels: [
+ {
+ name: 'green',
+ color: '#B60205',
+ description: 'An issue sswithss the system'
+ }
+ ]
+ },
+ modifications: {},
+ hasChanges: true
}
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const merged = mergeDeep.compareDeep(target, source)
// console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
expect(merged.additions).toEqual(expected.additions)
expect(merged.modifications.length).toEqual(expected.modifications.length)
-
const overrideConfig = mergeDeep.mergeDeep({}, target, source)
// console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`)
const same = mergeDeep.compareDeep(overrideConfig, source)
// console.log(`new diffs ${JSON.stringify(same, null, 2)}`)
expect(same.additions).toEqual({})
expect(same.modifications).toEqual({})
- })
+ })
it('Compare nested arrays deep', () => {
- const source = {colors: [{name: "blue", color: "green"},{name: "newone",color: "red"},{ name: "uber",color: "yellow"}]}
- const target = {colors: [{name: "blue", color: "blue", anotherArray:[1,2,3]}, {name: "blu", color: "blu"}]}
-
+ const source = { colors: [{ name: 'blue', color: 'green' }, { name: 'newone', color: 'red' }, { name: 'uber', color: 'yellow' }] }
+ const target = { colors: [{ name: 'blue', color: 'blue', anotherArray: [1, 2, 3] }, { name: 'blu', color: 'blu' }] }
+
// Note: properties in the target and not in source won't show up as deletions. This is by design.
const expected = {
- additions:{colors: [{name:"newone",color:"red"},{name:"uber",color:"yellow"}]},
- modifications: {colors: [{color:"green",name:"blue"}]},
- hasChanges:true
+ additions: { colors: [{ name: 'newone', color: 'red' }, { name: 'uber', color: 'yellow' }] },
+ modifications: { colors: [{ color: 'green', name: 'blue' }] },
+ hasChanges: true
}
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const merged = mergeDeep.compareDeep(target, source)
console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
expect(merged.additions).toEqual(expected.additions)
expect(merged.modifications.length).toEqual(expected.modifications.length)
-
const overrideConfig = mergeDeep.mergeDeep({}, target, source)
console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`)
const same = mergeDeep.compareDeep(overrideConfig, source)
@@ -558,26 +554,25 @@ branches:
})
it('Merge array of topics', () => {
- const source = { entries: ["blue","green","newone","red","uber","yellow"]}
- const target = { entries: ["red","blu"]}
-
+ const source = { entries: ['blue', 'green', 'newone', 'red', 'uber', 'yellow'] }
+ const target = { entries: ['red', 'blu'] }
const expected = {
additions: {
- entries:["blue","green","newone","uber","yellow"]
+ entries: ['blue', 'green', 'newone', 'uber', 'yellow']
},
- modifications:{}
+ modifications: {}
}
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const merged = mergeDeep.compareDeep(target, source)
console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
expect(merged.additions).toEqual(expected.additions)
@@ -609,7 +604,7 @@ branches:
block_creations: true
required_conversation_resolution: true
`)
-
+
const target = YAML.load(`
branches:
- name: default
@@ -619,53 +614,53 @@ branches:
contexts:
- "Lint, compile and build"
`)
-
+
const expected = {
- additions:{},
+ additions: {},
modifications: {
branches: [{
+ name: 'default',
protection: {
required_pull_request_reviews: {
- required_approving_review_count:1,
- dismiss_stale_reviews:true,
- require_code_owner_reviews:true
+ required_approving_review_count: 1,
+ dismiss_stale_reviews: true,
+ require_code_owner_reviews: true
},
- required_status_checks:{
- contexts:[
- "🔃 pre-commit"
+ required_status_checks: {
+ contexts: [
+ '🔃 pre-commit'
]
},
- enforce_admins:true,
- block_creations:true,
- required_conversation_resolution:true
+ enforce_admins: true,
+ block_creations: true,
+ required_conversation_resolution: true
}
}]
},
- hasChanges:true
+ hasChanges: true
}
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const merged = mergeDeep.compareDeep(target, source)
// console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
expect(merged.additions).toEqual(expected.additions)
expect(merged.modifications.length).toEqual(expected.modifications.length)
-
-
+ expect(merged.modifications).toEqual(expected.modifications)
const overrideConfig = mergeDeep.mergeDeep({}, target, source)
// console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`)
const same = mergeDeep.compareDeep(overrideConfig, target)
// console.log(`new diffs ${JSON.stringify(same, null, 2)}`)
expect(same.additions).toEqual({})
expect(same.modifications).toEqual({})
- })
+ })
it('Simple compare', () => {
const source = YAML.load(`
@@ -677,30 +672,29 @@ branches:
values:
a: [b]
`)
-
+
const target = YAML.load(`
x:
- name: default
values:
a: [c,a]
`)
-
- const expected = JSON.parse(`{"additions":{"x":[{"name":"new","values":{"a":["b"]}}]},"modifications":{"x":[{"values":{"a":["b"]},"name":"default"}]},"hasChanges":true}`)
+
+ const expected = JSON.parse('{"additions":{"x":[{"name":"new","values":{"a":["b"]}}]},"modifications":{"x":[{"values":{"a":["b"]},"name":"default"}]},"hasChanges":true}')
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const merged = mergeDeep.compareDeep(target, source)
// console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
expect(merged.additions).toEqual(expected.additions)
expect(merged.modifications.length).toEqual(expected.modifications.length)
-
const overrideConfig = mergeDeep.mergeDeep({}, target, source)
// console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`)
const same = mergeDeep.compareDeep(overrideConfig, target)
@@ -708,9 +702,8 @@ branches:
expect(same.additions).toEqual({})
expect(same.modifications).toEqual({})
})
-
- it('Repo test', () => {
+ it('Repo test', () => {
const source = YAML.load(`
repository:
name: test
@@ -741,28 +734,28 @@ branches:
- uber
- newone
`)
-
+
const expected = {
additions: {
repository: {
- name: "test",
- org: "decyjphr-org",
- homepage: "https://newhome.github.io/",
+ name: 'test',
+ org: 'decyjphr-org',
+ homepage: 'https://newhome.github.io/',
topics: [
- "red"
+ 'red'
],
auto_init: true,
has_issues: true,
has_projects: true,
has_downloads: true,
allow_squash_merge: true,
- default_branch: "develop"
+ default_branch: 'develop'
}
},
modifications: {
repository: {
- description: "description of test repository",
- name: "test"
+ description: 'description of test repository',
+ name: 'test'
}
},
hasChanges: true
@@ -770,30 +763,29 @@ branches:
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const merged = mergeDeep.compareDeep(target, source)
- //console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
+ // console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
expect(merged.additions).toEqual(expected.additions)
expect(merged.modifications.length).toEqual(expected.modifications.length)
- //console.log(`target = ${JSON.stringify(target, null, 2)}`)
+ // console.log(`target = ${JSON.stringify(target, null, 2)}`)
const overrideConfig = mergeDeep.mergeDeep({}, target, source)
- //console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`)
+ // console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`)
const same = mergeDeep.compareDeep(overrideConfig, source)
- //console.log(`new diffs ${JSON.stringify(same, null, 2)}`)
+ // console.log(`new diffs ${JSON.stringify(same, null, 2)}`)
expect(same.additions).toEqual({})
expect(same.modifications).toEqual({})
-
})
-
+
it('Autolinks test', () => {
const source = YAML.load(`
entries:
@@ -805,14 +797,14 @@ entries:
const target = []
const expected = {
additions: {
- entries: [
+ entries: [
{
- key_prefix: "ASDF-",
- url_template: "https://jira.company.com/browse/ASDF-"
+ key_prefix: 'ASDF-',
+ url_template: 'https://jira.company.com/browse/ASDF-'
},
{
- key_prefix: "BOLIGRAFO-",
- url_template: "https://jira.company.com/browse/BOLIGRAFO-"
+ key_prefix: 'BOLIGRAFO-',
+ url_template: 'https://jira.company.com/browse/BOLIGRAFO-'
}
]
},
@@ -821,8 +813,8 @@ entries:
}
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
@@ -850,32 +842,32 @@ entries:
`)
const expected = {
additions: {
- entries: [
+ entries: [
{
- key_prefix: "BOLIGRAFO-",
- url_template: "https://jira.company.com/browse/BOLIGRAFO-"
+ key_prefix: 'BOLIGRAFO-',
+ url_template: 'https://jira.company.com/browse/BOLIGRAFO-'
}
]
},
modifications: {
},
deletions: {
- entries: [ {
- key_prefix: "BOLSIGRAFO-",
- url_template: "https://jira.company.com/browse/BOLIGRAFO-"
- },]
+ entries: [{
+ key_prefix: 'BOLSIGRAFO-',
+ url_template: 'https://jira.company.com/browse/BOLIGRAFO-'
+ }]
},
hasChanges: true
}
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const merged = mergeDeep.compareDeep(target, source)
console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
expect(merged).toEqual(expected)
@@ -884,13 +876,13 @@ entries:
it('CompareDeep does not mutate source object', () => {
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const target = {
teams: [
{ name: 'developers' },
@@ -901,25 +893,25 @@ entries:
teams: ['developers']
}
mergeDeep.compareDeep(target, source)
-
+
// const result = mergeDeep.compareDeep(target, source)
// console.log(`source ${JSON.stringify(source, null, 2)}`)
// console.log(`target ${JSON.stringify(target, null, 2)}`)
// console.log(`result ${JSON.stringify(result, null, 2)}`)
-
+
expect(source.teams).toEqual(['developers'])
})
it('CompareDeep produces correct result for arrays of named objects', () => {
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const target = {
teams: [
{ name: 'developers' },
@@ -930,25 +922,24 @@ entries:
teams: ['developers']
}
const result = mergeDeep.compareDeep(target, source)
-
+
console.log(`source ${JSON.stringify(source, null, 2)}`)
console.log(`target ${JSON.stringify(target, null, 2)}`)
console.log(`result ${JSON.stringify(result, null, 2)}`)
-
+
expect(result.deletions.teams).toEqual([{ name: 'marketing' }])
})
-
-
+
it('CompareDeep result has changes when source is empty and target is not', () => {
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const target = {
required_pull_request_reviews: {
dismissal_restrictions: {
@@ -958,7 +949,7 @@ entries:
}
}
}
-
+
const source = {
required_pull_request_reviews: {
dismissal_restrictions: {
@@ -969,20 +960,20 @@ entries:
}
}
const result = mergeDeep.compareDeep(target, source)
-
+
expect(result.hasChanges).toBeTruthy()
})
-
+
it('CompareDeep result has no change when source and target match', () => {
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const target = {
required_pull_request_reviews: {
dismissal_restrictions: {
@@ -1003,89 +994,89 @@ entries:
}
}
const result = mergeDeep.compareDeep(target, source)
-
+
expect(result.hasChanges).toBeFalsy()
})
-
+
it('CompareDeep finds modifications on top-level arrays with different ordering', () => {
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const target = [
- { username: 'collaborator-1' },
- { username: 'collaborator-2' },
- ]
+ { username: 'collaborator-1' },
+ { username: 'collaborator-2' }
+ ]
const source = [
- { username: 'collaborator-2' },
- { username: 'collaborator-1' },
- ]
+ { username: 'collaborator-2' },
+ { username: 'collaborator-1' }
+ ]
const result = mergeDeep.compareDeep(target, source)
-
+
expect(result.hasChanges).toBeFalsy()
})
-
+
it('CompareDeep does not report changes for matching empty targets', () => {
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const target = []
const source = []
const result = mergeDeep.compareDeep(target, source)
-
+
expect(result.hasChanges).toBeFalsy()
})
it('Compare array as toplevel object', () => {
const source = [
{
- "username": "iksafagr",
- "pendinginvite": false,
- "permission": "admin"
+ username: 'iksafagr',
+ pendinginvite: false,
+ permission: 'admin'
}
]
const target = [
{
- "username": "iksafagr",
- "pendinginvite": false,
- "permission": "admin"
+ username: 'iksafagr',
+ pendinginvite: false,
+ permission: 'admin'
},
{
- "username": "iksathrr",
- "pendinginvite": false,
- "permission": "admin"
+ username: 'iksathrr',
+ pendinginvite: false,
+ permission: 'admin'
}
]
-
+
const expected = {
deletions: [{
- "username": "iksathrr",
- "pendinginvite": false,
- "permission": "admin"
- }],
- modifications:{}
+ username: 'iksathrr',
+ pendinginvite: false,
+ permission: 'admin'
+ }],
+ modifications: {}
}
const ignorableFields = []
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const merged = mergeDeep.compareDeep(target, source)
console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
expect(merged.deletions).toEqual(expected.deletions)
@@ -1097,230 +1088,541 @@ entries:
expect(same.modifications).toEqual({})
expect(same.modifications).toEqual({})
})
-
+
it('Ruleset Compare Works when no changes', () => {
const target = [
{
- "id": 68617,
- "name": "demo",
- "target": "branch",
- "source_type": "Organization",
- "source": "decyjphr-scale-test",
- "enforcement": "disabled",
- "conditions": {
- "ref_name": {
- "exclude": [
- "refs/heads/oldmaster"
+ id: 68617,
+ name: 'demo',
+ target: 'branch',
+ source_type: 'Organization',
+ source: 'decyjphr-scale-test',
+ enforcement: 'disabled',
+ conditions: {
+ ref_name: {
+ exclude: [
+ 'refs/heads/oldmaster'
],
- "include": [
- "~DEFAULT_BRANCH"
+ include: [
+ '~DEFAULT_BRANCH'
]
},
- "repository_name": {
- "exclude": [
- "test",
- "test1"
+ repository_name: {
+ exclude: [
+ 'test',
+ 'test1'
],
- "include": [
- "test*"
+ include: [
+ 'test*'
],
- "protected": true
+ protected: true
}
},
- "rules": [
+ rules: [
{
- "type": "creation"
+ type: 'creation'
},
{
- "type": "update"
+ type: 'update'
},
{
- "type": "deletion"
+ type: 'deletion'
},
{
- "type": "required_linear_history"
+ type: 'required_linear_history'
},
{
- "type": "required_signatures"
+ type: 'required_signatures'
},
{
- "type": "pull_request",
- "parameters": {
- "require_code_owner_review": true,
- "require_last_push_approval": true,
- "dismiss_stale_reviews_on_push": true,
- "required_approving_review_count": 10,
- "required_review_thread_resolution": true
+ type: 'pull_request',
+ parameters: {
+ require_code_owner_review: true,
+ require_last_push_approval: true,
+ dismiss_stale_reviews_on_push: true,
+ required_approving_review_count: 10,
+ required_review_thread_resolution: true
}
},
{
- "type": "commit_message_pattern",
- "parameters": {
- "name": "test commit_message_pattern",
- "negate": true,
- "pattern": "skip*",
- "operator": "starts_with"
+ type: 'commit_message_pattern',
+ parameters: {
+ name: 'test commit_message_pattern',
+ negate: true,
+ pattern: 'skip*',
+ operator: 'starts_with'
}
},
{
- "type": "commit_author_email_pattern",
- "parameters": {
- "name": "test commit_author_email_pattern",
- "negate": false,
- "pattern": "^.*@example.com$",
- "operator": "regex"
+ type: 'commit_author_email_pattern',
+ parameters: {
+ name: 'test commit_author_email_pattern',
+ negate: false,
+ pattern: '^.*@example.com$',
+ operator: 'regex'
}
},
{
- "type": "committer_email_pattern",
- "parameters": {
- "name": "test committer_email_pattern",
- "negate": false,
- "pattern": "^.*@example.com$",
- "operator": "regex"
+ type: 'committer_email_pattern',
+ parameters: {
+ name: 'test committer_email_pattern',
+ negate: false,
+ pattern: '^.*@example.com$',
+ operator: 'regex'
}
},
{
- "type": "branch_name_pattern",
- "parameters": {
- "name": "test branch_name_pattern",
- "negate": false,
- "pattern": ".*/.*",
- "operator": "regex"
+ type: 'branch_name_pattern',
+ parameters: {
+ name: 'test branch_name_pattern',
+ negate: false,
+ pattern: '.*/.*',
+ operator: 'regex'
}
}
],
- "node_id": "RRS_lACkVXNlcs4GH_FizgABDAk",
- "_links": {
- "self": {
- "href": "https://api.github.com/orgs/decyjphr-scale-test/rulesets/68617"
+ node_id: 'RRS_lACkVXNlcs4GH_FizgABDAk',
+ _links: {
+ self: {
+ href: 'https://api.github.com/orgs/decyjphr-scale-test/rulesets/68617'
},
- "html": {
- "href": "https://github.com/organizations/decyjphr-scale-test/settings/rules/68617"
+ html: {
+ href: 'https://github.com/organizations/decyjphr-scale-test/settings/rules/68617'
}
},
- "created_at": "2023-08-11T15:55:49.071Z",
- "updated_at": "2023-08-11T15:55:49.139Z",
- "bypass_actors": [
+ created_at: '2023-08-11T15:55:49.071Z',
+ updated_at: '2023-08-11T15:55:49.139Z',
+ bypass_actors: [
{
- "actor_id": 1,
- "actor_type": "OrganizationAdmin",
- "bypass_mode": "pull_request"
+ actor_id: 1,
+ actor_type: 'OrganizationAdmin',
+ bypass_mode: 'pull_request'
}
]
}
- ]
+ ]
const source = [
{
- "name": "demo",
- "target": "branch",
- "enforcement": "disabled",
- "bypass_actors": [
+ name: 'demo',
+ target: 'branch',
+ enforcement: 'disabled',
+ bypass_actors: [
{
- "actor_id": 1,
- "actor_type": "OrganizationAdmin",
- "bypass_mode": "pull_request"
+ actor_id: 1,
+ actor_type: 'OrganizationAdmin',
+ bypass_mode: 'pull_request'
}
],
- "conditions": {
- "ref_name": {
- "include": [
- "~DEFAULT_BRANCH"
+ conditions: {
+ ref_name: {
+ include: [
+ '~DEFAULT_BRANCH'
],
- "exclude": [
- "refs/heads/oldmaster"
+ exclude: [
+ 'refs/heads/oldmaster'
]
},
- "repository_name": {
- "include": [
- "test*"
+ repository_name: {
+ include: [
+ 'test*'
],
- "exclude": [
- "test",
- "test1"
+ exclude: [
+ 'test',
+ 'test1'
],
- "protected": true
+ protected: true
}
},
- "rules": [
+ rules: [
{
- "type": "creation"
+ type: 'creation'
},
{
- "type": "update"
+ type: 'update'
},
{
- "type": "deletion"
+ type: 'deletion'
},
{
- "type": "required_linear_history"
+ type: 'required_linear_history'
},
{
- "type": "required_signatures"
+ type: 'required_signatures'
},
{
- "type": "pull_request",
- "parameters": {
- "dismiss_stale_reviews_on_push": true,
- "require_code_owner_review": true,
- "require_last_push_approval": true,
- "required_approving_review_count": 10,
- "required_review_thread_resolution": true
+ type: 'pull_request',
+ parameters: {
+ dismiss_stale_reviews_on_push: true,
+ require_code_owner_review: true,
+ require_last_push_approval: true,
+ required_approving_review_count: 10,
+ required_review_thread_resolution: true
}
},
{
- "type": "commit_message_pattern",
- "parameters": {
- "name": "test commit_message_pattern",
- "negate": true,
- "operator": "starts_with",
- "pattern": "skip*"
+ type: 'commit_message_pattern',
+ parameters: {
+ name: 'test commit_message_pattern',
+ negate: true,
+ operator: 'starts_with',
+ pattern: 'skip*'
}
},
{
- "type": "commit_author_email_pattern",
- "parameters": {
- "name": "test commit_author_email_pattern",
- "negate": false,
- "operator": "regex",
- "pattern": "^.*@example.com$"
+ type: 'commit_author_email_pattern',
+ parameters: {
+ name: 'test commit_author_email_pattern',
+ negate: false,
+ operator: 'regex',
+ pattern: '^.*@example.com$'
}
},
{
- "type": "committer_email_pattern",
- "parameters": {
- "name": "test committer_email_pattern",
- "negate": false,
- "operator": "regex",
- "pattern": "^.*@example.com$"
+ type: 'committer_email_pattern',
+ parameters: {
+ name: 'test committer_email_pattern',
+ negate: false,
+ operator: 'regex',
+ pattern: '^.*@example.com$'
}
},
{
- "type": "branch_name_pattern",
- "parameters": {
- "name": "test branch_name_pattern",
- "negate": false,
- "operator": "regex",
- "pattern": ".*/.*"
+ type: 'branch_name_pattern',
+ parameters: {
+ name: 'test branch_name_pattern',
+ negate: false,
+ operator: 'regex',
+ pattern: '.*/.*'
}
}
]
}
- ]
+ ]
const ignorableFields = ['id', 'node_id', 'default', 'url']
const mockReturnGitHubContext = jest.fn().mockReturnValue({
- request: () => {},
- });
+ request: () => {}
+ })
const mergeDeep = new MergeDeep(
log,
mockReturnGitHubContext,
ignorableFields
- );
+ )
const merged = mergeDeep.compareDeep(target, source)
expect(merged.hasChanges).toBeFalsy()
- //console.log(`source ${JSON.stringify(source, null, 2)}`)
- //console.log(`target ${JSON.stringify(target, null, 2)}`)
- //console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
+ // console.log(`source ${JSON.stringify(source, null, 2)}`)
+ // console.log(`target ${JSON.stringify(target, null, 2)}`)
+ // console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
+ })
+
+ it('Merge array as the second level with same id - add new value', () => {
+ const target = YAML.load(`
+ environments:
+ - name: production
+ variables:
+ - name: ROUTE53_HOSTNAME
+ value: 'https://foo-prod.com'
+ `
+ )
+
+ const source = YAML.load(`
+ environments:
+ - name: production
+ variables:
+ - name: ROUTE53_HOSTNAME
+ value: 'https://foo-prod.com'
+ - name: SECOND_VARIABLE
+ value: 'My_SECOND_VARIABLE'
+ `)
+
+ const expected = {
+ additions: {},
+ modifications: {
+ environments: [{
+ name: 'production',
+ variables: [
+ {
+ name: 'SECOND_VARIABLE',
+ value: 'My_SECOND_VARIABLE'
+ }
+ ]
+ }]
+ },
+ deletions: {},
+ hasChanges: true
+ }
+
+ const ignorableFields = []
+ const mockReturnGitHubContext = jest.fn().mockReturnValue({
+ request: () => {}
+ })
+ const mergeDeep = new MergeDeep(
+ log,
+ mockReturnGitHubContext,
+ ignorableFields
+ )
+ const merged = mergeDeep.compareDeep(target, source)
+ console.log(`source ${JSON.stringify(source, null, 2)}`)
+ console.log(`target ${JSON.stringify(target, null, 2)}`)
+ console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
+ expect(merged.additions).toEqual(expected.additions)
+ expect(merged.deletions).toEqual(expected.deletions)
+ expect(merged.modifications).toEqual(expected.modifications)
+ expect(merged.modifications.length).toEqual(expected.modifications.length)
+
+ console.log(`target = ${JSON.stringify(target, null, 2)}`)
+ const overrideConfig = mergeDeep.mergeDeep({}, target, source)
+
+ console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`)
+
+ const same = mergeDeep.compareDeep(overrideConfig, target)
+ console.log(`new diffs ${JSON.stringify(same, null, 2)}`)
+ expect(same.additions).toEqual({})
+ expect(same.modifications).toEqual({})
+ })
+
+ it('Identify modification and addition to the environment variables correctly', () => {
+ const target = YAML.load(`
+ environments:
+ - name: production
+ variables:
+ - name: ROUTE53_HOSTNAME
+ value: 'https://foo-prod.com'
+ `
+ )
+
+ const source = YAML.load(`
+ environments:
+ - name: production
+ variables:
+ - name: ROUTE53_HOSTNAME
+ value: 'https://foo-prod.com.changed.value'
+ - name: SECOND_VARIABLE
+ value: 'My_SECOND_VARIABLE'
+ - name: development
+ variables:
+ - name: ROUTE53_HOSTNAME_DEVL
+ value: 'https://foo-devl.com'
+ - name: SECOND_VARIABLE_DEVL
+ value: 'My_SECOND_VARIABLE_DEVL'
+ `)
+
+ const expected = {
+ additions: {
+ environments: [{
+ name: 'development',
+ variables: [
+ {
+ name: 'ROUTE53_HOSTNAME_DEVL',
+ value: 'https://foo-devl.com'
+ },
+ {
+ name: 'SECOND_VARIABLE_DEVL',
+ value: 'My_SECOND_VARIABLE_DEVL'
+ }
+ ]
+ }]
+ },
+ modifications: {
+ environments: [{
+ name: 'production',
+ variables: [
+ {
+ name: 'ROUTE53_HOSTNAME',
+ value: 'https://foo-prod.com.changed.value'
+ },
+ {
+ name: 'SECOND_VARIABLE',
+ value: 'My_SECOND_VARIABLE'
+ }
+ ]
+ }]
+ },
+ deletions: {},
+ hasChanges: true
+ }
+
+ const ignorableFields = []
+ const mockReturnGitHubContext = jest.fn().mockReturnValue({
+ request: () => {}
+ })
+ const mergeDeep = new MergeDeep(
+ log,
+ mockReturnGitHubContext,
+ ignorableFields
+ )
+ const merged = mergeDeep.compareDeep(target, source)
+ console.log(`source ${JSON.stringify(source, null, 2)}`)
+ console.log(`target ${JSON.stringify(target, null, 2)}`)
+ console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
+ expect(merged.additions).toEqual(expected.additions)
+ expect(merged.deletions).toEqual(expected.deletions)
+ expect(merged.modifications).toEqual(expected.modifications)
+ expect(merged.modifications.length).toEqual(expected.modifications.length)
+
+ console.log(`target = ${JSON.stringify(target, null, 2)}`)
+ const overrideConfig = mergeDeep.mergeDeep({}, target, source)
+
+ console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`)
+
+ const same = mergeDeep.compareDeep(overrideConfig, target)
+ console.log(`new diffs ${JSON.stringify(same, null, 2)}`)
+ expect(same.additions).toEqual({})
+ expect(same.modifications).toEqual({})
+ })
+
+ it('CompareDeep branches', () => {
+ const target = YAML.load(`
+branches:
+ # If the name of the branch is default, it will create a branch protection for the default branch in the repo
+ - name: default
+ # https://developer.github.com/v3/repos/branches/#update-branch-protection
+ # Branch Protection settings. Set to null to disable
+ protection:
+ # Required. Require at least one approving review on a pull request, before merging. Set to null to disable.
+ required_pull_request_reviews:
+ # The number of approvals required. (1-6)
+ required_approving_review_count: 2
+ # Dismiss approved reviews automatically when a new commit is pushed.
+ dismiss_stale_reviews: true
+ # Blocks merge until code owners have reviewed.
+ require_code_owner_reviews: true
+ # Specify which users and teams can dismiss pull request reviews. Pass an empty dismissal_restrictions object to disable. User and team dismissal_restrictions are only available for organization-owned repositories. Omit this parameter for personal repositories.
+ dismissal_restrictions:
+ users: []
+ teams: []
+ # Required. Require status checks to pass before merging. Set to null to disable
+ required_status_checks:
+ # Required. Require branches to be up to date before merging.
+ strict: true
+ # Required. The list of status checks to require in order to merge into this branch
+ contexts: []
+ # Required. Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable.
+ enforce_admins: true
+ # Required. Restrict who can push to this branch. Team and user restrictions are only available for organization-owned repositories. Set to null to disable.
+ restrictions:
+ apps: []
+ users: []
+ teams: []`)
+
+ const source = YAML.load(`
+ branches:
+ - name: feature1
+ # https://developer.github.com/v3/repos/branches/#update-branch-protection
+ # Branch Protection settings. Set to null to disable
+ protection:
+ # Required. Require at least one approving review on a pull request, before merging. Set to null to disable.
+ required_pull_request_reviews:
+ # The number of approvals required. (1-6)
+ required_approving_review_count: 5
+ # Dismiss approved reviews automatically when a new commit is pushed.
+ dismiss_stale_reviews: true
+ # Blocks merge until code owners have reviewed.
+ require_code_owner_reviews: true
+ # Specify which users and teams can dismiss pull request reviews. Pass an empty dismissal_restrictions object to disable. User and team dismissal_restrictions are only available for organization-owned repositories. Omit this parameter for personal repositories.
+ dismissal_restrictions:
+ users: []
+ teams: []
+ # Required. Require status checks to pass before merging. Set to null to disable
+ required_status_checks:
+ # Required. Require branches to be up to date before merging.
+ strict: true
+ # Required. The list of status checks to require in order to merge into this branch
+ contexts: []
+ # Required. Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable.
+ enforce_admins: false
+ # Required. Restrict who can push to this branch. Team and user restrictions are only available for organization-owned repositories. Set to null to disable.
+ restrictions:
+ apps: []
+ users: []
+ teams: []`)
+
+ const expected = {
+ additions: {
+ branches: [
+ {
+ name: 'feature1',
+ protection: {
+ required_pull_request_reviews: {
+ required_approving_review_count: 5,
+ dismiss_stale_reviews: true,
+ require_code_owner_reviews: true,
+ dismissal_restrictions: {
+ users: [],
+ teams: []
+ }
+ },
+ required_status_checks: {
+ strict: true,
+ contexts: []
+ },
+ enforce_admins: false,
+ restrictions: {
+ apps: [],
+ users: [],
+ teams: []
+ }
+ }
+ }
+ ]
+ },
+ modifications: {},
+ deletions: {
+ branches: [
+ {
+ name: 'default',
+ protection: {
+ required_pull_request_reviews: {
+ required_approving_review_count: 2,
+ dismiss_stale_reviews: true,
+ require_code_owner_reviews: true,
+ dismissal_restrictions: {
+ teams: [],
+ users: []
+ }
+ },
+ required_status_checks: {
+ strict: true,
+ contexts: []
+ },
+ enforce_admins: true,
+ restrictions: {
+ apps: [],
+ teams: [],
+ users: []
+ }
+ }
+ }
+ ]
+ },
+ hasChanges: true
+ }
+
+ const ignorableFields = []
+ const mockReturnGitHubContext = jest.fn().mockReturnValue({
+ request: () => {}
+ })
+ const mergeDeep = new MergeDeep(
+ log,
+ mockReturnGitHubContext,
+ ignorableFields
+ )
+ const merged = mergeDeep.compareDeep(target, source)
+ console.log(`source ${JSON.stringify(source, null, 2)}`)
+ console.log(`target ${JSON.stringify(target, null, 2)}`)
+ console.log(`diffs ${JSON.stringify(merged, null, 2)}`)
+ expect(merged.additions).toEqual(expected.additions)
+ expect(merged.deletions).toEqual(expected.deletions)
+ expect(merged.modifications).toEqual(expected.modifications)
+ expect(merged.modifications.length).toEqual(expected.modifications.length)
+ // //
+ console.log(`target = ${JSON.stringify(target, null, 2)}`)
+ const overrideConfig = mergeDeep.mergeDeep({}, target, source)
+
+ console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`)
+
+ const same = mergeDeep.compareDeep(overrideConfig, target)
+ console.log(`new diffs ${JSON.stringify(same, null, 2)}`)
+ expect(same.additions).toEqual({})
+ expect(same.modifications).toEqual({})
})
-
})
From fc5b693ba435f815c0c653e92b94ecd1745f9f47 Mon Sep 17 00:00:00 2001
From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com>
Date: Tue, 20 Aug 2024 19:55:22 -0400
Subject: [PATCH 3/6] Add Actions workflow to delete old releases (#667)
* Create delete-old-releases.yml
* Update delete-old-releases.yml
* Update delete-old-releases.yml
---
.github/workflows/delete-old-releases.yml | 25 +++++++++++++++++++++++
1 file changed, 25 insertions(+)
create mode 100644 .github/workflows/delete-old-releases.yml
diff --git a/.github/workflows/delete-old-releases.yml b/.github/workflows/delete-old-releases.yml
new file mode 100644
index 00000000..79837aad
--- /dev/null
+++ b/.github/workflows/delete-old-releases.yml
@@ -0,0 +1,25 @@
+name: Delete old releases
+permissions: write-all
+
+on:
+ workflow_dispatch:
+ inputs:
+ beforeDate:
+ type: string
+ required: true
+ description: YYYY-MM-DD - All releases before this date are deleted.
+ default: "2024-01-01"
+
+jobs:
+ delete-releases:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Delete releases
+ run: |
+ for i in $(gh release list --repo https://github.com/$GITHUB_REPOSITORY --json createdAt,tagName --limit 1000 | jq --arg date $BEFORE_DATE '.[] | select(.createdAt < $date ) | .tagName' | tr -d '"'); do gh release delete $i -y --cleanup-tag --repo https://github.com/$GITHUB_REPOSITORY ; done
+ echo Deleted releases before $BEFORE_DATE in https://github.com/$GITHUB_REPOSITORY >> $GITHUB_STEP_SUMMARY
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ BEFORE_DATE: ${{ inputs.beforeDate }}
+
+
From c9247f5f864222263ee8864c5f18062e156fe067 Mon Sep 17 00:00:00 2001
From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com>
Date: Fri, 30 Aug 2024 17:48:04 -0400
Subject: [PATCH 4/6] fix code scanning alerts (#669)
* fix alerts
* fix alerts
* fix alerts
* fix alerts
* add tests and simplify Glob
* fix import to lowercase file
* removed debugging code
---
lib/glob.js | 20 +++++++++-
package-lock.json | 4 +-
package.json | 5 ++-
test/unit/lib/glob.test.ts | 78 ++++++++++++++++++++++++++++++++++++++
4 files changed, 101 insertions(+), 6 deletions(-)
create mode 100644 test/unit/lib/glob.test.ts
diff --git a/lib/glob.js b/lib/glob.js
index beeb1ada..78a0eaaf 100644
--- a/lib/glob.js
+++ b/lib/glob.js
@@ -1,8 +1,24 @@
class Glob {
constructor (glob) {
this.glob = glob
- const regexptex = glob.replace(/\//g, '\\/').replace(/\?/g, '([^\\/])').replace(/\./g, '\\.').replace(/\*/g, '([^\\/]*)')
- this.regexp = new RegExp(`^${regexptex}$`, 'u')
+
+ // If not a glob pattern then just match the string.
+ if (!this.glob.includes('*')) {
+ this.regexp = new RegExp(`.*${this.glob}.*`, 'u')
+ return
+ }
+ this.regexptText = this.globize(this.glob)
+ this.regexp = new RegExp(`^${this.regexptText}$`, 'u')
+ }
+
+ globize (glob) {
+ return glob
+ .replace(/\\/g, '\\\\') // escape backslashes
+ .replace(/\//g, '\\/') // escape forward slashes
+ .replace(/\./g, '\\.') // escape periods
+ .replace(/\?/g, '([^\\/])') // match any single character except /
+ .replace(/\*\*/g, '.+') // match any character except /, including /
+ .replace(/\*/g, '([^\\/]*)') // match any character except /
}
toString () {
diff --git a/package-lock.json b/package-lock.json
index dc617ec2..9baba6f5 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/unit/lib/glob.test.ts b/test/unit/lib/glob.test.ts
new file mode 100644
index 00000000..27b6d29b
--- /dev/null
+++ b/test/unit/lib/glob.test.ts
@@ -0,0 +1,78 @@
+const Glob = require('../../../lib/glob')
+
+describe('glob test', function () {
+
+ test('Test Glob **', () => {
+ let pattern = new Glob('**/xss')
+ let str = 'test/web/xss'
+ expect(str.search(pattern)>=0).toBeTruthy()
+ str = 'test/web/xsssss'
+ expect(str.search(pattern)>=0).toBeFalsy()
+
+ pattern = new Glob('**/*.txt')
+ str = 'sub/3.txt'
+ expect(str.search(pattern)>=0).toBeTruthy()
+ str = '/sub1/sub2/sub3/3.txt'
+ expect(str.search(pattern)>=0).toBeTruthy()
+
+ pattern = new Glob('**/csrf-protection-disabled')
+ str = 'java/csrf-protection-disabled'
+ expect(str.search(pattern)>=0).toBeTruthy()
+ str = '/java/test/csrf-protection-disabled'
+ expect(str.search(pattern)>=0).toBeTruthy()
+ })
+
+ test('Test Glob *', () => {
+ let str = 'web/xss'
+ let pattern = new Glob('*/xss')
+ expect(str.search(pattern)>=0).toBeTruthy()
+
+ pattern = new Glob('./[0-9].*')
+ str = './1.gif'
+ expect(str.search(pattern)>=0).toBeTruthy()
+ str = './2.gif'
+ expect(str.search(pattern)>=0).toBeTruthy()
+ str = './2.'
+ expect(str.search(pattern)>=0).toBeTruthy()
+
+ pattern = new Glob('*/csrf-protection-disabled')
+ str = 'java/csrf-protection-disabled'
+ expect(str.search(pattern)>=0).toBeTruthy()
+ str = 'rb/csrf-protection-disabled'
+ expect(str.search(pattern)>=0).toBeTruthy()
+
+ pattern = new Glob('*/hardcoded-credential*')
+ str = 'java/csrf-protection-disabled'
+ expect(str.search(pattern)>=0).toBeFalsy()
+ str = 'rb/csrf-protection-disabled'
+ expect(str.search(pattern)>=0).toBeFalsy()
+ str = 'cs/hardcoded-credentials'
+ expect(str.search(pattern)>=0).toBeTruthy()
+ str = 'java/hardcoded-credential-api-call'
+ expect(str.search(pattern)>=0).toBeTruthy()
+
+ })
+
+ test('Test Glob no *', () => {
+ let pattern = new Glob('csrf-protection-disabled')
+ let str = 'java/hardcoded-credential-api-call'
+ expect(str.search(pattern)>=0).toBeFalsy()
+ str = 'cs/test/hardcoded-credentials'
+ expect(str.search(pattern)>=0).toBeFalsy()
+ str = 'rb/csrf-protection-disabled'
+ expect(str.search(pattern)>=0).toBeTruthy()
+ str = 'java/csrf-protection-disabled'
+ expect(str.search(pattern)>=0).toBeTruthy()
+
+ pattern = new Glob('csrf')
+ str = 'java/hardcoded-credential-api-call'
+ expect(str.search(pattern)>=0).toBeFalsy()
+ str = 'cs/test/hardcoded-credentials'
+ expect(str.search(pattern)>=0).toBeFalsy()
+ str = 'rb/csrf-protection-disabled'
+ expect(str.search(pattern)>=0).toBeTruthy()
+ str = 'java/csrf-protection-disabled'
+ expect(str.search(pattern)>=0).toBeTruthy()
+ })
+
+})
From 610094d8af855f89c0a2135c180cb6e840e060d9 Mon Sep 17 00:00:00 2001
From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com>
Date: Mon, 16 Sep 2024 19:31:00 -0400
Subject: [PATCH 5/6] Decyjphr/pr 682 (#685)
* update comments from #249
I found the logic behind some of what happened here hard to follow and updated the comments to try to make it easier to follow.
* fix environments updating global array
The environments plugin was changing `MergeDeep.NAME_FIELDS`, which is a global object. The reason was to avoid environments being filtered out from the change list if they only have a name field.
However, the environments plugin has it's own overriden sync method, and thus we can simply drop the whole filtering from that method.
Fixes #108
* remove repeating line
This has to be a "typo" from 9a74e05 calling the same assignment twice. Removing to clean up.
---------
Co-authored-by: Torgeir S. hos Sykehuspartner <93591857+torgst@users.noreply.github.com>
---
lib/mergeDeep.js | 1 -
lib/plugins/diffable.js | 3 ++-
lib/plugins/environments.js | 8 ++------
3 files changed, 4 insertions(+), 8 deletions(-)
diff --git a/lib/mergeDeep.js b/lib/mergeDeep.js
index a685a048..054a5b34 100644
--- a/lib/mergeDeep.js
+++ b/lib/mergeDeep.js
@@ -378,5 +378,4 @@ class MergeDeep {
}
}
MergeDeep.NAME_FIELDS = NAME_FIELDS
-MergeDeep.NAME_FIELDS = NAME_FIELDS
module.exports = MergeDeep
diff --git a/lib/plugins/diffable.js b/lib/plugins/diffable.js
index 74871691..8db6a0ef 100644
--- a/lib/plugins/diffable.js
+++ b/lib/plugins/diffable.js
@@ -93,7 +93,7 @@ module.exports = class Diffable extends ErrorStash {
}
}
- // Filter out all empty entries (usually from repo override)
+ // Remove any null or undefined values from the diffables (usually comes from repo override)
for (const entry of filteredEntries) {
for (const key of Object.keys(entry)) {
if (entry[key] === null || entry[key] === undefined) {
@@ -101,6 +101,7 @@ module.exports = class Diffable extends ErrorStash {
}
}
}
+ // Delete any diffable that now only has name and no other attributes
filteredEntries = filteredEntries.filter(entry => Object.keys(entry).filter(key => !MergeDeep.NAME_FIELDS.includes(key)).length !== 0)
const changes = []
diff --git a/lib/plugins/environments.js b/lib/plugins/environments.js
index 6d52b409..4ceaa9ff 100644
--- a/lib/plugins/environments.js
+++ b/lib/plugins/environments.js
@@ -17,10 +17,6 @@ module.exports = class Environments extends Diffable {
}
})
}
-
- // Remove 'name' from filtering list so Environments with only a name defined are processed.
- MergeDeep.NAME_FIELDS.splice(MergeDeep.NAME_FIELDS.indexOf('name'), 1)
-
}
async find() {
@@ -296,7 +292,7 @@ module.exports = class Environments extends Diffable {
let filteredEntries = this.filterEntries()
return this.find().then(existingRecords => {
- // Filter out all empty entries (usually from repo override)
+ // Remove any null or undefined values from the diffables (usually comes from repo override)
for (const entry of filteredEntries) {
for (const key of Object.keys(entry)) {
if (entry[key] === null || entry[key] === undefined) {
@@ -304,7 +300,7 @@ module.exports = class Environments extends Diffable {
}
}
}
- filteredEntries = filteredEntries.filter(entry => Object.keys(entry).filter(key => !MergeDeep.NAME_FIELDS.includes(key)).length !== 0)
+ // For environments, we want to keep the entries with only name defined.
const changes = []
From 85aae4f2c105bb308bf420e00fb6f0fc1ad5f3e6 Mon Sep 17 00:00:00 2001
From: "Torgeir S. hos Sykehuspartner"
<93591857+torgst@users.noreply.github.com>
Date: Tue, 17 Sep 2024 01:32:11 +0200
Subject: [PATCH 6/6] Fix: array comparison breaks if using the environments
plugin (#682)
* update comments from #249
I found the logic behind some of what happened here hard to follow and updated the comments to try to make it easier to follow.
* fix environments updating global array
The environments plugin was changing `MergeDeep.NAME_FIELDS`, which is a global object. The reason was to avoid environments being filtered out from the change list if they only have a name field.
However, the environments plugin has it's own overriden sync method, and thus we can simply drop the whole filtering from that method.
Fixes #108
* remove repeating line
This has to be a "typo" from 9a74e05 calling the same assignment twice. Removing to clean up.