Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Prevent Naked Deploys #214

Merged
merged 14 commits into from
Oct 6, 2023
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ As seen above, we have two steps. One for a noop deploy, and one for a regular d
| `sticky_locks` | `false` | `"false"` | If set to `"true"`, locks will not be released after a deployment run completes. This applies to both successful, and failed deployments.Sticky locks are also known as ["hubot style deployment locks"](./docs/hubot-style-deployment-locks.md). They will persist until they are manually released by a user, or if you configure [another workflow with the "unlock on merge" mode](./docs/unlock-on-merge.md) to remove them automatically on PR merge. |
| `sticky_locks_for_noop` | `false` | `"false"` | If set to `"true"`, then sticky_locks will also be used for noop deployments. This can be useful in some cases but it often leads to locks being left behind when users test noop deployments. |
| `allow_sha_deployments` | `false` | `"false"` | If set to `"true"`, then you can deploy a specific sha instead of a branch. Example: `".deploy 1234567890abcdef1234567890abcdef12345678 to production"` - This is dangerous and potentially unsafe, [view the docs](docs/sha-deployments.md) to learn more |
| `disable_naked_commands` | `false` | `"false"` | If set to `"true"`, then naked commands will be disabled. Example: `.deploy` will not trigger a deployment. Instead, you must use `.deploy to production` to trigger a deployment. This is useful if you want to prevent accidental deployments from happening. View the [docs](docs/naked-commands.md) to learn more |

## Outputs 📤

Expand Down
157 changes: 157 additions & 0 deletions __tests__/functions/naked-command-check.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import * as core from '@actions/core'
import {nakedCommandCheck} from '../../src/functions/naked-command-check'
import {COLORS} from '../../src/functions/colors'

const docs =
'https://github.com/github/branch-deploy/blob/main/docs/naked-commands.md'
const warningMock = jest.spyOn(core, 'warning')

var context
var octokit
var triggers
var param_separator

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

process.env.INPUT_GLOBAL_LOCK_FLAG = '--global'

triggers = ['.deploy', '.noop', '.lock', '.unlock', '.wcid']
param_separator = '|'

context = {
repo: {
owner: 'corp',
repo: 'test'
},
issue: {
number: 1
},
payload: {
comment: {
id: '1'
}
}
}

octokit = {
rest: {
reactions: {
createForIssueComment: jest.fn().mockReturnValueOnce({
data: {}
})
},
issues: {
createComment: jest.fn().mockReturnValueOnce({
data: {}
})
}
}
}
})

test('checks the command and finds that it is naked', async () => {
const body = '.deploy'
expect(
await nakedCommandCheck(body, param_separator, triggers, octokit, context)
).toBe(true)
expect(warningMock).toHaveBeenCalledWith(
`🩲 naked commands are ${COLORS.warning}not${COLORS.reset} allowed based on your configuration: ${COLORS.highlight}${body}${COLORS.reset}`
)
expect(warningMock).toHaveBeenCalledWith(
`📚 view the documentation around ${COLORS.highlight}naked commands${COLORS.reset} to learn more: ${docs}`
)
})

test('checks the command and finds that it is naked (noop)', async () => {
const body = '.noop'
expect(
await nakedCommandCheck(body, param_separator, triggers, octokit, context)
).toBe(true)
})

test('checks the command and finds that it is naked (lock)', async () => {
const body = '.lock'
expect(
await nakedCommandCheck(body, param_separator, triggers, octokit, context)
).toBe(true)
})

test('checks the command and finds that it is naked (lock) with a reason', async () => {
const body = '.lock --reason I am testing a big change'
expect(
await nakedCommandCheck(body, param_separator, triggers, octokit, context)
).toBe(true)
})

test('checks the command and finds that it is NOT naked (lock) with a reason', async () => {
const body = '.lock production --reason I am testing a big change'
expect(
await nakedCommandCheck(body, param_separator, triggers, octokit, context)
).toBe(false)
})

test('checks the command and finds that it is naked (unlock)', async () => {
const body = '.unlock'
expect(
await nakedCommandCheck(body, param_separator, triggers, octokit, context)
).toBe(true)
})

test('checks the command and finds that it is NOT naked because it is global', async () => {
const body = '.unlock --global'
expect(
await nakedCommandCheck(body, param_separator, triggers, octokit, context)
).toBe(false)
})

test('checks the command and finds that it is naked (alias)', async () => {
const body = '.wcid'
expect(
await nakedCommandCheck(body, param_separator, triggers, octokit, context)
).toBe(true)
})

test('checks the command and finds that it is naked (whitespaces)', async () => {
const body = '.deploy '
expect(
await nakedCommandCheck(body, param_separator, triggers, octokit, context)
).toBe(true)
})

test('checks the command and finds that it is not naked', async () => {
const body = '.deploy production'
expect(
await nakedCommandCheck(body, param_separator, triggers, octokit, context)
).toBe(false)
})

test('checks the command and finds that it is not naked with "to"', async () => {
const body = '.deploy to production'
expect(
await nakedCommandCheck(body, param_separator, triggers, octokit, context)
).toBe(false)
})

test('checks the command and finds that it is not naked with an alias lock command', async () => {
const body = '.wcid staging '
expect(
await nakedCommandCheck(body, param_separator, triggers, octokit, context)
).toBe(false)
})

test('checks the command and finds that it is naked with params', async () => {
const body = '.deploy | cpus=1 memory=2g,3g env=production'
expect(
await nakedCommandCheck(body, param_separator, triggers, octokit, context)
).toBe(true)
})

test('checks the command and finds that it is naked with params and extra whitespace', async () => {
const body = '.deploy | cpus=1 memory=2g,3g env=production'
expect(
await nakedCommandCheck(body, param_separator, triggers, octokit, context)
).toBe(true)
})
14 changes: 14 additions & 0 deletions __tests__/main.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as actionStatus from '../src/functions/action-status'
import * as github from '@actions/github'
import * as core from '@actions/core'
import * as isDeprecated from '../src/functions/deprecated-checks'
import * as nakedCommandCheck from '../src/functions/naked-command-check'
import {COLORS} from '../src/functions/colors'

const setOutputMock = jest.spyOn(core, 'setOutput')
Expand Down Expand Up @@ -55,6 +56,7 @@ beforeEach(() => {
process.env.INPUT_STICKY_LOCKS = 'false'
process.env.INPUT_STICKY_LOCKS_FOR_NOOP = 'false'
process.env.INPUT_ALLOW_SHA_DEPLOYMENTS = 'false'
process.env.INPUT_DISABLE_NAKED_COMMANDS = 'false'

github.context.payload = {
issue: {
Expand Down Expand Up @@ -581,6 +583,18 @@ test('runs with the deprecated noop input', async () => {
expect(saveStateMock).toHaveBeenCalledWith('bypass', 'true')
})

test('runs with a naked command when naked commands are NOT allowed', async () => {
process.env.INPUT_DISABLE_NAKED_COMMANDS = 'true'
github.context.payload.comment.body = '.deploy'
jest.spyOn(nakedCommandCheck, 'nakedCommandCheck').mockImplementation(() => {
return true
})
expect(await run()).toBe('safe-exit')
expect(saveStateMock).toHaveBeenCalledWith('isPost', 'true')
expect(saveStateMock).toHaveBeenCalledWith('actionsToken', 'faketoken')
expect(saveStateMock).toHaveBeenCalledWith('bypass', 'true')
})

test('successfully runs the action after trimming the body', async () => {
jest.spyOn(prechecks, 'prechecks').mockImplementation(() => {
return {
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 @@ -360,6 +360,16 @@ inputs:
default:
required: true
type: string
disable_naked_commands:
description:
type: string
required: true
required:
type: boolean
required: true
default:
required: true
type: string

# outputs section
outputs:
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ inputs:
description: 'If set to "true", then you can deploy a specific sha instead of a branch. Example: ".deploy 1234567890abcdef1234567890abcdef12345678 to production" - This is dangerous and potentially unsafe, view the docs to learn more: https://github.com/github/branch-deploy/blob/main/docs/sha-deployments.md'
required: false
default: "false"
disable_naked_commands:
description: 'If set to "true", then naked commands will be disabled. Example: ".deploy" will not trigger a deployment. Instead, you must use ".deploy to production" to trigger a deployment. This is useful if you want to prevent accidental deployments from happening. Read more about naked commands here: https://github.com/github/branch-deploy/blob/main/docs/naked-commands.md'
required: false
default: "false"
outputs:
continue:
description: 'The string "true" if the deployment should continue, otherwise empty - Use this to conditionally control if your deployment should proceed or not'
Expand Down
Loading