Skip to content

Commit

Permalink
Merge pull request #237 from github/update-base-branch-improvements
Browse files Browse the repository at this point in the history
Update Base Branch Improvements
  • Loading branch information
GrantBirki committed Feb 1, 2024
2 parents 511a5b8 + ef4274f commit fdb7b54
Show file tree
Hide file tree
Showing 13 changed files with 596 additions and 100 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ As seen above, we have two steps. One for a noop deploy, and one for a regular d
| `production_environments` | `false` | `production` | A comma separated list of environments that should be treated as "production". GitHub defines "production" as an environment that end users or systems interact with. Example: "production,production-eu". By default, GitHub will set the "production_environment" to "true" if the environment name is "production". This option allows you to override that behavior so you can use "prod", "prd", "main", "production-eu", etc. as your production environment name. ref: [#208](https://github.com/github/branch-deploy/issues/208) |
| `stable_branch` | `false` | `main` | The name of a stable branch to deploy to (rollbacks). Example: "main" |
| `update_branch` | `false` | `warn` | Determine how you want this Action to handle "out-of-date" branches. Available options: "disabled", "warn", "force". "disabled" means that the Action will not care if a branch is out-of-date. "warn" means that the Action will warn the user that a branch is out-of-date and exit without deploying. "force" means that the Action will force update the branch. Note: The "force" option is not recommended due to Actions not being able to re-run CI on commits originating from Actions itself |
| `outdated_mode` | `false` | `"strict"` | The mode to use for determining if a branch is up-to-date or not before allowing deployments. This option is closely related to the `update_branch` input option above. There are three available modes to choose from: `pr_base`, `default_branch`, or `strict`. The default is `strict` to help ensure that deployments are using the most up-to-date code. Please see the [documentation](docs/outdated_mode.md) for more details. |
| `required_contexts` | `false` | `"false"` | Manually enforce commit status checks before a deployment can continue. Only use this option if you wish to manually override the settings you have configured for your branch protection settings for your GitHub repository. Default is "false" - Example value: "context1,context2,context3" - In most cases you will not need to touch this option |
| `skip_ci` | `false` | `""` | A comma separated list of environments that will not use passing CI as a requirement for deployment. Use this option to explicitly bypass branch protection settings for a certain environment in your repository. Default is an empty string `""` - Example: `"development,staging"` |
| `skip_reviews` | `false` | `""` | A comma separated list of environment that will not use reviews/approvals as a requirement for deployment. Use this options to explicitly bypass branch protection settings for a certain environment in your repository. Default is an empty string `""` - Example: `"development,staging"` |
Expand Down
3 changes: 3 additions & 0 deletions __tests__/functions/help.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const defaultInputs = {
lock_info_alias: '.wcid',
global_lock_flag: '--global',
update_branch: 'warn',
outdated_mode: 'strict',
required_contexts: 'false',
allowForks: 'true',
skipCi: '',
Expand Down Expand Up @@ -70,6 +71,7 @@ test('successfully calls help with non-defaults', async () => {
lock_info_alias: '.wcid',
global_lock_flag: '--global',
update_branch: 'force',
outdated_mode: 'pr_base',
required_contexts: 'cat',
allowForks: 'false',
skipCi: 'development',
Expand Down Expand Up @@ -102,6 +104,7 @@ test('successfully calls help with non-defaults', async () => {
lock_info_alias: '.wcid',
global_lock_flag: '--global',
update_branch: 'force',
outdated_mode: 'default_branch',
required_contexts: 'cat',
allowForks: 'false',
skipCi: 'development',
Expand Down
151 changes: 151 additions & 0 deletions __tests__/functions/outdated-check.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import * as core from '@actions/core'
import {isOutdated} from '../../src/functions/outdated-check'

const debugMock = jest.spyOn(core, 'debug')

var context
var octokit
var data

beforeEach(() => {
jest.clearAllMocks()
jest.spyOn(core, 'info').mockImplementation(() => {})
jest.spyOn(core, 'debug').mockImplementation(() => {})
jest.spyOn(core, 'warning').mockImplementation(() => {})

data = {
outdated_mode: 'strict',
mergeStateStatus: 'CLEAN',
stableBaseBranch: {
data: {
commit: {sha: 'beefdead'},
name: 'stable-branch'
},
status: 200
},
baseBranch: {
data: {
commit: {sha: 'deadbeef'},
name: 'test-branch'
},
status: 200
},
pr: {
data: {
head: {
ref: 'test-ref',
sha: 'abc123'
},
base: {
ref: 'base-ref'
}
},
status: 200
}
}

context = {
repo: {
owner: 'corp',
repo: 'test'
}
}

octokit = {
rest: {
repos: {
compareCommits: jest
.fn()
.mockReturnValue({data: {behind_by: 0}, status: 200})
}
}
}
})

test('checks if the branch is out-of-date via commit comparison and finds that it is not', async () => {
expect(await isOutdated(context, octokit, data)).toStrictEqual({
branch: 'test-branch|stable-branch',
outdated: false
})
})

test('checks if the branch is out-of-date via commit comparison and finds that it is not, when the stable branch and base branch are the same (i.e a PR to main)', async () => {
data.baseBranch = data.stableBaseBranch
expect(await isOutdated(context, octokit, data)).toStrictEqual({
branch: 'stable-branch|stable-branch',
outdated: false
})
})

test('checks if the branch is out-of-date via commit comparison and finds that it is, when the stable branch and base branch are the same (i.e a PR to main)', async () => {
data.baseBranch = data.stableBaseBranch

octokit.rest.repos.compareCommits = jest
.fn()
.mockReturnValue({data: {behind_by: 1}, status: 200})

expect(await isOutdated(context, octokit, data)).toStrictEqual({
branch: 'stable-branch',
outdated: true
})
})

test('checks if the branch is out-of-date via commit comparison and finds that it is not using outdated_mode pr_base', async () => {
data.outdated_mode = 'pr_base'
expect(await isOutdated(context, octokit, data)).toStrictEqual({
branch: 'test-branch',
outdated: false
})
expect(debugMock).toHaveBeenCalledWith(
'checking isOutdated with pr_base mode'
)
})

test('checks if the branch is out-of-date via commit comparison and finds that it is not using outdated_mode default_branch', async () => {
data.outdated_mode = 'default_branch'
expect(await isOutdated(context, octokit, data)).toStrictEqual({
branch: 'stable-branch',
outdated: false
})
expect(debugMock).toHaveBeenCalledWith(
'checking isOutdated with default_branch mode'
)
})

test('checks if the branch is out-of-date via commit comparison and finds that it is', async () => {
octokit.rest.repos.compareCommits = jest
.fn()
.mockReturnValue({data: {behind_by: 1}, status: 200})
expect(await isOutdated(context, octokit, data)).toStrictEqual({
branch: 'test-branch',
outdated: true
})
expect(debugMock).toHaveBeenCalledWith('checking isOutdated with strict mode')
})

test('checks if the branch is out-of-date via commit comparison and finds that it is only behind the stable branch', async () => {
octokit.rest.repos.compareCommits = jest
.fn()
.mockImplementationOnce(() =>
Promise.resolve({data: {behind_by: 0}, status: 200})
)
.mockImplementationOnce(() =>
Promise.resolve({data: {behind_by: 1}, status: 200})
)
expect(await isOutdated(context, octokit, data)).toStrictEqual({
branch: 'stable-branch',
outdated: true
})
expect(debugMock).toHaveBeenCalledWith('checking isOutdated with strict mode')
})

test('checks the mergeStateStatus and finds that it is BEHIND', async () => {
data.mergeStateStatus = 'BEHIND'
expect(await isOutdated(context, octokit, data)).toStrictEqual({
branch: 'test-branch',
outdated: true
})
expect(debugMock).toHaveBeenCalledWith(
'mergeStateStatus is BEHIND - exiting isOutdated logic early'
)
})
67 changes: 50 additions & 17 deletions __tests__/functions/prechecks.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {prechecks} from '../../src/functions/prechecks'
import {COLORS} from '../../src/functions/colors'
import * as isAdmin from '../../src/functions/admin'
import * as isOutdated from '../../src/functions/outdated-check'
import * as core from '@actions/core'

// Globals for testing
Expand Down Expand Up @@ -107,6 +108,19 @@ beforeEach(() => {
},
graphql: graphQLOK
}

// mock the request for fetching the baseBranch variable
octokit.rest.repos.getBranch = jest.fn().mockReturnValue({
data: {
commit: {sha: 'deadbeef'},
name: 'test-branch'
},
status: 200
})

jest.spyOn(isOutdated, 'isOutdated').mockImplementation(() => {
return {outdated: false, branch: 'test-branch'}
})
})

test('runs prechecks and finds that the IssueOps command is valid for a branch deployment', async () => {
Expand All @@ -122,7 +136,7 @@ test('runs prechecks and finds that the IssueOps command is valid for a branch d
test('runs prechecks and finds that the IssueOps command is valid for a rollback deployment', async () => {
octokit.rest.repos.getBranch = jest
.fn()
.mockReturnValueOnce({data: {commit: {sha: 'deadbeef'}}, status: 200})
.mockReturnValue({data: {commit: {sha: 'deadbeef'}}, status: 200})

data.environmentObj.stable_branch_used = true

Expand Down Expand Up @@ -505,7 +519,7 @@ test('runs prechecks and deploys to the stable branch', async () => {
})
octokit.rest.repos.getBranch = jest
.fn()
.mockReturnValueOnce({data: {commit: {sha: 'deadbeef'}}, status: 200})
.mockReturnValue({data: {commit: {sha: 'deadbeef'}}, status: 200})

data.environmentObj.stable_branch_used = true

Expand Down Expand Up @@ -700,6 +714,10 @@ test('runs prechecks and finds the PR is behind the stable branch and a noop dep
data.inputs.update_branch = 'force'
data.environmentObj.noop = true

jest.spyOn(isOutdated, 'isOutdated').mockImplementation(() => {
return {outdated: true, branch: 'base-ref'}
})

expect(await prechecks(context, octokit, data)).toStrictEqual({
message:
'### ⚠️ Cannot proceed with deployment\n\n- mergeStateStatus: `BEHIND`\n- update_branch: `force`\n\n> I went ahead and updated your branch with `main` - Please try again once this operation is complete',
Expand Down Expand Up @@ -772,12 +790,16 @@ test('runs prechecks and finds the PR is BEHIND and a noop deploy and it fails t
status: 422
})

jest.spyOn(isOutdated, 'isOutdated').mockImplementation(() => {
return {outdated: true, branch: 'base-ref'}
})

data.environmentObj.noop = true
data.inputs.update_branch = 'force'

expect(await prechecks(context, octokit, data)).toStrictEqual({
message:
'### ⚠️ Cannot proceed with deployment\n\n- update_branch http code: `422`\n- update_branch: `force`\n\n> Failed to update pull request branch with `main`',
'### ⚠️ Cannot proceed with deployment\n\n- update_branch http code: `422`\n- update_branch: `force`\n\n> Failed to update pull request branch with the `base-ref` branch',
status: false
})
})
Expand Down Expand Up @@ -805,6 +827,11 @@ test('runs prechecks and finds the PR is BEHIND and a noop deploy and it hits an
}
}
})

jest.spyOn(isOutdated, 'isOutdated').mockImplementation(() => {
return {outdated: true, branch: 'base-ref'}
})

octokit.rest.pulls.updateBranch = jest.fn().mockReturnValue(null)

data.environmentObj.noop = true
Expand Down Expand Up @@ -844,9 +871,13 @@ test('runs prechecks and finds the PR is BEHIND and a noop deploy and update_bra
data.environmentObj.noop = true
data.inputs.update_branch = 'warn'

jest.spyOn(isOutdated, 'isOutdated').mockImplementation(() => {
return {outdated: true, branch: 'base-ref'}
})

expect(await prechecks(context, octokit, data)).toStrictEqual({
message:
'### ⚠️ Cannot proceed with deployment\n\nYour branch is behind the base branch and will need to be updated before deployments can continue.\n\n- mergeStateStatus: `BEHIND`\n- update_branch: `warn`\n\n> Please ensure your branch is up to date with the `main` branch and try again',
'### ⚠️ Cannot proceed with deployment\n\nYour branch is behind the base branch and will need to be updated before deployments can continue.\n\n- mergeStateStatus: `BEHIND`\n- update_branch: `warn`\n\n> Please ensure your branch is up to date with the `base-ref` branch and try again',
status: false
})
})
Expand Down Expand Up @@ -1017,9 +1048,13 @@ test('runs prechecks and finds the PR is BEHIND and a full deploy and update_bra

data.inputs.update_branch = 'warn'

jest.spyOn(isOutdated, 'isOutdated').mockImplementation(() => {
return {outdated: true, branch: 'base-ref'}
})

expect(await prechecks(context, octokit, data)).toStrictEqual({
message:
'### ⚠️ Cannot proceed with deployment\n\nYour branch is behind the base branch and will need to be updated before deployments can continue.\n\n- mergeStateStatus: `BEHIND`\n- update_branch: `warn`\n\n> Please ensure your branch is up to date with the `main` branch and try again',
'### ⚠️ Cannot proceed with deployment\n\nYour branch is behind the base branch and will need to be updated before deployments can continue.\n\n- mergeStateStatus: `BEHIND`\n- update_branch: `warn`\n\n> Please ensure your branch is up to date with the `base-ref` branch and try again',
status: false
})
})
Expand Down Expand Up @@ -1048,6 +1083,10 @@ test('runs prechecks and finds the PR is behind the stable branch and a full dep
}
})

jest.spyOn(isOutdated, 'isOutdated').mockImplementation(() => {
return {outdated: true, branch: 'base-ref'}
})

octokit.rest.pulls.updateBranch = jest.fn().mockReturnValue({
data: {
message: 'Updating pull request branch.',
Expand Down Expand Up @@ -1817,12 +1856,11 @@ test('runs prechecks and finds the PR is behind the stable branch (BLOCKED) and
},
status: 200
})
octokit.rest.repos.getBranch = jest
.fn()
.mockReturnValueOnce({data: {commit: {sha: 'deadbeef'}}, status: 200})
octokit.rest.repos.compareCommits = jest
.fn()
.mockReturnValueOnce({data: {behind_by: 1}, status: 200})

jest.spyOn(isOutdated, 'isOutdated').mockImplementation(() => {
return {outdated: true, branch: 'base-ref'}
})

octokit.rest.pulls.updateBranch = jest.fn().mockReturnValue({
data: {
message: 'Updating pull request branch.',
Expand Down Expand Up @@ -1879,9 +1917,7 @@ test('runs prechecks and finds the PR is NOT behind the stable branch (BLOCKED)
octokit.rest.repos.getBranch = jest
.fn()
.mockReturnValueOnce({data: {commit: {sha: 'deadbeef'}}, status: 200})
octokit.rest.repos.compareCommits = jest
.fn()
.mockReturnValueOnce({data: {behind_by: 0}, status: 200})

octokit.rest.pulls.updateBranch = jest.fn().mockReturnValue({
data: {
message: 'Updating pull request branch.',
Expand Down Expand Up @@ -1940,9 +1976,6 @@ test('runs prechecks and finds the PR is NOT behind the stable branch (HAS_HOOKS
octokit.rest.repos.getBranch = jest
.fn()
.mockReturnValueOnce({data: {commit: {sha: 'deadbeef'}}, status: 200})
octokit.rest.repos.compareCommits = jest
.fn()
.mockReturnValueOnce({data: {behind_by: 0}, status: 200})
octokit.rest.pulls.updateBranch = jest.fn().mockReturnValue({
data: {
message: 'Updating pull request branch.',
Expand Down
10 changes: 10 additions & 0 deletions __tests__/schemas/action.schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,16 @@ inputs:
default:
type: string
required: true
outdated_mode:
description:
type: string
required: true
required:
type: boolean
required: true
default:
required: true
type: string
required_contexts:
description:
type: string
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ inputs:
description: 'Determine how you want this Action to handle "out-of-date" branches. Available options: "disabled", "warn", "force". "disabled" means that the Action will not care if a branch is out-of-date. "warn" means that the Action will warn the user that a branch is out-of-date and exit without deploying. "force" means that the Action will force update the branch. Note: The "force" option is not recommended due to Actions not being able to re-run CI on commits originating from Actions itself'
required: false
default: "warn"
outdated_mode:
description: 'The mode to use for determining if a branch is up-to-date or not before allowing deployments. This option is closely related to the "update_branch" input option above. There are three available modes to choose from "pr_base", "default_branch", or "strict". The default is "strict" to help ensure that deployments are using the most up-to-date code. Please see the docs/outdated_mode.md document for more details.'
required: false
default: "strict"
required_contexts:
description: 'Manually enforce commit status checks before a deployment can continue. Only use this option if you wish to manually override the settings you have configured for your branch protection settings for your GitHub repository. Default is "false" - Example value: "context1,context2,context3" - In most cases you will not need to touch this option'
required: false
Expand Down
Loading

0 comments on commit fdb7b54

Please sign in to comment.