# Bitrise build finished notifications on your phone

> Get a High-priority push notification when a Bitrise build fails, and optionally a quiet one when it passes.

- Company: Bitrise (https://www.justpush.io/recipes/bitrise)
- Event: `build/finished` (Build)
- Tags: CI/CD, Builds & CI
- Install: https://studio.justpush.io/recipes/bitrise/build-finished
- Web page: https://www.justpush.io/recipes/bitrise/build-finished

## Setup

1. Click Install in Studio and sign in. The recipe is added to your account and you get a personal webhook URL.
2. In Bitrise, open your project and go to Project settings > Integrations > Webhooks.
3. Under Outgoing webhooks, click Add webhook and use the URL from the setup notes below.
4. Choose Select individual events, tick the Build events, and click Create webhook.
5. Use this as the outgoing webhook's URL, replacing the app name with your own:
6. ```
7. your webhook URL (shown in Studio after install)?app=Acme%20Shop
8. ```
9. Bitrise's payload has no app name, so `app` is what the notification calls your project (`%20` is a space). Without it the push just says "Build #312 failed".
10. Only failed builds (High priority) and builds aborted with a failure, such as a timeout (Normal), push by default. Add `&all=1` to also get a quiet push when a build passes:
11. ```
12. your webhook URL (shown in Studio after install)?app=Acme%20Shop&all=1
13. ```
14. Tick only the build events. Bitrise also sends a webhook when a build starts (`build/triggered`); the recipe ignores it, as it does pipeline events.
15. Bitrise doesn't sign outgoing webhooks, and JustPush can't check custom headers, so keep your endpoint URL private. If a delivery goes missing, you can inspect and redeliver it from the webhook's **Recent deliveries**.

## Code

Studio calls `handleRequest(request)` with the incoming webhook (`{ method, headers, body, query, raw }`) and sends the message object it returns. Returning `null` sends nothing.

```js
// Bitrise → build finished
// Fires when a Bitrise build ends (Bitrise-Event-Type: build/finished). Failures push by
// default; add ?all=1 to the webhook URL to also hear about passing and aborted-OK builds.
// Docs: https://docs.bitrise.io/en/bitrise-platform/integrations/webhooks/adding-outgoing-webhooks.html

// build_status values, as in the Bitrise API's build list.
const STATUSES = {
    1: { emoji: "✅", verb: "passed", priority: -1, always: false }, // good news, only with ?all=1
    2: { emoji: "❌", verb: "failed", priority: 1, always: true }, // broken build: needs you soon
    3: { emoji: "⏹️", verb: "was aborted", priority: 0, always: true }, // aborted with failure (e.g. timed out or stopped)
    4: { emoji: "⏹️", verb: "was aborted", priority: -1, always: false }, // aborted with success, only with ?all=1
}

// Look a key up in a table without tripping over inherited names like "toString".
function pick(table, key, fallback) {
    const k = String(key)
    return Object.prototype.hasOwnProperty.call(table, k) ? table[k] : fallback
}

// Header names can arrive in any case.
function header(request, name) {
    const headers = request.headers || {}
    const key = Object.keys(headers).find((k) => k.toLowerCase() === name)
    const value = key ? headers[key] : null
    return Array.isArray(value) ? value[0] : value
}

// Treat blank or non-string values as missing.
function text(value) {
    if (typeof value === "number" && Number.isFinite(value)) return String(value)
    return typeof value === "string" && value.trim() ? value.trim() : null
}

function handleRequest(request) {
    const body = request.body && typeof request.body === "object" ? request.body : {}
    const event = header(request, "bitrise-event-type")

    // Guard: only build/finished. The header decides when it's there; without it (Studio's
    // Test runner) a payload with a build slug and a final build_status counts.
    const looksFinished = typeof body.build_slug === "string" && Number(body.build_status) > 0
    if (event ? event !== "build/finished" : !looksFinished) {
        console.log(`Ignoring Bitrise "${event ?? "unknown"}" event`)
        return null
    }

    // ?all=1 on the webhook URL also sends quiet pushes for passing builds.
    const all = ["1", "true", "yes"].includes(String(request.query?.all ?? "").toLowerCase())
    const status = pick(STATUSES, body.build_status, null)
    if (!status || (!status.always && !all)) {
        console.log(`Skipping Bitrise build_status ${body.build_status ?? "unknown"}`)
        return null
    }

    // The payload has no app name, so it comes from ?app= on the webhook URL.
    const app = text(request.query?.app)
    const number = text(body.build_number)
    const build = number ? `build #${number}` : "build"

    // "deploy · feature/checkout → main (PR #88)" or "deploy · tag v1.4.0"
    const git = body.git && typeof body.git === "object" ? body.git : {}
    const pr = Number(git.pull_request_id) > 0 ? ` (PR #${git.pull_request_id})` : ""
    const where = text(git.tag)
        ? `tag ${text(git.tag)}`
        : text(git.src_branch)
            ? text(git.src_branch) + (pr && text(git.dst_branch) ? ` → ${text(git.dst_branch)}` : "") + pr
            : null

    const slug = /^[A-Za-z0-9-]+$/.test(body.build_slug) ? body.build_slug : null

    return {
        title: `${status.emoji} ${app ? `${app} ${build}` : build.charAt(0).toUpperCase() + build.slice(1)} ${status.verb}`,
        message: [text(body.build_triggered_workflow), where].filter(Boolean).join(" · ") || "Bitrise build finished",
        topic: "Bitrise",
        priority: status.priority,
        buttons: slug ? [{ cta: "View build", url: `https://app.bitrise.io/build/${slug}` }] : [],
    }
}
```

## Bitrise build/finished webhook payload (sample)

```json
{
  "git": {
    "tag": null,
    "provider": "github",
    "dst_branch": "main",
    "src_branch": "feature/checkout",
    "pull_request_id": 88
  },
  "app_slug": "de7829a7317d976e",
  "build_slug": "58f4da148d884d68",
  "build_number": 312,
  "build_status": 2,
  "build_triggered_workflow": "deploy-testflight"
}
```

## FAQ

### Does this work on iPhone and Android?

Yes. Install the JustPush app from the App Store or Google Play and sign in. Every phone signed in to your account gets the notification.

### Do I need to write code?

No. Install the recipe in Studio, paste your webhook URL into Bitrise and you are done. The code is there if you want to change the text, the sound or the buttons.

### Can I change what the notification says?

Yes. After install the recipe's code is yours. Edit it in Studio and test it against the sample payload before you save.

### What does it cost?

JustPush is free for 30 days. After that it's $19.99 a year, or $39.99 once. There is no extra charge for recipes.
