# Argo

Record Argo CD deploys and gate releases in Argo Workflows. Open the **Argo** card on the Integrations page; each part has its own status and setup.

## Argo CD deploy events

> **Beta, early access.** A workspace without access gets `403 FEATURE_EARLY_ACCESS` and a link to request it. Flag: `argocd_deploys`.

Argo CD notifications record each synced revision as a deployment, so release health, incidents and change tickets follow what actually shipped.

1. Add a source with the repository on the Argo card, or with `bumpkit sources add argocd --repository owner/name`. Bumpkit shows a URL and a secret once.
2. Add a webhook service to `argocd-notifications-cm` with the URL and the header `X-Bumpkit-Secret: <secret>`.
3. Add an `on-sync-succeeded` template with this body:

```json
{
  "repository": "{{.app.spec.source.repoURL}}",
  "revision": "{{.app.status.operationState.syncResult.revision}}",
  "environment": "{{.app.metadata.name}}",
  "phase": "{{.app.status.operationState.phase}}",
  "finished_at": "{{.app.status.operationState.finishedAt}}"
}
```

The application name becomes the environment. Redeploying an earlier revision marks the newer change rolled back. The `sources_add` and `sources_list` MCP tools manage sources from an agent.

## Argo Workflows gate step

A ready-made WorkflowTemplate puts the release gate, deploy records and rollback inside Argo Workflows.

> **Beta, early access.** A workspace without access gets `403 FEATURE_EARLY_ACCESS` and a link to request it. Flags: `release_health` or `forensics` for deploy records, `intelligent_rollback` for rollback. The gate is on for every workspace.

## What you get

| Template | What it does |
|----------|--------------|
| `gate` | Checks out the revision, runs `bumpkit gate` and fails the step unless the release can ship. Exposes the outcome, decision, check and dashboard link as Argo output parameters |
| `record-deploy` | Runs `bumpkit deploy` after your deploy, with `success` or `failure`, so [release health](/docs/release-health) and [what broke production](/docs/release-health#what-broke-production) follow what actually shipped |
| `record-deploy-hook` | Records a successful deploy with an Argo `http` template: no pod, no API key, just the signed deploy webhook |
| `rollback` | Plans the smallest revert after a failed deploy and opens it as a pull request for a person to review and merge |

Everything runs the same `bumpkit` CLI you run on a laptop, with no terminal, no config file and no prompts.

## Prerequisites

- **An API key** in a Kubernetes Secret. A key carries the role of the person who created it. The gate and deploy records need a key from a member or above. Opening a rollback needs a key from an owner or admin, so keep that one in a separate Secret. See [roles](/docs/sso).
- **The Bumpkit GitHub App** on the repository. With the App, the gate reads `bumpkit.yml` from the base on GitHub itself, so a pull request can't loosen its own rules, and it counts the pull request's approvals. Without it, the gate uses the policy in the checkout and marks the decision `policy_source: client`. See [the policy comes from the base](/docs/release-gate#the-policy-comes-from-the-base).
- **Argo Workflows** with permission to read the Secrets in the workflow's namespace.
- **An image** with `sh`, `git`, `curl`, `tar` and `sha256sum`. The templates default to `buildpack-deps:bookworm-scm`; mirror it to your registry and pin it by digest. Bumpkit doesn't publish a container image.

## Quick start

### 1. Store the keys

```sh
kubectl -n argo create secret generic bumpkit --from-file=api-key=./bumpkit-api-key.txt
kubectl -n argo create secret generic bumpkit-rollback --from-file=api-key=./bumpkit-owner-key.txt
kubectl -n argo create secret generic git-credentials --from-file=username=./git-username.txt --from-file=password=./git-token.txt
```

Create the keys on the dashboard's **Keys** page and write each to its file without echoing it to your shell history; delete the files afterward. `bumpkit-rollback` is only for the `rollback` template, with a key from an owner or admin; skip it if you don't roll back from Argo. `git-credentials` holds a read-only token for the repository; for a public repository, drop `usernameSecret` and `passwordSecret` from the templates.

### 2. Install the WorkflowTemplate

Save this as `bumpkit-workflowtemplate.yaml` and run `kubectl -n argo apply -f bumpkit-workflowtemplate.yaml`. Set `bumpkit-version` to the version at `https://bumpkit.dev/download/latest/version`.

```yaml
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: bumpkit
spec:
  templates:
    - name: gate
      inputs:
        parameters:
          - name: repo
          - name: revision
          - name: base
            value: ""
          - name: pr
            value: ""
          - name: on-needs-approval
            value: fail
          - name: image
            value: buildpack-deps:bookworm-scm
          - name: bumpkit-version
            value: v0.94.0
          - name: bumpkit-sha256
            value: ""
          - name: api-url
            value: https://bumpkit.dev/v1
        artifacts:
          - name: source
            path: /work/src
            git:
              repo: "{{inputs.parameters.repo}}"
              revision: "{{inputs.parameters.revision}}"
              usernameSecret:
                name: git-credentials
                key: username
              passwordSecret:
                name: git-credentials
                key: password
      outputs:
        parameters:
          - name: outcome
            valueFrom:
              path: /tmp/bumpkit/outcome
              default: error
          - name: ship
            valueFrom:
              path: /tmp/bumpkit/ship
              default: "false"
          - name: decision-id
            valueFrom:
              path: /tmp/bumpkit/decision-id
              default: ""
          - name: check-id
            valueFrom:
              path: /tmp/bumpkit/check-id
              default: ""
          - name: url
            valueFrom:
              path: /tmp/bumpkit/url
              default: ""
          - name: version
            valueFrom:
              path: /tmp/bumpkit/version
              default: ""
      volumes:
        - name: bumpkit-bin
          emptyDir: {}
      initContainers:
        - name: install-bumpkit
          image: "{{inputs.parameters.image}}"
          env:
            - name: BUMPKIT_VERSION
              value: "{{inputs.parameters.bumpkit-version}}"
            - name: BUMPKIT_SHA256
              value: "{{inputs.parameters.bumpkit-sha256}}"
          volumeMounts:
            - name: bumpkit-bin
              mountPath: /opt/bumpkit
          command: [sh, -c]
          args:
            - |
              set -eu
              case "$(uname -m)" in
                x86_64|amd64) arch=amd64 ;;
                aarch64|arm64) arch=arm64 ;;
                *) echo "Unsupported architecture $(uname -m)" >&2; exit 1 ;;
              esac
              name="bumpkit_linux_${arch}.tar.gz"
              cd "$(mktemp -d)"
              curl -fsSLO "https://bumpkit.dev/download/${BUMPKIT_VERSION}/${name}"
              curl -fsSLO "https://bumpkit.dev/download/${BUMPKIT_VERSION}/checksums.txt"
              grep " ${name}\$" checksums.txt | sha256sum -c -
              if [ -n "${BUMPKIT_SHA256}" ]; then
                echo "${BUMPKIT_SHA256}  ${name}" | sha256sum -c -
              fi
              tar -xzf "${name}" -C /opt/bumpkit ./bumpkit
              /opt/bumpkit/bumpkit --version
      script:
        image: "{{inputs.parameters.image}}"
        workingDir: /work/src
        command: [sh]
        env:
          - name: BUMPKIT_API_KEY
            valueFrom:
              secretKeyRef:
                name: bumpkit
                key: api-key
          - name: BUMPKIT_API_URL
            value: "{{inputs.parameters.api-url}}"
          - name: BASE
            value: "{{inputs.parameters.base}}"
          - name: PR
            value: "{{inputs.parameters.pr}}"
          - name: ON_NEEDS_APPROVAL
            value: "{{inputs.parameters.on-needs-approval}}"
          - name: PATH
            value: /opt/bumpkit:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
          - name: GIT_CONFIG_COUNT
            value: "1"
          - name: GIT_CONFIG_KEY_0
            value: safe.directory
          - name: GIT_CONFIG_VALUE_0
            value: /work/src
        volumeMounts:
          - name: bumpkit-bin
            mountPath: /opt/bumpkit
        source: |
          set -u
          if [ -n "$PR" ] && [ -n "$BASE" ]; then
            BASE=$(git merge-base HEAD "$BASE" 2>/dev/null || echo "$BASE")
          fi
          bumpkit gate ${BASE:+--base "$BASE"} ${PR:+--pr "$PR"} --output-dir /tmp/bumpkit
          code=$?
          if [ "$code" -eq 1 ] && [ "$(cat /tmp/bumpkit/outcome)" = needs_approval ] && [ "$ON_NEEDS_APPROVAL" = pass ]; then
            echo "Needs approval; passing because on-needs-approval is pass."
            code=0
          fi
          if [ "$code" -eq 0 ]; then echo true > /tmp/bumpkit/ship; else echo false > /tmp/bumpkit/ship; fi
          exit "$code"

    - name: record-deploy
      inputs:
        parameters:
          - name: repo
          - name: revision
          - name: environment
            value: production
          - name: status
            value: success
          - name: version
            value: ""
          - name: base
            value: ""
          - name: image
            value: buildpack-deps:bookworm-scm
          - name: bumpkit-version
            value: v0.94.0
          - name: bumpkit-sha256
            value: ""
          - name: api-url
            value: https://bumpkit.dev/v1
        artifacts:
          - name: source
            path: /work/src
            git:
              repo: "{{inputs.parameters.repo}}"
              revision: "{{inputs.parameters.revision}}"
              usernameSecret:
                name: git-credentials
                key: username
              passwordSecret:
                name: git-credentials
                key: password
      volumes:
        - name: bumpkit-bin
          emptyDir: {}
      initContainers:
        - name: install-bumpkit
          image: "{{inputs.parameters.image}}"
          env:
            - name: BUMPKIT_VERSION
              value: "{{inputs.parameters.bumpkit-version}}"
            - name: BUMPKIT_SHA256
              value: "{{inputs.parameters.bumpkit-sha256}}"
          volumeMounts:
            - name: bumpkit-bin
              mountPath: /opt/bumpkit
          command: [sh, -c]
          args:
            - |
              set -eu
              case "$(uname -m)" in
                x86_64|amd64) arch=amd64 ;;
                aarch64|arm64) arch=arm64 ;;
                *) echo "Unsupported architecture $(uname -m)" >&2; exit 1 ;;
              esac
              name="bumpkit_linux_${arch}.tar.gz"
              cd "$(mktemp -d)"
              curl -fsSLO "https://bumpkit.dev/download/${BUMPKIT_VERSION}/${name}"
              curl -fsSLO "https://bumpkit.dev/download/${BUMPKIT_VERSION}/checksums.txt"
              grep " ${name}\$" checksums.txt | sha256sum -c -
              if [ -n "${BUMPKIT_SHA256}" ]; then
                echo "${BUMPKIT_SHA256}  ${name}" | sha256sum -c -
              fi
              tar -xzf "${name}" -C /opt/bumpkit ./bumpkit
              /opt/bumpkit/bumpkit --version
      script:
        image: "{{inputs.parameters.image}}"
        workingDir: /work/src
        command: [sh]
        env:
          - name: BUMPKIT_API_KEY
            valueFrom:
              secretKeyRef:
                name: bumpkit
                key: api-key
          - name: BUMPKIT_API_URL
            value: "{{inputs.parameters.api-url}}"
          - name: ENVIRONMENT
            value: "{{inputs.parameters.environment}}"
          - name: STATUS
            value: "{{inputs.parameters.status}}"
          - name: VERSION
            value: "{{inputs.parameters.version}}"
          - name: BASE
            value: "{{inputs.parameters.base}}"
          - name: PATH
            value: /opt/bumpkit:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
          - name: GIT_CONFIG_COUNT
            value: "1"
          - name: GIT_CONFIG_KEY_0
            value: safe.directory
          - name: GIT_CONFIG_VALUE_0
            value: /work/src
        volumeMounts:
          - name: bumpkit-bin
            mountPath: /opt/bumpkit
        source: |
          set -eu
          bumpkit deploy --environment "$ENVIRONMENT" --status "$STATUS" ${VERSION:+--version "$VERSION"} ${BASE:+--base "$BASE"}

    - name: record-deploy-hook
      inputs:
        parameters:
          - name: hook-url
          - name: repository
          - name: revision
          - name: environment
            value: production
      http:
        url: "{{inputs.parameters.hook-url}}"
        method: POST
        timeoutSeconds: 30
        headers:
          - name: Content-Type
            value: application/json
          - name: X-Bumpkit-Secret
            valueFrom:
              secretKeyRef:
                name: bumpkit-deploy-hook
                key: secret
        body: '{"repository": "{{inputs.parameters.repository}}", "revision": "{{inputs.parameters.revision}}", "environment": "{{inputs.parameters.environment}}", "phase": "Succeeded"}'
        successCondition: response.statusCode == 204

    - name: rollback
      inputs:
        parameters:
          - name: repo
          - name: revision
          - name: base
          - name: failure
            value: ""
          - name: image
            value: buildpack-deps:bookworm-scm
          - name: bumpkit-version
            value: v0.94.0
          - name: bumpkit-sha256
            value: ""
          - name: api-url
            value: https://bumpkit.dev/v1
        artifacts:
          - name: source
            path: /work/src
            git:
              repo: "{{inputs.parameters.repo}}"
              revision: "{{inputs.parameters.revision}}"
              usernameSecret:
                name: git-credentials
                key: username
              passwordSecret:
                name: git-credentials
                key: password
      outputs:
        parameters:
          - name: plan-id
            valueFrom:
              path: /tmp/bumpkit/plan-id
              default: ""
      volumes:
        - name: bumpkit-bin
          emptyDir: {}
      initContainers:
        - name: install-bumpkit
          image: "{{inputs.parameters.image}}"
          env:
            - name: BUMPKIT_VERSION
              value: "{{inputs.parameters.bumpkit-version}}"
            - name: BUMPKIT_SHA256
              value: "{{inputs.parameters.bumpkit-sha256}}"
          volumeMounts:
            - name: bumpkit-bin
              mountPath: /opt/bumpkit
          command: [sh, -c]
          args:
            - |
              set -eu
              case "$(uname -m)" in
                x86_64|amd64) arch=amd64 ;;
                aarch64|arm64) arch=arm64 ;;
                *) echo "Unsupported architecture $(uname -m)" >&2; exit 1 ;;
              esac
              name="bumpkit_linux_${arch}.tar.gz"
              cd "$(mktemp -d)"
              curl -fsSLO "https://bumpkit.dev/download/${BUMPKIT_VERSION}/${name}"
              curl -fsSLO "https://bumpkit.dev/download/${BUMPKIT_VERSION}/checksums.txt"
              grep " ${name}\$" checksums.txt | sha256sum -c -
              if [ -n "${BUMPKIT_SHA256}" ]; then
                echo "${BUMPKIT_SHA256}  ${name}" | sha256sum -c -
              fi
              tar -xzf "${name}" -C /opt/bumpkit ./bumpkit
              /opt/bumpkit/bumpkit --version
      script:
        image: "{{inputs.parameters.image}}"
        workingDir: /work/src
        command: [sh]
        env:
          - name: BUMPKIT_API_KEY
            valueFrom:
              secretKeyRef:
                name: bumpkit-rollback
                key: api-key
          - name: BUMPKIT_API_URL
            value: "{{inputs.parameters.api-url}}"
          - name: BASE
            value: "{{inputs.parameters.base}}"
          - name: FAILURE
            value: "{{inputs.parameters.failure}}"
          - name: PATH
            value: /opt/bumpkit:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
          - name: GIT_CONFIG_COUNT
            value: "1"
          - name: GIT_CONFIG_KEY_0
            value: safe.directory
          - name: GIT_CONFIG_VALUE_0
            value: /work/src
        volumeMounts:
          - name: bumpkit-bin
            mountPath: /opt/bumpkit
        source: |
          set -eu
          mkdir -p /tmp/bumpkit
          bumpkit rollback plan --base "$BASE" --head HEAD --error "$FAILURE" --json > /tmp/bumpkit/plan.json
          plan=$(sed -n 's/^  "id": "\([^"]*\)",$/\1/p' /tmp/bumpkit/plan.json)
          printf '%s\n' "$plan" > /tmp/bumpkit/plan-id
          if [ -z "$plan" ]; then
            cat /tmp/bumpkit/plan.json
            echo "Bumpkit found no commit that explains this failure, so it opened nothing." >&2
            exit 1
          fi
          bumpkit rollback open "$plan" --yes
```

### 3. Gate before you deploy

Call `gate` with `templateRef` as the step before your deploy. When the gate blocks, the step fails, the workflow stops, and nothing deploys:

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: shop-release-
spec:
  entrypoint: release
  arguments:
    parameters:
      - name: revision
      - name: base
        value: origin/main
      - name: pr
        value: ""
  templates:
    - name: release
      steps:
        - - name: gate
            templateRef:
              name: bumpkit
              template: gate
            arguments:
              parameters:
                - name: repo
                  value: https://github.com/acme/shop.git
                - name: revision
                  value: "{{workflow.parameters.revision}}"
                - name: base
                  value: "{{workflow.parameters.base}}"
                - name: pr
                  value: "{{workflow.parameters.pr}}"
        - - name: deploy
            template: deploy
    - name: deploy
      container:
        image: registry.example.com/acme/deployer:1.4.0
        args: [deploy, --revision, "{{workflow.parameters.revision}}"]
```

Submit it with `argo submit -n argo shop-release.yaml -p revision=<sha>`, and add `-p pr=<number>` for a pull request.

## WorkflowTemplate reference

### gate

| Parameter | Default | Description |
|-----------|---------|-------------|
| `repo` | | Git URL of the repository, e.g. `https://github.com/acme/shop.git` |
| `revision` | | The commit, branch or tag being released |
| `base` | The latest tag before the revision | What the release starts from. With `pr`, the template diffs from the merge base of `base` and the revision. Without `pr` and with the GitHub App, use a branch or tag, such as `origin/main` or `v1.4.0` |
| `pr` | | The pull request number. Bumpkit reads its approvals and its base's policy through the GitHub App |
| `on-needs-approval` | `fail` | `pass` lets a `needs_approval` decision through, for pipelines where a person approves later |
| `image` | `buildpack-deps:bookworm-scm` | The image for the install step and the gate |
| `bumpkit-version` | The release pinned in the template | The CLI release to download. Set it to the version at `https://bumpkit.dev/download/latest/version` |
| `bumpkit-sha256` | | The SHA-256 of `bumpkit_linux_<arch>.tar.gz` for your nodes. When set, the download must match it as well as `checksums.txt` |
| `api-url` | `https://bumpkit.dev/v1` | The Bumpkit API |

| Output parameter | Value |
|------------------|-------|
| `outcome` | `pass`, `needs_approval`, `fail`, or `error` when Bumpkit couldn't decide |
| `ship` | `true` when the step passed, else `false` |
| `decision-id` | The gate decision's ID |
| `check-id` | The release check's ID, for `bumpkit rollback open` and the API |
| `url` | The release page on the dashboard |
| `version` | The suggested version, like `1.5.0` |

| Step exit | When |
|-----------|------|
| `0` | Ready (`pass`), or `needs_approval` with `on-needs-approval: pass`, or Bumpkit couldn't decide and the policy sets `on_error: pass` |
| `1` | Blocked (`fail`), or `needs_approval` with `on-needs-approval: fail` |
| `2` | Bumpkit couldn't decide: bad flag, invalid `bumpkit.yml`, missing key, git or API error |

An invalid `bumpkit.yml` always exits `2`, even with `on_error: pass`. See [CLI exit codes](/docs/cli-flags#exit-codes).

### record-deploy

| Parameter | Default | Description |
|-----------|---------|-------------|
| `repo`, `revision` | | The repository and the commit that was deployed |
| `environment` | `production` | Where it went |
| `status` | `success` | `success`, `failure` or `in_progress` |
| `version` | A tag on the commit | The version that went live, like `v1.5.0` |
| `base` | The tag before the commit | The previous release, so Bumpkit gets the commits it shipped |
| `image`, `bumpkit-version`, `bumpkit-sha256`, `api-url` | | As for `gate` |

It exits `0` once recorded, `1` on an API error, and `2` on a bad flag, a missing key or a git error. Pass `version` and `base` when your clone has no tags.

### record-deploy-hook

| Parameter | Description |
|-----------|-------------|
| `hook-url` | The source's webhook URL, like `https://bumpkit.dev/hooks/deploys/<token>` |
| `repository` | `owner/name`, like `acme/shop` |
| `revision` | The deployed commit SHA |
| `environment` | Where it went (default `production`) |

### rollback

| Parameter | Description |
|-----------|-------------|
| `repo`, `revision` | The repository and the commit that failed |
| `base` | The last good release, like `v1.4.0` |
| `failure` | The error and stack trace, so Bumpkit can rank the commits behind it |
| `image`, `bumpkit-version`, `bumpkit-sha256`, `api-url` | As for `gate` |

Its `plan-id` output is the saved [rollback plan](/docs/rollback#intelligent-rollback). The step exits `1` when no commit explains the failure.

## Install the CLI in a step

Each template installs the CLI in an init container, into a shared `emptyDir`:

1. It downloads `bumpkit_linux_<arch>.tar.gz` and `checksums.txt` from `https://bumpkit.dev/download/<version>/`.
2. It checks the archive's SHA-256 against `checksums.txt`, and against `bumpkit-sha256` when you set it.
3. It unpacks only `bumpkit` and runs `bumpkit --version`.

That's the same verification the [install script](/docs/cli-install#install) does, without `bumpkit install` editing shell startup files in a throwaway container. Pinning `bumpkit-sha256` means a changed archive fails the step instead of running.

Only the current release is served. When Bumpkit ships a new version, `/download/<old version>/` answers `404` and the install step fails until you update `bumpkit-version` and `bumpkit-sha256`. To pin for longer, and to drop the download from every run, bake the CLI into your own CI image with the install script, in its build file:

```text
FROM buildpack-deps:bookworm-scm
RUN curl -fsSL https://bumpkit.dev/install.sh | BUMPKIT_VERSION=v0.94.0 BUMPKIT_INSTALL_DIR=/usr/local/bin sh
```

Then remove the `initContainers`, `volumes` and `volumeMounts` from the templates, and set `image` to your image.

## Gate outputs

`bumpkit gate --output-dir DIR` writes one file per value, which is what Argo's `valueFrom.path` reads: `outcome`, `decision-id`, `check-id`, `url` and `version`. It writes them on every exit once the flags parse, with `outcome` set to `error` when Bumpkit couldn't decide. The templates' `default:` values cover a bad flag, so a failed gate never turns into a "missing output" error.

`bumpkit gate --json` prints the whole decision instead. If your image has `jq`, you can pick fields from it yourself:

```yaml
    - name: gate-json
      script:
        image: registry.example.com/ci/bumpkit:0.91.0
        workingDir: /work/src
        command: [sh]
        source: |
          bumpkit gate --json > /tmp/decision.json
          code=$?
          jq -r '.check.id' /tmp/decision.json > /tmp/check-id
          exit "$code"
      outputs:
        parameters:
          - name: check-id
            valueFrom:
              path: /tmp/check-id
              default: ""
```

## Skip deploy unless the gate passed

To keep the workflow running when the gate blocks, for example to post a message, let the gate step fail without stopping the workflow and put a `when` on the deploy:

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: shop-release-
spec:
  entrypoint: release
  arguments:
    parameters:
      - name: revision
  templates:
    - name: release
      steps:
        - - name: gate
            templateRef:
              name: bumpkit
              template: gate
            continueOn:
              failed: true
            arguments:
              parameters:
                - name: repo
                  value: https://github.com/acme/shop.git
                - name: revision
                  value: "{{workflow.parameters.revision}}"
                - name: base
                  value: v1.4.0
        - - name: deploy
            template: deploy
            when: "{{steps.gate.outputs.parameters.ship}} == true"
          - name: notify
            template: notify
            when: "{{steps.gate.outputs.parameters.ship}} != true"
            arguments:
              parameters:
                - name: outcome
                  value: "{{steps.gate.outputs.parameters.outcome}}"
                - name: url
                  value: "{{steps.gate.outputs.parameters.url}}"
    - name: deploy
      container:
        image: registry.example.com/acme/deployer:1.4.0
        args: [deploy, --revision, "{{workflow.parameters.revision}}"]
    - name: notify
      inputs:
        parameters:
          - name: outcome
          - name: url
      container:
        image: registry.example.com/acme/notifier:2.0.0
        args: ["Release gate: {{inputs.parameters.outcome}}", "{{inputs.parameters.url}}"]
```

Without `continueOn`, a failed gate stops the workflow before `when` is evaluated.

## Deploy events

Record each deploy so release health compares errors before and after it and what broke production ranks the releases that actually shipped. Run `record-deploy` as the final steps, keyed on the deploy step's status:

```yaml
        - - name: deploy
            template: deploy
            continueOn:
              failed: true
        - - name: record-success
            templateRef:
              name: bumpkit
              template: record-deploy
            when: "{{steps.deploy.status}} == Succeeded"
            arguments:
              parameters:
                - name: repo
                  value: https://github.com/acme/shop.git
                - name: revision
                  value: "{{workflow.parameters.revision}}"
          - name: record-failure
            templateRef:
              name: bumpkit
              template: record-deploy
            when: "{{steps.deploy.status}} != Succeeded"
            arguments:
              parameters:
                - name: repo
                  value: https://github.com/acme/shop.git
                - name: revision
                  value: "{{workflow.parameters.revision}}"
                - name: status
                  value: failure
```

Key the record on the deploy step, not on `{{workflow.status}}`. An `onExit` handler runs when the gate blocked too, and recording a failed deploy that never happened skews release health.

### Without the CLI

`record-deploy-hook` posts to an existing [Argo CD deploy source](/docs/built-in#deploy-events) with an Argo `http` template, so no pod starts and no API key is in the workflow.

1. Add the source: `bumpkit sources add argocd --repository acme/shop`. It shows the webhook URL and a signing secret, once.
2. Store the secret: `kubectl -n argo create secret generic bumpkit-deploy-hook --from-file=secret=./bumpkit-hook-secret.txt`.
3. Call the template after a successful deploy:

```yaml
        - - name: record
            templateRef:
              name: bumpkit
              template: record-deploy-hook
            when: "{{steps.deploy.status}} == Succeeded"
            arguments:
              parameters:
                - name: hook-url
                  value: https://bumpkit.dev/hooks/deploys/<token>
                - name: repository
                  value: acme/shop
                - name: revision
                  value: "{{workflow.parameters.revision}}"
```

The hook records successes only, sent without commits, and Bumpkit shows them as Argo CD deploys. It needs the `argocd_deploys` flag. Argo Workflows runs `http` templates in its agent, which needs RBAC for `workflowtasksets` and to read the Secret. An Argo Events sensor can post the same body with an HTTP trigger.

## Rollback

> **Beta, early access.** Flag: `intelligent_rollback`. Opening the pull request needs the Bumpkit GitHub App on the repository.

After a failed deploy, `rollback` runs [`bumpkit rollback plan`](/docs/rollback#plan-a-rollback) with the failure text. It finds the commits since `base` most likely behind it, picks the smallest revert that applies cleanly and saves the plan. `bumpkit rollback open PLAN_ID --yes` then opens that revert as a pull request through the GitHub App.

- `--yes` is required, because nobody's at a terminal to confirm. It opens the pull request; it doesn't approve or merge anything.
- The human approval is the revert pull request itself. The gate runs on it like any pull request, and a person merges it. Bumpkit never merges.
- Opening a rollback needs an owner or admin key, kept in the `bumpkit-rollback` Secret, and it's written to the audit log as `rollback.open` (`loop.rollback.open` when you pass a check ID with Close the loop).
- With Close the loop on (Beta, flag `close_the_loop`), you can skip the plan and run `bumpkit rollback open CHECK_ID --yes` with the gate's `check-id` output. Bumpkit then plans on its side and reverts the whole release when no evidence points at specific commits.

## Stacked pull requests and merge queues

- **Stacked pull requests.** Pass the parent's branch as `base`, like `origin/feature/checkout-v2`, and the pull request's number as `pr`. The template diffs from the merge base, so each pull request is gated on its own changes, not its parent's. With the GitHub App and the `stacked_prs` flag, Bumpkit also tracks the stack and [`stacks.require_parent_merged`](/docs/bumpkit-yml#stacks-require-parent-merged) can hold a child until its parent merges.
- **Merge queues.** With the GitHub App, Bumpkit already gates each merge queue group as a check. To gate the queue's commit in Argo too, run `gate` on that commit with no `pr` and the target branch as `base`, like `origin/main`, so the decision covers what will actually land.
- **Re-runs.** A decision isn't cached: retrying the step gates the revision again, so an approval given since the last run counts.

## Secrets

- The API key lives only in a Kubernetes Secret and reaches the gate as `BUMPKIT_API_KEY` through `secretKeyRef`. It never appears in parameters, arguments, logs or outputs.
- `bumpkit` needs no config file in the container. It never prompts, opens a browser or writes the key anywhere.
- Keep the rollback key, which can open pull requests, in its own Secret and give it only to the `rollback` template.
- Use a read-only git token in `git-credentials`. Bumpkit doesn't need write access to the repository; the GitHub App opens rollback pull requests.
- Output parameters hold IDs and a dashboard link, never a secret.

## Network egress

The steps only need outbound HTTPS to:

| Host | For |
|------|-----|
| `bumpkit.dev` | The API and the CLI download |
| Your git host | The checkout |
| Your image registry | The step image |

Bumpkit itself talks to GitHub through the GitHub App, so your pods don't need access to `api.github.com`. Set `HTTPS_PROXY` in the template's `env` if your cluster requires a proxy. Baking the CLI into your own image drops the download.

## Troubleshooting

| You see | Why | Fix |
|---------|-----|-----|
| `403 FEATURE_EARLY_ACCESS` | The feature isn't on for your workspace yet | Request access from the link. See [feature not enabled](/docs/troubleshooting#feature-not-enabled) |
| `400 INVALID_POLICY` or `on_error must be fail or pass` | `bumpkit.yml` doesn't parse. The gate always fails closed | See [the policy is invalid](/docs/troubleshooting#the-policy-is-invalid) |
| `503 POLICY_UNAVAILABLE` | Bumpkit couldn't read `bumpkit.yml` from the base with the GitHub App | Retry the step |
| `not signed in: run bumpkit login, or set BUMPKIT_API_KEY` | The Secret or its `api-key` key is missing | See [missing API key](/docs/troubleshooting#missing-api-key) |
| `v1.4.0 is not a commit, tag or branch in .` | The clone doesn't have the base | Pass a branch the clone has, like `origin/main`, or a tag your git host serves. See [shallow checkout](/docs/troubleshooting#shallow-checkout) |
| `detected dubious ownership in repository` | The checkout belongs to another user than the step | Keep the `GIT_CONFIG_*` variables in the template's `env` |
| `curl: (22) The requested URL returned error: 404` in `install-bumpkit` | `bumpkit-version` is no longer the current release | Update `bumpkit-version` and `bumpkit-sha256`, or bake the CLI into your image |
| `sha256sum: WARNING: 1 computed checksum did NOT match` | The archive doesn't match `checksums.txt` or `bumpkit-sha256` | Don't run it. Check `bumpkit-sha256` for your nodes' architecture |
| The gate says `NEEDS APPROVAL` after an approval | Approvals only count with `pr` and the GitHub App, on the latest commit | See [an approval didn't count](/docs/troubleshooting#an-approval-didn-t-count) |
| `pass --yes to confirm when not running interactively` | A command asked for confirmation with no terminal | Add `--yes` |
| `409 GITHUB_APP_REQUIRED` | Rollback needs the GitHub App on the repository | Install the App. See [open the revert pull request](/docs/rollback#open-the-revert-pull-request) |

Every gate reason links to its [explanation](/docs/explanations#gate-reasons), and the decision's `url` output opens it on the dashboard.

## End-to-end example

Check out, gate, deploy, record the result, and open a rollback for review when the deploy fails:

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: shop-release-
spec:
  entrypoint: release
  arguments:
    parameters:
      - name: revision
      - name: last-good
        value: v1.4.0
  templates:
    - name: release
      steps:
        - - name: gate
            templateRef:
              name: bumpkit
              template: gate
            arguments:
              parameters:
                - name: repo
                  value: https://github.com/acme/shop.git
                - name: revision
                  value: "{{workflow.parameters.revision}}"
                - name: base
                  value: "{{workflow.parameters.last-good}}"
        - - name: deploy
            template: deploy
            continueOn:
              failed: true
        - - name: record-success
            templateRef:
              name: bumpkit
              template: record-deploy
            when: "{{steps.deploy.status}} == Succeeded"
            arguments:
              parameters:
                - name: repo
                  value: https://github.com/acme/shop.git
                - name: revision
                  value: "{{workflow.parameters.revision}}"
                - name: base
                  value: "{{workflow.parameters.last-good}}"
          - name: record-failure
            templateRef:
              name: bumpkit
              template: record-deploy
            when: "{{steps.deploy.status}} != Succeeded"
            arguments:
              parameters:
                - name: repo
                  value: https://github.com/acme/shop.git
                - name: revision
                  value: "{{workflow.parameters.revision}}"
                - name: base
                  value: "{{workflow.parameters.last-good}}"
                - name: status
                  value: failure
        - - name: rollback
            templateRef:
              name: bumpkit
              template: rollback
            when: "{{steps.deploy.status}} != Succeeded"
            arguments:
              parameters:
                - name: repo
                  value: https://github.com/acme/shop.git
                - name: revision
                  value: "{{workflow.parameters.revision}}"
                - name: base
                  value: "{{workflow.parameters.last-good}}"
                - name: failure
                  value: "{{steps.deploy.outputs.parameters.failure}}"
        - - name: deploy-failed
            template: deploy-failed
            when: "{{steps.deploy.status}} != Succeeded"
    - name: deploy
      outputs:
        parameters:
          - name: failure
            valueFrom:
              path: /tmp/failure.txt
              default: ""
      script:
        image: registry.example.com/acme/deployer:1.4.0
        command: [sh]
        source: |
          deployer deploy --revision "{{workflow.parameters.revision}}" > /tmp/deploy.log 2>&1
          code=$?
          cat /tmp/deploy.log
          tail -c 4000 /tmp/deploy.log > /tmp/failure.txt
          exit "$code"
    - name: deploy-failed
      script:
        image: buildpack-deps:bookworm-scm
        command: [sh]
        source: |
          echo "The deploy failed. Bumpkit recorded it and opened a revert for review." >&2
          exit 1
```

The gate blocks first, so a blocked release never deploys and never records a deploy. A failed deploy is recorded as `failure`, Bumpkit opens the revert pull request, and the last step fails the workflow so your alerts still fire.
