How to approve production deploys in Slack
This guide builds a workflow that asks for approval in Slack before a production deploy, then runs the deploy. It takes one workflow file and the Slack connection in Settings → Approvals.
What you build
- A
deployworkflow with one approval step. - Requests in a Slack channel with Approve and Deny buttons.
- A deploy that runs only after a yes: a command on your runner, or a GitHub Actions run.
Before you start
- A ShiftLabs workspace. Free works. Start free.
- The CLI:
npx shiftlabs@latest. See Install. - Admin access to your Slack workspace, to add the app.
- For the command route, a runner with a kubeconfig that can deploy. For the GitHub route, the
github-actions-runbooksetup.
1. Connect Slack
- In Settings → Approvals, an admin selects Connect Slack and allows the app.
- Pick the channel for approval requests. For a private channel, run
/invite @shiftlabsthere first. - Each approver selects Link my Slack account and signs in with Slack.
Choose a channel only approvers read.
2. Write the workflow
// workflows/deploy.ts
import { workflow } from "shiftlabs";
export default workflow<{ service: string; version: string }>(async ({ payload, step }) => {
const decision = await step.waitForApproval("deploy", {
message: `Deploy ${payload.service} ${payload.version} to production?`,
timeout: "4 hours",
approvers: ["role:admin", "role:manager"],
});
if (!decision.approved) return { deployed: false, note: decision.note };
await step.exec("deploy", {
command: ["kubectl", "set", "image", `deployment/${payload.service}`, `app=${payload.version}`, "-n", "apps"],
});
return { deployed: true, by: decision.by };
});
In Python:
from shiftlabs import workflow
@workflow
async def deploy(ctx):
service, version = ctx.payload["service"], ctx.payload["version"]
decision = await ctx.step.wait_for_approval(
"deploy",
message=f"Deploy {service} {version} to production?",
timeout="4 hours",
approvers=["role:admin", "role:manager"],
)
if not decision["approved"]:
return {"deployed": False, "note": decision["note"]}
await ctx.step.exec(
"deploy",
command=["kubectl", "set", "image", f"deployment/{service}", f"app={version}", "-n", "apps"],
)
return {"deployed": True, "by": decision["by"]}
The kubectl line is an example. Swap in your own deploy command.
3. Deploy it
npx shiftlabs deploy
Start a run with Run now in the dashboard, the API, a webhook from your CI, or an agent's tool call.
What the approver sees
The request arrives in the channel with the workflow's message and Approve and Deny buttons. The approver can write a note. The message updates when the request is answered anywhere: in Slack, in the dashboard or with the API. A workflow's message never shows the value of a secret your workspace stores.
When the answer is no, or nobody answers
- Denied:
decision.approvedisfalse, and the workflow returns without deploying. The note comes back with it. - No answer:
the request runs out after
timeout, 72 hours by default, and counts as denied.byisnull.
Require two approvers
Pass approvals: 2 to need two different members. The run waits for the last approval. An admin can also set a workspace minimum. See Two approvers.
Deploy through GitHub Actions instead
The github-actions-runbook template starts .github/workflows/shiftlabs-runbook.yml in your repository after the approval, then waits for a signed result. Your deploy credentials stay in GitHub secrets.
npx shiftlabs init --template github-actions-runbook
What it costs
On Free, each deploy is one run of the 1,000 a day. Waiting for an approval is not a step, and it counts toward the run's lifetime of 168 hours. A runner command uses no CPU time of your plan. Free allows commands up to 30 minutes; Pro allows 12 hours. See Pricing.