Skip to content
Open
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ As seen above, we have two steps. One for a noop deploy, and one for a regular d
| `deploy_message_path` | `false` | `".github/deployment_message.md"` | The path to a markdown file which is used as a template for custom deployment messages. Example: `".github/deployment_message.md"` |
| `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. |
| `disable_lock` | `false` | `"false"` | If set to `"true"`, all deployment locking is disabled. Useful for workflows where concurrent deployments are safe (e.g. iOS/Android builds uploaded to TestFlight or the Play Store). When disabled, `.lock` and `.unlock` commands will return an informational message instead of modifying lock state. See the [disable lock](docs/locks.md#disabling-locks) documentation for more details. |
| `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 |
| `successful_deploy_labels` | `false` | `""` | A comma separated list of labels to add to the pull request when a deployment is successful. Example: `"deployed,success"` |
Expand Down
11 changes: 11 additions & 0 deletions __tests__/functions/post-deploy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -668,3 +668,14 @@ test('fails due to no noop', async () => {
expect(e.message).toBe('no noop value provided')
}
})

test('skips lock check and release when disable_lock is true', async () => {
const lockSpy = vi.spyOn(lock, 'lock')
const unlockSpy = vi.spyOn(unlock, 'unlock')

data.disable_lock = true

expect(await postDeploy(context, octokit, data)).toBe('success')
expect(lockSpy).not.toHaveBeenCalled()
expect(unlockSpy).not.toHaveBeenCalled()
})
66 changes: 66 additions & 0 deletions __tests__/main.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ beforeEach(() => {
process.env.INPUT_UNLOCK_ON_MERGE_MODE = 'false'
process.env.INPUT_STICKY_LOCKS = 'false'
process.env.INPUT_STICKY_LOCKS_FOR_NOOP = 'false'
process.env.INPUT_DISABLE_LOCK = 'false'
process.env.INPUT_ALLOW_SHA_DEPLOYMENTS = 'false'
process.env.INPUT_DISABLE_NAKED_COMMANDS = 'false'
process.env.INPUT_OUTDATED_MODE = 'default_branch'
Expand Down Expand Up @@ -1476,3 +1477,68 @@ test('stores params and parsed params into context with complex params', async (
expect(setOutputMock).toHaveBeenCalledWith('params', params)
expect(setOutputMock).toHaveBeenCalledWith('parsed_params', parsed_params)
})

test('successfully runs the action with disable_lock enabled - skips lock acquisition on deploy', async () => {
process.env.INPUT_DISABLE_LOCK = 'true'

// clear the mock set up in beforeEach so we can assert it was not called
const lockSpy = vi.spyOn(lock, 'lock').mockClear()

expect(await run()).toBe('success')
expect(lockSpy).not.toHaveBeenCalled()
expect(setOutputMock).toHaveBeenCalledWith('triggered', 'true')
expect(setOutputMock).toHaveBeenCalledWith('type', 'deploy')
expect(saveStateMock).toHaveBeenCalledWith('disable_lock', true)
})

test('returns safe-exit and posts informational comment when disable_lock is set and .lock is triggered', async () => {
process.env.INPUT_DISABLE_LOCK = 'true'

github.context.payload.comment.body = '.lock'

vi.spyOn(validPermissions, 'validPermissions').mockImplementation(() => {
return true
})
const actionStatusSpy = vi
.spyOn(actionStatus, 'actionStatus')
.mockImplementation(() => {
return undefined
})

expect(await run()).toBe('safe-exit')
expect(setOutputMock).toHaveBeenCalledWith('type', 'lock')
expect(actionStatusSpy).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
123,
'🔓 Deployment locking is disabled for this Action — lock/unlock commands have no effect.',
true
)
expect(saveStateMock).toHaveBeenCalledWith('bypass', 'true')
})

test('returns safe-exit and posts informational comment when disable_lock is set and .unlock is triggered', async () => {
process.env.INPUT_DISABLE_LOCK = 'true'

github.context.payload.comment.body = '.unlock'

vi.spyOn(validPermissions, 'validPermissions').mockImplementation(() => {
return true
})
const actionStatusSpy = vi
.spyOn(actionStatus, 'actionStatus')
.mockImplementation(() => {
return undefined
})

expect(await run()).toBe('safe-exit')
expect(setOutputMock).toHaveBeenCalledWith('type', 'unlock')
expect(actionStatusSpy).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
123,
'🔓 Deployment locking is disabled for this Action — lock/unlock commands have no effect.',
true
)
expect(saveStateMock).toHaveBeenCalledWith('bypass', 'true')
})
10 changes: 10 additions & 0 deletions __tests__/schemas/action.schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,16 @@ inputs:
default:
required: true
type: string
disable_lock:
description:
type: string
required: true
required:
type: boolean
required: true
default:
required: true
type: string
allow_sha_deployments:
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 @@ -149,6 +149,10 @@ inputs:
description: '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.'
required: false
default: "false"
disable_lock:
description: 'If set to "true", all deployment locking is disabled. Useful for workflows where concurrent deployments are safe (e.g. iOS/Android builds uploaded to TestFlight). When disabled, .lock and .unlock commands will return an informational message instead of modifying lock state.'
required: false
default: "false"
allow_sha_deployments:
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
Expand Down
107 changes: 64 additions & 43 deletions dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions docs/locks.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,27 @@ Removing the global deploy lock:

![remove-global-deploy-lock](https://user-images.githubusercontent.com/23362539/224514485-e60605fd-0918-466e-9aab-7597fa32e7d9.png)

## Disabling Locks

For some workflows, deployment locking is simply not needed. A good example is mobile CI/CD pipelines that upload build artifacts to TestFlight or the Google Play Store — each upload is additive and independent, so two concurrent deployments can never conflict with each other. In these cases, requiring a lock before every deploy adds unnecessary friction.

You can completely disable all locking logic by setting the `disable_lock` input to `"true"`:

```yaml
- uses: github/branch-deploy@vX
with:
disable_lock: "true"
```

When `disable_lock` is enabled:

- Lock acquisition is **skipped** before a deployment starts — no lock branch is ever created
- The post-deploy lock release step is **skipped**
- `.lock` and `.unlock` commands respond with an informational comment and exit cleanly — lock state is never modified

> [!NOTE]
> `disable_lock` disables locking entirely. If you want locks to persist across deployments (rather than be released automatically), see [hubot-style sticky locks](./hubot-style-deployment-locks.md) instead.

## Actions Concurrency

> Note: Using the locking mechanism included in this Action (above) is highly recommended over Actions concurrency. The section below will be included anyways should you have a valid reason to use it instead of the deploy lock features this Action provides
Expand Down
2 changes: 2 additions & 0 deletions src/functions/inputs.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function getInputs() {
const permissions = stringToArray(core.getInput('permissions'))
const sticky_locks = core.getBooleanInput('sticky_locks')
const sticky_locks_for_noop = core.getBooleanInput('sticky_locks_for_noop')
const disable_lock = core.getBooleanInput('disable_lock')
const allow_sha_deployments = core.getBooleanInput('allow_sha_deployments')
const disable_naked_commands = core.getBooleanInput('disable_naked_commands')
const enforced_deployment_order = stringToArray(
Expand Down Expand Up @@ -123,6 +124,7 @@ export function getInputs() {
param_separator: param_separator,
sticky_locks: sticky_locks,
sticky_locks_for_noop: sticky_locks_for_noop,
disable_lock: disable_lock,
enforced_deployment_order: enforced_deployment_order,
commit_verification: commit_verification,
ignored_checks: ignored_checks,
Expand Down
Loading