Skip to content

Commit

Permalink
Merge pull request #214 from github/prevent-naked-deploys
Browse files Browse the repository at this point in the history
Prevent Naked Deploys
  • Loading branch information
GrantBirki authored Oct 6, 2023
2 parents 5498c78 + 6cb58f4 commit ff4d3f6
Show file tree
Hide file tree
Showing 10 changed files with 494 additions and 12 deletions.
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

0 comments on commit ff4d3f6

Please sign in to comment.