# Deployment Playbook documentation # Getting started Deployment Playbook turns your release checklist into a **plan** with **phases** (Before, During, After deployment), **approvals** and an audit trail, and lets your CI/CD pipeline wait on it through a simple **gate API**. ## 1. Create a workspace 1. [Sign up](https://deployment-playbook.com/signup) with email + password (or Google, GitHub or Microsoft when enabled). With email + password, open the verification link we email you first. 2. Name your organisation. You become the workspace **Admin** with every role, and a default template *Standard deployment* is created for you. 3. Invite your team from **Members & roles**. A person can hold several roles (see [Concepts](https://deployment-playbook.com/docs/concepts)). ## 2. Look at the template Open **Templates → Standard deployment**. Each step belongs to a phase, is *required* or *optional*, and can need **four-eyes** (two different approvers). Edit the steps and save to create a new template version. Plans always keep a snapshot of the version they were created from. Copy the **Template ID** at the top of the page: you use it from CI (`POST /api/plans`) and from the [MCP server](https://deployment-playbook.com/docs/use-with-ai). ## 3. Create a plan Either click **New plan** in the app, or let CI create it from a git tag (recommended, see [Create a plan on a git tag](https://deployment-playbook.com/docs/recipes#create-a-plan-on-a-git-tag)). A new plan is **Proposed** and must be approved by an Approver (two with plan-wide four-eyes) before any step can be worked on. ## 4. Gate your pipeline 1. As Admin, create an **API token** on **Connections & webhooks**. It is shown once; store it as a CI secret. 2. Let each pipeline stage poll the gate until it returns `allow: true`: ```bash curl -fsS -H "Authorization: Bearer $PLAYBOOK_API_TOKEN" \ "https://deployment-playbook.com/api/gate/$PLAN_ID?phase=during" | jq -e '.allow' ``` 3. Approve steps in the app and watch the pipeline continue. A complete, copy-paste GitHub Actions example lives in the public repository [Generation-Y-BV/deployment-playbook-example](https://github.com/Generation-Y-BV/deployment-playbook-example). ## Next - [Concepts](https://deployment-playbook.com/docs/concepts): roles, phases, approvals, four-eyes and statuses. - [Recipes](https://deployment-playbook.com/docs/recipes): GitHub Actions, GitLab CI, Azure DevOps, webhooks. - [API reference](https://deployment-playbook.com/docs/api). - [Use with AI](https://deployment-playbook.com/docs/use-with-ai): the MCP server and `llms.txt`. --- # Concepts ## Workspaces A workspace is your organisation (or team). Templates, plans, members, API tokens, webhooks, SSO and the audit log all belong to one workspace and are never shared between workspaces. A user can be a member of several workspaces. ## Roles A member can hold **several roles** at once. | Role | Can | |---|---| | **Viewer** | Read-only: view plans, steps and status. Viewers are free (they don't count as seats). | | **Approver** | Approve or reject plans and steps; create plans from a template; work on steps; mark a plan rolled back. | | **Template manager** | Create, edit and archive templates; create plans; work on steps. | | **Admin** | Invite people, assign roles, manage connections (API tokens, webhooks), SSO and billing; create plans; mark rolled back. **An Admin cannot approve unless also Approver.** | Only **Approvers** can approve. Approvals always stay with people: neither the API nor the MCP server can approve. ## Templates A template is a reusable checklist: an ordered list of steps per phase. Saving a template creates a new **version**; plans copy (snapshot) the steps of the current version, so later template edits never change running plans. Archived templates can't be used for new plans. Every template has a stable **Template ID** (shown with a copy button) that stays the same when you rename it. ## Plans A plan is one release, created from a template, with a name, an **environment** (free text, e.g. `production`, `staging`) and optional CI metadata (repository, ref/tag, commit, run link) when created through the API. ## Phases Every step belongs to one of three phases that run **in order**: 1. **Before deployment** (`before`): preparation, reviews, rollback plan, backups. 2. **During deployment** (`during`): go/no-go, trigger the deploy, watch it, smoke tests. 3. **After deployment** (`after`): health checks, release notes, sign-off. A phase is **unlocked** once every step of all earlier phases is resolved (approved, or skipped when optional). ## Steps: required and optional - **Required** steps must be approved. They can never be skipped. - **Optional** steps can be approved or **skipped**; a skip is proposed with a reason and needs the same approvals as an approval would. Working a step: someone with a working role submits **evidence** (a link or note), then Approvers approve or reject it (a rejection needs a comment and sends the step back). ## Approvals and four-eyes - **Plan approval**: a proposed plan needs 1 approval (2 with plan-wide four-eyes) before work starts. Rejecting needs a comment; the plan can then be fixed and re-proposed (a new round). - **Plan-wide four-eyes**: every approval on the plan (the plan itself and every step) needs **two different Approvers**, and the person who proposed the plan cannot approve it. - **Per-step four-eyes**: when plan-wide four-eyes is off, individual steps can still require two approvers (set per step in the template, and adjustable when creating a plan). The person who submitted the evidence for such a step can't approve it. ## Plan statuses | Status | API `planStatus` | Meaning | |---|---|---| | Proposed | `waiting_for_approval` | Waiting for the plan to be approved. | | Rejected | `rejected` | The plan was rejected; fix and re-propose it. | | In progress | `in_progress` (or `waiting_for_approval` while a step waits for approval, `rejected` when a step is rejected) | Steps are being worked on. | | Done | `done` | Every step is resolved. Set automatically when the last step is approved or skipped. | | Rolled back | `rolled_back` | Someone marked the deployment as rolled back (with a reason). | ## Audit log Every decision, change, token, SSO configuration and sign-in via SSO is written to the workspace audit log with who, what and when. --- # Recipes All examples use three settings in your CI system: | Name | Kind | Value | |---|---|---| | `PLAYBOOK_URL` | variable | `https://deployment-playbook.com` | | `PLAYBOOK_TEMPLATE_ID` | variable | the Template ID (Templates → your template → *Copy*) | | `PLAYBOOK_API_TOKEN` | **secret** | a workspace API token (Admin → Connections & webhooks) | ## How the gate maps to pipeline jobs `GET /api/gate/?phase=

` answers *"may work in phase `p` start?"*. So: | Job waits until… | Poll | |---|---| | the plan is approved (nothing else) | `?phase=before` | | every **Before** step is done | `?phase=during` | | every **During** step is done | `?phase=after` | | every **After** step is done (plan complete) | `?phase=after` **and** `planStatus == "done"` | `?phase=after` becomes `allow: true` as soon as Before and During are done, *before* any After step is approved. Use `planStatus == "done"` for a final job that must only turn green when the whole plan is complete. ## Create a plan on a git tag `POST /api/plans` is **idempotent per repository + ref**: re-running the pipeline for the same tag returns the same plan (`200`, `created: false`) instead of creating a duplicate. ```bash curl -fsS -X POST "$PLAYBOOK_URL/api/plans" \ -H "Authorization: Bearer $PLAYBOOK_API_TOKEN" -H "Content-Type: application/json" \ -d "{\"templateId\":\"$PLAYBOOK_TEMPLATE_ID\",\"ref\":\"v1.4.0\",\"repository\":\"acme/shop\",\"commitSha\":\"$(git rev-parse HEAD)\",\"environment\":\"production\"}" # => {"id":"cm…","name":"Release v1.4.0","status":"PROPOSED","planUrl":"…","gateUrl":"…/api/gate/cm…","created":true} ``` ### A reusable polling script Save as `scripts/wait-for-playbook.sh` (bash + curl + jq). It fails closed: a non-200 response or invalid JSON counts as "not allowed" and polling continues until the timeout. ```bash #!/usr/bin/env bash # usage: wait-for-playbook.sh [require-done] [timeout-minutes] set -uo pipefail plan="$1"; phase="$2"; require_done="${3:-false}"; timeout="${4:-60}" deadline=$(( $(date +%s) + timeout * 60 )) while :; do body=$(curl -sS --max-time 30 -w '\n%{http_code}' -H "Authorization: Bearer $PLAYBOOK_API_TOKEN" \ "$PLAYBOOK_URL/api/gate/$plan?phase=$phase" 2>/dev/null) || body=$'\n000' code=${body##*$'\n'}; json=${body%$'\n'*} allow=false; status="" if [ "$code" = 200 ] && echo "$json" | jq -e 'type=="object"' >/dev/null 2>&1; then allow=$(echo "$json" | jq -r '.allow == true'); status=$(echo "$json" | jq -r '.planStatus // ""') echo "allow=$allow planStatus=$status reason=$(echo "$json" | jq -r .reason) $(echo "$json" | jq -r .planUrl)" else echo "HTTP $code (not allowed)"; fi if [ "$allow" = true ] && { [ "$require_done" != true ] || [ "$status" = done ]; }; then exit 0; fi [ "$status" = rolled_back ] && { echo "plan rolled back"; exit 1; } [ "$(date +%s)" -ge "$deadline" ] && { echo "timed out"; exit 1; } sleep 15 done ``` ### GitHub Actions The complete, maintained version (composite actions `create-playbook-plan` and `wait-for-playbook`, job summary, Vercel bypass support) is in [Generation-Y-BV/deployment-playbook-example](https://github.com/Generation-Y-BV/deployment-playbook-example). The shape: ```yaml name: Deploy on: push: tags: ['v*'] jobs: create-plan: runs-on: ubuntu-latest outputs: plan-id: ${{ steps.plan.outputs.id }} steps: - id: plan env: PLAYBOOK_URL: ${{ vars.PLAYBOOK_URL }} TEMPLATE_ID: ${{ vars.PLAYBOOK_TEMPLATE_ID }} TOKEN: ${{ secrets.PLAYBOOK_API_TOKEN }} run: | body=$(jq -n --arg t "$TEMPLATE_ID" --arg r "$GITHUB_REF_NAME" --arg repo "$GITHUB_REPOSITORY" --arg sha "$GITHUB_SHA" \ --arg run "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ '{templateId:$t, ref:$r, repository:$repo, commitSha:$sha, runUrl:$run}') res=$(curl -fsS -X POST "$PLAYBOOK_URL/api/plans" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$body") echo "id=$(echo "$res" | jq -r .id)" >> "$GITHUB_OUTPUT" echo "Plan: $(echo "$res" | jq -r .planUrl)" >> "$GITHUB_STEP_SUMMARY" before: # waits until every Before step is done needs: create-plan runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: scripts/wait-for-playbook.sh "${{ needs.create-plan.outputs.plan-id }}" during env: { PLAYBOOK_URL: '${{ vars.PLAYBOOK_URL }}', PLAYBOOK_API_TOKEN: '${{ secrets.PLAYBOOK_API_TOKEN }}' } deploy: # runs in parallel with "during" needs: [create-plan, before] runs-on: ubuntu-latest steps: - run: echo "deploy here" during: # waits until every During step is done needs: [create-plan, before] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: scripts/wait-for-playbook.sh "${{ needs.create-plan.outputs.plan-id }}" after env: { PLAYBOOK_URL: '${{ vars.PLAYBOOK_URL }}', PLAYBOOK_API_TOKEN: '${{ secrets.PLAYBOOK_API_TOKEN }}' } after-deployment: # green only when the After steps are done (planStatus == done) needs: [create-plan, deploy, during] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: scripts/wait-for-playbook.sh "${{ needs.create-plan.outputs.plan-id }}" after true env: { PLAYBOOK_URL: '${{ vars.PLAYBOOK_URL }}', PLAYBOOK_API_TOKEN: '${{ secrets.PLAYBOOK_API_TOKEN }}' } ``` ### GitLab CI Store `PLAYBOOK_API_TOKEN` as a **masked, protected** CI/CD variable; `PLAYBOOK_URL` and `PLAYBOOK_TEMPLATE_ID` as variables. ```yaml stages: [plan, before, deploy, after] default: image: alpine:3.20 before_script: [apk add --no-cache bash curl jq] workflow: rules: - if: $CI_COMMIT_TAG =~ /^v/ create-plan: stage: plan script: - | body=$(jq -n --arg t "$PLAYBOOK_TEMPLATE_ID" --arg r "$CI_COMMIT_TAG" --arg repo "$CI_PROJECT_PATH" \ --arg sha "$CI_COMMIT_SHA" --arg run "$CI_PIPELINE_URL" '{templateId:$t, ref:$r, repository:$repo, commitSha:$sha, runUrl:$run}') res=$(curl -fsS -X POST "$PLAYBOOK_URL/api/plans" -H "Authorization: Bearer $PLAYBOOK_API_TOKEN" -H "Content-Type: application/json" -d "$body") echo "PLAN_ID=$(echo "$res" | jq -r .id)" > plan.env echo "Plan: $(echo "$res" | jq -r .planUrl)" artifacts: reports: dotenv: plan.env before: stage: before timeout: 2h script: [bash scripts/wait-for-playbook.sh "$PLAN_ID" during] deploy: # same stage as "during" => runs in parallel stage: deploy script: [echo "deploy here"] during: stage: deploy timeout: 2h script: [bash scripts/wait-for-playbook.sh "$PLAN_ID" after] after-deployment: stage: after timeout: 2h script: [bash scripts/wait-for-playbook.sh "$PLAN_ID" after true] ``` ### Azure DevOps Pipelines Add `PLAYBOOK_API_TOKEN` as a **secret** pipeline variable (or variable group) and `PLAYBOOK_URL`, `PLAYBOOK_TEMPLATE_ID` as normal variables. Secret variables must be mapped into `env` explicitly. ```yaml trigger: tags: include: ['v*'] pool: vmImage: ubuntu-latest stages: - stage: plan jobs: - job: create_plan steps: - bash: | body=$(jq -n --arg t "$(PLAYBOOK_TEMPLATE_ID)" --arg r "$(Build.SourceBranchName)" --arg repo "$(Build.Repository.Name)" \ --arg sha "$(Build.SourceVersion)" --arg run "$(System.CollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId)" \ '{templateId:$t, ref:$r, repository:$repo, commitSha:$sha, runUrl:$run}') res=$(curl -fsS -X POST "$(PLAYBOOK_URL)/api/plans" -H "Authorization: Bearer $PLAYBOOK_API_TOKEN" -H "Content-Type: application/json" -d "$body") echo "##vso[task.setvariable variable=planId;isOutput=true]$(echo "$res" | jq -r .id)" echo "Plan: $(echo "$res" | jq -r .planUrl)" name: plan env: { PLAYBOOK_API_TOKEN: $(PLAYBOOK_API_TOKEN) } - stage: before dependsOn: plan variables: { planId: $[ stageDependencies.plan.create_plan.outputs['plan.planId'] ] } jobs: - job: wait timeoutInMinutes: 120 steps: - bash: bash scripts/wait-for-playbook.sh "$(planId)" during env: { PLAYBOOK_URL: $(PLAYBOOK_URL), PLAYBOOK_API_TOKEN: $(PLAYBOOK_API_TOKEN) } - stage: deploy_and_during dependsOn: [plan, before] variables: { planId: $[ stageDependencies.plan.create_plan.outputs['plan.planId'] ] } jobs: - job: deploy # jobs in one stage run in parallel steps: [ { bash: echo "deploy here" } ] - job: during timeoutInMinutes: 120 steps: - bash: bash scripts/wait-for-playbook.sh "$(planId)" after env: { PLAYBOOK_URL: $(PLAYBOOK_URL), PLAYBOOK_API_TOKEN: $(PLAYBOOK_API_TOKEN) } - stage: after_deployment dependsOn: [plan, deploy_and_during] variables: { planId: $[ stageDependencies.plan.create_plan.outputs['plan.planId'] ] } jobs: - job: wait timeoutInMinutes: 120 steps: - bash: bash scripts/wait-for-playbook.sh "$(planId)" after true env: { PLAYBOOK_URL: $(PLAYBOOK_URL), PLAYBOOK_API_TOKEN: $(PLAYBOOK_API_TOKEN) } ``` Alternatively, gate an Azure DevOps **environment** with *Approvals and checks → Invoke REST API*: URL `$(PLAYBOOK_URL)/api/gate/?phase=during`, header `Authorization: Bearer `, success criteria `eq(root['allow'], true)`. ## Gate a deploy per phase Use the table at the top: `phase=during` before you deploy (Before phase done), `phase=after` once the During checks are done, and `phase=after` + `planStatus == "done"` for the final job. ## Deploy and During in parallel Start the `deploy` job and a `during` job that polls `?phase=after` at the same time, both after the `before` job. Your deploy runs while people tick off the During steps (watch the rollout, smoke tests); the final job needs both. ## Final job green only when After is done Poll `?phase=after` and require `planStatus == "done"` (see the script's `require-done` argument). A plan becomes `done` automatically when its last step is approved or skipped. Fail the job if `planStatus` becomes `rolled_back`. ## Gate on a single step (`&step=`) `?step=` additionally requires every **earlier step in the same phase** to be resolved. Example: let the deploy job wait for everything before *Trigger the deploy* (so the *Go or no-go decision* must be approved): ```bash curl -fsS -H "Authorization: Bearer $PLAYBOOK_API_TOKEN" \ "$PLAYBOOK_URL/api/gate/$PLAN_ID?step=$TRIGGER_STEP_ID" | jq -e .allow ``` Step ids are shown in the plan's webhook payloads (`step.id`) and via the MCP tool `get_plan`. ## Environments `environment` is free text on each plan (`production`, `staging`, `eu-prod`, …). Create one plan per environment you release to; with the API, pass `environment` and make the `ref` unique per environment if you want separate plans for the same tag (e.g. `v1.4.0@staging`), because idempotency is keyed on repository + ref. ## Idempotent re-runs Re-running a pipeline for the same tag calls `POST /api/plans` with the same `repository` + `ref` and gets the existing plan back (`200`, `created: false`), so approvals already given are kept. To start over, create a new tag (or pass a different `ref`). ## Status webhooks (outgoing) Set a webhook URL on **Connections & webhooks**. Every plan/step status change is POSTed as JSON with headers: - `X-Playbook-Event`: e.g. `plan.approved`, `step.approved`, `plan.done` - `X-Playbook-Timestamp`: unix seconds - `X-Playbook-Signature-256`: `sha256=` + hex HMAC-SHA256 of `.` with the webhook secret Verify (Node.js): ```js import { createHmac, timingSafeEqual } from "node:crypto"; export function verifyPlaybookWebhook(rawBody, headers, secret) { const ts = headers["x-playbook-timestamp"]; const sig = headers["x-playbook-signature-256"] ?? ""; if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay window const expected = "sha256=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex"); return sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); } ``` Payload fields: `event`, `status`, `planStatus`, `phase`, `steps {approved, skipped, total}`, `plan {id, name, environment, fourEyes, status}`, `step {…}` (for step events), `signedOffBy[]`, `planUrl`, `workspace {id, slug}`, `timestamp`. Deliveries are logged on the Connections page; failures never block approvals. --- # API reference Base URL: `https://deployment-playbook.com` (or your own host). All requests and responses are JSON. ## Authentication: workspace API tokens - An **Admin** creates tokens on **Connections & webhooks** (`/w//connections`). The token (`dpk_…`) is shown **once**; only a SHA-256 hash is stored. Tokens can be revoked at any time; the page shows when each was last used. - Send it as `Authorization: Bearer `. - A token belongs to one workspace and can only see that workspace's templates and plans. - Plans created with a token are created **on behalf of the Admin who created the token**: that person must still be a member with a role that may create plans (Approver, Template manager or Admin), otherwise the API returns `403`. With four-eyes on, that person counts as the proposer, so other people must approve. - Tokens can create and read; they can **never approve** anything. ## POST /api/plans Create a plan from a template (status `PROPOSED`). Idempotent per workspace + `repository` + `ref`. | Field | Type | Required | Description | |---|---|---|---| | `templateId` | string | yes* | Template ID (Templates → template → *Copy*). Recommended: stable when renamed. | | `template` | string | * | Backwards compatible: template id or exact template name. Used only when `templateId` is absent. | | `name` | string | no | Plan name. Default `Release ` (or `Release ` without `ref`). Max 200. | | `environment` | string | no | Default `production`. Max 100. | | `ref` | string | no | Tag or branch, e.g. `v1.4.0`. Max 255. | | `repository` | string | no | e.g. `acme/shop`. Max 255. With `ref` it makes the call idempotent. | | `commitSha` | string | no | Commit SHA (shown on the plan page). | | `runUrl` | string | no | `http(s)` link to the CI run (shown on the plan page). | | `fourEyes` | boolean | no | Plan-wide four-eyes. Default `false`; per-step four-eyes from the template still applies. | \* one of `templateId` or `template` is required. ```bash curl -sS -X POST https://deployment-playbook.com/api/plans \ -H "Authorization: Bearer $PLAYBOOK_API_TOKEN" -H "Content-Type: application/json" \ -d '{"templateId":"cmabc123template","ref":"v1.4.0","repository":"acme/shop","commitSha":"3f2c1ab","runUrl":"https://github.com/acme/shop/actions/runs/1","environment":"production"}' ``` Response `201 Created` (new plan) or `200 OK` (a plan for this repository + ref already existed): ```json { "id": "cmplan123", "name": "Release v1.4.0", "status": "PROPOSED", "planUrl": "https://deployment-playbook.com/w/acme/plans/cmplan123", "gateUrl": "https://deployment-playbook.com/api/gate/cmplan123", "created": true } ``` Errors return `{"error": ""}`: | Status | When | |---|---| | `400` | Body is not JSON or a field is invalid (e.g. `runUrl` is not http(s)); template archived or without steps. | | `401` | Missing, invalid or revoked token. | | `403` | Token creator is no longer allowed to create plans, or the workspace's plan limit is reached. | | `404` | Template not found in this workspace. | | `500` | Unexpected error (no internals are returned). | ## GET /api/gate/:planId `GET /api/gate/?phase=before|during|after[&step=]`, header `Authorization: Bearer `. | Param | Description | |---|---| | `phase` | Phase whose work may start. Default `during`. `before`: the plan is approved. `during`: all Before steps resolved. `after`: all Before + During steps resolved. | | `step` | Optional plan step id: additionally all earlier steps **in the same phase** must be resolved (the phase is taken from the step). | "Resolved" = approved, or skipped when optional. The plan must be approved (`IN_PROGRESS`) or `DONE`, and no step may be rejected. Response (always HTTP `200` for a valid token and plan): ```json { "allow": false, "reason": "2 steps in \"Before deployment\" still open", "gatePhase": "DURING", "planStatus": "in_progress", "currentPhase": "BEFORE", "steps": { "approved": 6, "skipped": 0, "total": 19 }, "planUrl": "https://deployment-playbook.com/w/acme/plans/cmplan123" } ``` | Field | Description | |---|---| | `allow` | `true` when work in `gatePhase` may start. | | `reason` | Human-readable explanation. | | `gatePhase` | The phase that was evaluated (`BEFORE`, `DURING`, `AFTER`). | | `planStatus` | `waiting_for_approval`, `in_progress`, `rejected`, `done`, `rolled_back`. Use `done` to detect that the whole plan (incl. After) is complete. | | `currentPhase` | First phase with open steps, or `null` when everything is resolved. | | `steps` | Counts of approved, skipped and total steps. | | `planUrl` | Link to the plan. | The gate **fails closed**: `401` (bad token), `404` (unknown plan or another workspace's plan) and `500` all return `{"allow": false, "reason": "…"}`. Treat anything other than `200` + `allow: true` as "wait". ```bash curl -sS -H "Authorization: Bearer $PLAYBOOK_API_TOKEN" \ "https://deployment-playbook.com/api/gate/cmplan123?phase=after" | jq '{allow, planStatus, reason}' ``` ## MCP endpoint `POST /api/mcp` (Streamable HTTP, same Bearer token). See [Use with AI](https://deployment-playbook.com/docs/use-with-ai). ## Rate of polling Poll every 10–30 seconds. Each call is cheap, but there is no reason to poll faster than people approve. --- # SSO and social login ## Social login (Google, GitHub, Microsoft) Users can sign up and sign in with Google, GitHub or Microsoft next to email + password. Accounts are matched by **verified** email: if you already have a verified account with the same email, signing in with a provider links it. If an email + password account exists for that address but its email was never verified, the provider-verified identity takes it over: the password is removed and its sessions are signed out (written to the audit log). New users create a workspace (or join the workspace whose invite link they followed). Security: authorization-code flow with `state` (one-time, bound to your browser), PKCE (S256) and an OIDC `nonce`; Google and Microsoft id_tokens are verified against the provider's signing keys. Only verified emails are accepted (Google `email_verified`; Microsoft personal accounts or the `xms_edov` claim; GitHub the verified primary email). ### Self-hosting: enable the providers Each provider shows up only when both of its environment variables are set. The callback URL is `/api/auth/callback/`. | Provider | Env vars | Callback URL (deployment-playbook.com) | |---|---|---| | Google | `AUTH_GOOGLE_ID`, `AUTH_GOOGLE_SECRET` | `https://deployment-playbook.com/api/auth/callback/google` | | GitHub | `AUTH_GITHUB_ID`, `AUTH_GITHUB_SECRET` | `https://deployment-playbook.com/api/auth/callback/github` | | Microsoft | `AUTH_MICROSOFT_ID`, `AUTH_MICROSOFT_SECRET` | `https://deployment-playbook.com/api/auth/callback/microsoft` | **Google**: Google Cloud Console → APIs & Services → OAuth consent screen (External, scopes `openid`, `email`, `profile`) → Credentials → *Create credentials → OAuth client ID* → Web application → add the callback URL under *Authorized redirect URIs*. Copy the client ID and secret. **GitHub**: GitHub → Settings → Developer settings → *OAuth Apps* → New OAuth App (for an organisation: the org's Settings → Developer settings). Homepage URL = your app URL, *Authorization callback URL* = the callback URL. Generate a client secret. **Microsoft (Entra ID)**: Azure portal → Microsoft Entra ID → App registrations → New registration. Supported account types: *Accounts in any organizational directory and personal Microsoft accounts* (multi-tenant, uses the `common` endpoint). Redirect URI: platform *Web*, the callback URL. Then Certificates & secrets → New client secret (copy the *Value*). Token configuration → *Add optional claim* → ID token → `email` and `xms_edov` (so work-account emails count as verified). API permissions: `openid`, `email`, `profile` (Microsoft Graph, delegated). ## Company SSO (SAML 2.0 or OIDC) Available on **Business** and **Enterprise** (and Early access workspaces). An Admin configures it on **Single sign-on** (`/w//sso`). 1. **Give your IdP our values** (copy buttons on the page): - SAML: **ACS (Reply) URL** `…/api/sso/saml/acs/`, **Entity ID / Audience** and **metadata URL** `…/api/sso/saml/metadata/`. NameID format *email address*; send the email as NameID or as an `email` attribute, optionally `displayName`. Assertions must be signed. - OIDC: **Redirect URI** `…/api/sso/oidc/callback`, scopes `openid email profile`. 2. **Connection**: SAML → paste the IdP metadata URL or XML. OIDC → issuer/discovery URL, client ID and client secret (the secret is never shown again). Pick the **default role** for people who sign in for the first time (Viewer by default). 3. **Verify your email domains**: add `example.com`, create the DNS TXT record shown (`_deployment-playbook.example.com` = `deployment-playbook-verify=…`) and click *Verify*. Only users with an email on a verified domain can sign in through your SSO, and a domain can be verified by one workspace only. 4. Users sign in at **/sso** with their work email (or the workspace slug). First-time users are created and join the workspace with the default role (**JIT provisioning**); existing users with that email are linked. 5. Optional: **Enforce SSO**. Members whose email is on a verified domain can then no longer sign in with a password or social login. One **break-glass Admin** (must have a password) keeps password access in case your IdP is down. ### IdP notes - **Okta**: Applications → Create App Integration → SAML 2.0. Single sign-on URL = ACS URL, Audience URI = Entity ID, Name ID format EmailAddress, Application username Email. Copy the *Metadata URL*. - **Microsoft Entra ID**: Enterprise applications → New application → Create your own → Non-gallery → Single sign-on → SAML. Identifier = Entity ID, Reply URL = ACS URL. Copy the *App Federation Metadata Url*. Or use OIDC with an app registration (redirect URI above; discovery `https://login.microsoftonline.com//v2.0`). - **Google Workspace**: Admin console → Apps → Web and mobile apps → Add custom SAML app. ACS URL and Entity ID from above, Name ID format EMAIL, Name ID Basic Information → Primary email. Download the IdP metadata and paste it. ### How it is secured SP-initiated sign-in only: each AuthnRequest id is stored and the response must answer it (`InResponseTo`), so responses can't be replayed and unsolicited (IdP-initiated) responses are rejected. Signatures (assertion), audience, recipient and validity window are checked; RelayState is bound to your browser with a cookie. Configuration changes, domain verification, SSO sign-ins, failed SSO attempts and JIT memberships are written to the audit log. **SCIM** user provisioning/deprovisioning is planned for Enterprise. --- # Use with AI ## MCP server Deployment Playbook has a remote [Model Context Protocol](https://modelcontextprotocol.io) server so AI assistants can look up templates and plans, create plans and check gates for you. - **URL**: `https://deployment-playbook.com/api/mcp` (Streamable HTTP) - **Auth**: `Authorization: Bearer ` (Admin → Connections & webhooks → *Create API token*) | Tool | What it does | |---|---| | `list_templates` | Templates in the workspace (id, name, version, archived, step counts). | | `get_template` | One template with its current steps per phase. | | `create_plan` | Create a plan from a template id (idempotent by `repository` + `ref`). | | `list_plans` | Recent plans, optionally filtered by status. | | `get_plan` | A plan with phases, step ids and statuses, approvals count and link. | | `check_gate` | Same as `GET /api/gate` (phase / step). | **Approvals stay human.** No tool can approve, reject, skip or submit evidence: those decisions are made by people in the app (with four-eyes where configured). The token's permissions are those described in the [API reference](https://deployment-playbook.com/docs/api#authentication-workspace-api-tokens). ### Cursor `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json`): ```json { "mcpServers": { "deployment-playbook": { "url": "https://deployment-playbook.com/api/mcp", "headers": { "Authorization": "Bearer ${env:PLAYBOOK_API_TOKEN}" } } } } ``` ### Claude Code ```bash claude mcp add --transport http deployment-playbook https://deployment-playbook.com/api/mcp \ --header "Authorization: Bearer $PLAYBOOK_API_TOKEN" ``` ### Claude Desktop Settings → Developer → Edit config (`claude_desktop_config.json`), using the `mcp-remote` bridge: ```json { "mcpServers": { "deployment-playbook": { "command": "npx", "args": ["-y", "mcp-remote", "https://deployment-playbook.com/api/mcp", "--header", "Authorization:${PLAYBOOK_AUTH}"], "env": { "PLAYBOOK_AUTH": "Bearer dpk_your_token" } } } } ``` ### VS Code (GitHub Copilot) `.vscode/mcp.json`: ```json { "inputs": [{ "type": "promptString", "id": "playbook-token", "description": "Deployment Playbook API token", "password": true }], "servers": { "deployment-playbook": { "type": "http", "url": "https://deployment-playbook.com/api/mcp", "headers": { "Authorization": "Bearer ${input:playbook-token}" } } } } ``` ### Any other client Any MCP client that supports Streamable HTTP: URL `https://deployment-playbook.com/api/mcp`, header `Authorization: Bearer `. Quick test with the inspector: ```bash npx @modelcontextprotocol/inspector@latest --cli https://deployment-playbook.com/api/mcp --transport http \ --header "Authorization: Bearer $PLAYBOOK_API_TOKEN" --method tools/list ``` Treat the token like a password: keep it in an environment variable or your client's secret store, never in a committed file. ## llms.txt Machine-readable docs for AI tools, following [llmstxt.org](https://llmstxt.org): - [/llms.txt](https://deployment-playbook.com/llms.txt): index with links to every docs page. - [/llms-full.txt](https://deployment-playbook.com/llms-full.txt): all docs in one Markdown file. --- # Security & network This page is for corporate IT and security reviewers. It only describes what is implemented today. Deployment Playbook does not currently hold certifications (such as SOC 2 or ISO 27001) and does not publish an uptime SLA. ## Allow-list ### Domains the browser needs | Domain | Why | | --- | --- | | `deployment-playbook.com` | The app, its API and all static assets (fonts and scripts are self-hosted; no CDN, font or analytics third parties are loaded). | | `accounts.google.com` | Sign in with Google (only if you use it). | | `github.com` | Sign in with GitHub (only if you use it). | | `login.microsoftonline.com` | Sign in with Microsoft (only if you use it). | | Your own identity provider | SAML or OIDC single sign-on, if your workspace configures it. | | `checkout.stripe.com`, `billing.stripe.com` | Paid plans: Stripe Checkout and the Stripe customer portal (Admins only). | ### Endpoints your CI/CD and tools call All on `https://deployment-playbook.com`, authenticated with a workspace API token (`Authorization: Bearer …`): - `GET /api/gate/` - pipeline gate (see [API](https://deployment-playbook.com/docs/api)). - `POST /api/plans` - create a plan from CI. - `/api/mcp` - remote MCP server for AI assistants (read and create; never approve). - `/api/hooks/` - incoming provider events, verified with a per-connection secret. - `/api/integrations/github/webhook` - GitHub App events, verified with the app webhook secret (`X-Hub-Signature-256`), de-duplicated by delivery id and rate-limited per installation. - `/api/integrations/gitlab/webhook/` - GitLab tag-push and deployment events, verified with the connection's secret token (`X-Gitlab-Token`), de-duplicated and rate-limited. - `/api/integrations/azure-devops/webhook/` and `/api/integrations/azure-devops/check/` - Azure DevOps service hook and environment check, authenticated with the connection password (HTTP Basic), rate-limited; hook deliveries de-duplicated. ### Outbound connections from the service to you - **Status webhooks** to the URL a workspace Admin configures, signed with HMAC-SHA256 (header `X-Playbook-Signature-256`, over `timestamp.body`). Redirects are not followed; 5-second timeout. - **GitHub integration** (only when a workspace installs the GitHub App): `https://api.github.com` (installation tokens, check runs, deployment protection rule answers, repository list) and `https://github.com/login/oauth/access_token` (one-time check during installation that the installing user can access the installation). - **GitLab integration**: only the GitLab URL a workspace Admin configures (`https://gitlab.com` or a self-managed instance over https on a public address): token check, commit statuses and deployment approvals. - **Azure DevOps integration**: only the organization URL a workspace Admin configures (`https://dev.azure.com/` or Azure DevOps Server over https on a public address): token check, tag lookup, Git statuses, and the check callback (`TaskCompleted`) to that same organization. - **SSO**: the server fetches your OIDC discovery document, JWKS and token endpoint, or your SAML metadata URL. - **SSRF protection**: these URLs must be `https`; host names are resolved and requests to loopback, private, link-local, CGNAT, cloud-metadata, IPv6 unique-local and other reserved ranges are refused, both when the URL is saved and when the request is made (the checked address is the one connected to). - **Source IPs are not fixed.** The service runs on managed AWS infrastructure without a static egress IP, so allow-listing by IP is not possible; verify webhook signatures instead. ### Email Email (verification links, invitations) is sent from `no-reply@deployment-playbook.com` through Amazon SES (eu-central-1). Messages are DKIM-signed for `deployment-playbook.com`, use the custom MAIL FROM domain `mail.deployment-playbook.com` with an SPF record for Amazon SES, and the domain publishes a DMARC record. ## Hosting & data residency - Hosting: AWS Amplify and Amazon Aurora PostgreSQL in **AWS eu-central-1 (Frankfurt, Germany)**. The database is encrypted at rest and has automated backups. - Processor: **Generation Y B.V.** (Breda, the Netherlands). - Subprocessors: Amazon Web Services EMEA SARL (hosting, database) and Stripe Payments Europe Ltd. (billing). The authoritative list, transfers and notice periods are in the [Data Processing Agreement](https://deployment-playbook.com/dpa) and the [Privacy Policy](https://deployment-playbook.com/privacy). - Retention, per the [Privacy Policy](https://deployment-playbook.com/privacy): account and workspace data for the duration of the agreement, then a 30-day export window and deletion within a further 30 days; backups expire automatically within 35 days; server/security logs at most 90 days; billing records 7 years (Dutch fiscal obligation). ## Security controls - **Transport**: the service is served over HTTPS only, with HSTS (`max-age=63072000; includeSubDomains`). - **Browser security headers**: Content-Security-Policy (no third-party script hosts, `frame-ancestors 'none'`), `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin` and a restrictive `Permissions-Policy`. - **Tenant isolation**: every query is scoped to a workspace; API tokens only see their own workspace and unknown or foreign plan ids return `404`. - **Passwords**: bcrypt (cost 12). Changing your password signs out your other sessions. - **Password reset**: emailed single-use links valid for 1 hour, stored only as hashes, with a cooldown; the response never reveals whether an account exists, and a reset signs out every session and is recorded in the audit log. - **Rate limiting**: failed sign-ins are limited per email address (10 per 15 minutes) and per IP address, with increasing delays; sign-ups, verification emails and password-reset requests are rate-limited too. Errors are generic. - **Sessions**: random 256-bit tokens, stored only as SHA-256 hashes, in an `HttpOnly`, `Secure`, `SameSite=Lax` cookie, valid 14 days. - **Email verification**: password sign-ups confirm their address through a single-use link (24 hours, stored as a hash) before they can create or join a workspace. Resends are rate-limited. - **Social login and SSO**: Google, GitHub and Microsoft sign-in; SAML 2.0 and OIDC single sign-on per workspace with state, PKCE and nonce, signature checks on id_tokens and SAML assertions, DNS-verified domains and optional SSO enforcement with a break-glass Admin. See [SSO and social login](https://deployment-playbook.com/docs/sso-and-social-login). - **Roles**: Viewer, Approver, Template manager and Admin. Only Approvers approve; Admins cannot approve unless they are also Approvers. - **Four-eyes approvals**: steps marked four-eyes need two different approvers, and nobody can approve their own proposal or evidence. Required steps can't be skipped. - **Gate fails closed**: errors and unknown plans return `allow: false`. - **API tokens**: per workspace, shown once, stored as SHA-256 hashes, revocable, last use recorded. They can create and read plans and check gates, but never approve. - **Secrets at rest**: webhook signing secrets, provider-connection secrets and OIDC client secrets are encrypted with AES-256-GCM and never shown again after saving. - **Payments**: card data is handled by Stripe; Stripe webhooks are verified with Stripe signatures. - **Audit log**: plan and step decisions, template changes, members and roles, tokens, webhooks, SSO configuration and SSO sign-ins. ## Responsible disclosure Found a vulnerability? Email **info@genydev.nl** with steps to reproduce. Please give us reasonable time to fix it before disclosing it publicly. See also [`/.well-known/security.txt`](https://deployment-playbook.com/.well-known/security.txt). --- # GitHub integration **Available on:** Team and above for connecting, plans from tags and check runs. Business and Enterprise for the deployment protection rule. On lower plans the rule answers *rejected* with the comment "Requires Business plan" (fail closed, because it is a gate). After a downgrade, connections are kept but gated features stop being processed. See [pricing](https://deployment-playbook.com/pricing). The Deployment Playbook GitHub App connects a workspace to your GitHub organization: - **Tag or release → plan.** Pushing a tag (or publishing a release) that matches a pattern in a mapped repository creates a plan from the mapped template. One plan per repository + tag; redeliveries and re-pushes don't create duplicates. - **Check run on the tagged commit.** A "Deployment Playbook" check run links to the plan and is updated on every change: *in progress* while sign-off runs, *success* when the plan is done, *failure* when it's rejected, *cancelled* when it's rolled back. - **Deployment protection rule.** Enable the app on a GitHub environment and deployment jobs to that environment wait until the plan's gate phase is open (the same rule as the [gate API](https://deployment-playbook.com/docs/api)). Then the app approves the deployment. A rejected plan, or a deployment with no matching plan, is rejected. No long-lived GitHub credentials are stored: the app uses short-lived installation tokens (valid 1 hour), created on demand with the app's private key. ## Connect a workspace You need the **Admin** role in the workspace and permission to install apps on the GitHub organization. 1. Open **Integrations** in the workspace menu and click **Install the GitHub App**. 2. On GitHub, pick the organization and the repositories the app may access, then confirm. 3. GitHub asks you to authorize the app once. This lets us check that your GitHub account can access the installation, so no one can link someone else's installation. You're sent back to **Integrations**. 4. Under **Repository → template**, add a mapping: - **Repository**: `owner/repo`. - **Template**: the template new plans use. - **Environment**: the environment name shown on the plan. - **Tag pattern**: `*` is a wildcard. The default is `v*`. - **Gate phase**: deployments may start once this phase is open. `DURING` means the plan is approved and every `BEFORE` step is resolved. - **Four-eyes**: whether plans created from this mapping require four-eyes approval. Plans are created on behalf of the Admin who installed the app, and the audit log records them as created "via github integration". ## Protect a GitHub environment 1. In the repository, open **Settings → Environments → production** (or another environment). 2. Under **Deployment protection rules**, enable **Deployment Playbook** and save. 3. In your workflow, deploy with `environment: production` from a tag: ```yaml on: push: tags: ["v*"] jobs: deploy: runs-on: ubuntu-latest environment: production # waits for Deployment Playbook steps: - uses: actions/checkout@v4 - run: ./deploy.sh ``` When the job starts, GitHub asks the app. The app looks up the plan by repository and commit (or tag), and answers as soon as the gate phase opens. The job waits on GitHub (environment protection rules can wait up to 30 days). ## Permissions requested | Permission | Access | Why | | --- | --- | --- | | Checks | Read & write | Create and update the check run on the tagged commit | | Deployments | Read & write | Approve or reject deployments waiting on the protection rule | | Actions | Read | Required by GitHub for deployment protection rules | | Contents | Read | Resolve a release's tag to its commit | | Metadata | Read | Mandatory for every app | Events: `push`, `release`, `deployment_protection_rule` (plus `installation`, which every app receives). ## Security - Webhooks are verified with HMAC-SHA256 (`X-Hub-Signature-256`), de-duplicated by `X-GitHub-Delivery`, and rate-limited per installation. - Deployment callbacks are only sent to `api.github.com`. The callback URL is stored encrypted (AES-256-GCM). - Outbound calls go only to `api.github.com` and `github.com`. See [Security & network](https://deployment-playbook.com/docs/security). - **Disconnect** on the Integrations page removes the link and its mappings. Uninstall the app on GitHub (**Settings → GitHub Apps**) to revoke its access entirely. When the app is uninstalled, the integration is disabled automatically. ## Troubleshooting - **No plan appears.** Check that the repository name matches the mapping exactly, that the tag matches the pattern, and that the app has access to the repository. The Integrations page shows the last error. - **Deployment rejected with "No Deployment Playbook plan found".** The deployment's commit or tag has no plan. Push a mapped tag first, or create the plan with `POST /api/plans` using the same repository and tag. - **GitHub Enterprise Server** isn't supported yet. --- # GitLab integration **Available on:** Team and above for connecting, plans from tags and commit statuses. Business and Enterprise for deployment approvals. On lower plans Deployment Playbook never approves or rejects GitLab deployments, so approvals stay with people in GitLab. The Integrations page notes that deployment blocking needs Business. After a downgrade, connections are kept but gated features stop being processed. See [pricing](https://deployment-playbook.com/pricing). Works with GitLab.com and self-managed GitLab over https. - **Tag push → plan.** Pushing a tag that matches a mapped project's pattern creates a plan from the mapped template. You get one plan per project and tag; redeliveries are ignored. - **Commit status.** The tagged commit gets a `deployment-playbook` status that links to the plan: *pending* while waiting for approval, *running* while in progress, *success* when done, *failed* when rejected, *canceled* when rolled back. - **Deployment approvals** (GitLab Premium/Ultimate). A deployment to a protected environment that requires approval waits until the plan's gate phase is open. Deployment Playbook then approves it through the Deployments approval API. If the plan is rejected, the deployment is rejected. On GitLab Free, statuses still work and approval calls are skipped; the Integrations page explains why. ## 1. Create an access token Use a **project access token** (one project) or a **group access token** (all projects in a group): **Settings → Access tokens → Add new token**. | Setting | Value | | --- | --- | | Role | **Developer** for plans and statuses. **Maintainer** (or a role allowed to approve the protected environment) for deployment approvals | | Scopes | `api` (required to post statuses and approve deployments) | | Expiry | Your policy. Update the token on the Integrations page before it expires | The token is stored encrypted (AES-256-GCM) and is never shown again. ## 2. Connect the workspace Under **Integrations → GitLab** (Admins only), enter your GitLab URL (`https://gitlab.com` or your own instance) and the token, then click **Connect**. The token is checked against `/api/v4/personal_access_tokens/self`. **Self-managed instances** must be reachable over **https** on a public address. Hosts that resolve to private, loopback, link-local or other reserved ranges are refused, so an instance that is only reachable inside your network can't be connected yet. ## 3. Add the webhook In the project (or group, on Premium), go to **Settings → Webhooks → Add new webhook**: - **URL**: the webhook URL shown on the Integrations page (`https://deployment-playbook.com/api/integrations/gitlab/webhook/`). - **Secret token**: the secret shown on the Integrations page. It is checked on every request (`X-Gitlab-Token`). - **Trigger**: **Tag push events** and **Deployment events**. - Keep **SSL verification** on. ## 4. Map projects Add a mapping with the project path (`group/project`), the template, the tag pattern (default `v*`), the environment, the gate phase and the four-eyes setting. ## 5. Protect the environment (Premium/Ultimate) 1. Go to **Settings → CI/CD → Protected environments** and protect `production`. 2. Set **Required approvals** to 1. 3. Add the token's bot user (or its role) as an approver. Then deploy from a tag: ```yaml deploy: stage: deploy rules: - if: $CI_COMMIT_TAG =~ /^v/ environment: name: production script: ./deploy.sh ``` The job waits for approval. When the plan's gate phase opens, Deployment Playbook approves the waiting deployment for that commit and the job starts. ## Security - The webhook secret is compared in constant time. - Deliveries are de-duplicated (`X-Gitlab-Event-UUID`) and rate-limited per connection. - Outbound calls go only to the GitLab URL you configured, through the [SSRF guard](https://deployment-playbook.com/docs/security). - **Disconnect** deletes the stored token. Also revoke the token and remove the webhook in GitLab. --- # Azure DevOps integration **Available on:** Team and above for connecting, plans from tags and Git statuses. Business and Enterprise for the environment check, in both ApiResponse and Callback mode. On lower plans the check answers `status: "rejected"` with the reason "Requires Business plan" (fail closed), and in Callback mode it completes as *failed*. After a downgrade, connections are kept but gated features stop being processed. See [pricing](https://deployment-playbook.com/pricing). Works with Azure DevOps Services (`https://dev.azure.com/`) and Azure DevOps Server over https. - **Tag push → plan.** A *Code pushed* service hook creates a plan from the mapped template when a matching tag is pushed. You get one plan per repository and tag; redeliveries are ignored. Annotated tags are resolved to their commit. - **Git status.** The tagged commit gets a `deployment-playbook/release` status that links to the plan: *pending* while sign-off runs, *succeeded* when the plan is done, *failed* when it's rejected, *error* when it's rolled back. - **Environment check.** An *Invoke REST API* check on a pipeline environment keeps deployment jobs waiting until the plan's gate phase is open. ## 1. Create a personal access token Go to **User settings → Personal access tokens → New token** and pick the organization. Use a service account if you have one. | Scope | Why | | --- | --- | | **Code: Read & status** (`vso.code_status`) | Resolve annotated tags, post commit statuses | The token is stored encrypted (AES-256-GCM). Renew it on the Integrations page before it expires. ## 2. Connect the workspace Under **Integrations → Azure DevOps** (Admins only), enter the organization URL (for example `https://dev.azure.com/acme`, or `https://tfs.example.com/DefaultCollection` for Server) and the PAT, then click **Connect**. The page then shows the **service hook URL**, the **environment check URL** and a **password**. **Azure DevOps Server** must be reachable over **https** on a public address. Private and reserved IP ranges are refused, so a server that is only reachable inside your network can't be connected yet. ## 3. Add the service hook 1. In the project, go to **Project settings → Service hooks → + → Web Hooks**. 2. Trigger: **Code pushed**. Pick the repository (or *[Any]*). Leave the branch filter empty, because tags have no branch. 3. **URL**: the service hook URL. **Basic authentication username**: `playbook`. **Password**: the password from the Integrations page. 4. Resource details to send: **All**. ## 4. Map repositories Add a mapping with `Project/Repository` (for example `Shop/web`), the template, the tag pattern (default `v*`), the environment, the gate phase and the four-eyes setting. ## 5. Gate the environment 1. Create a service connection: **Project settings → Service connections → New → Generic**. - **Server URL**: `https://deployment-playbook.com/` - **Username**: `playbook` - **Password**: the password from the Integrations page. 2. Open **Pipelines → Environments → production → Approvals and checks → + → Invoke REST API** and set: - **Connection type**: Generic, using the service connection above. - **Method**: `POST`. - **URL suffix**: `api/integrations/azure-devops/check/` (copy it from the Integrations page). - **Body**: ```json { "repository": "$(System.TeamProject)/$(Build.Repository.Name)", "sha": "$(Build.SourceVersion)", "ref": "$(Build.SourceBranch)" } ``` 3. Pick one of two completion modes: - **ApiResponse** (simplest). Set the **success criteria** to `eq(root['status'], 'approved')` and a **time between evaluations** of 5 to 10 minutes. The check answers `pending` until the gate phase is open, and Azure DevOps keeps re-evaluating. - **Callback** (immediate). Set **Completion event** to *Callback* and add these headers: ```json { "Content-Type": "application/json", "PlanUrl": "$(system.CollectionUri)", "ProjectId": "$(system.TeamProjectId)", "HubName": "$(system.HostType)", "PlanId": "$(system.PlanId)", "JobId": "$(system.JobId)", "TaskInstanceId": "$(system.TaskInstanceId)", "AuthToken": "$(system.AccessToken)" } ``` Deployment Playbook stores the job's callback token encrypted. When the gate decides, it posts a `TaskCompleted` event (*succeeded* or *failed*) back to your organization, then deletes the token. Callbacks are only sent to the organization you connected. A deployment job that targets the environment then waits: ```yaml trigger: tags: include: ["v*"] stages: - stage: Deploy jobs: - deployment: web environment: production # waits for the Deployment Playbook check strategy: runOnce: deploy: steps: - script: ./deploy.sh ``` If no plan exists for the repository and commit, the check answers `rejected`. ## Security - The service hook and the check endpoint require the connection password (HTTP Basic, or the header `X-Playbook-Secret`). - Both are rate-limited per connection. Service-hook deliveries are de-duplicated by event id. - Outbound calls go only to the organization URL you configured, through the [SSRF guard](https://deployment-playbook.com/docs/security). - **Disconnect** deletes the stored PAT. Also revoke the PAT and remove the service hook in Azure DevOps.