Breakdance

We sped up our SDLC with a few hundred lines of TypeScript

Mergify served us well for years. But in 2025, we wanted something we could see inside, tweak to our exact workflow, and didn’t cost anything beyond the CI minutes we were already paying for.

So we replaced it with a script and a GitHub Action. At WorkTango, we call it Breakdance.

The name is on-brand for us, but it’s also a reference to how our merge process works. One PR steps into the circle at a time. It does its thing: CI runs, checks pass, merge happens. Meanwhile, everyone else waits their turn. Then, the next PR steps in. One at a time, each getting its moment.

The workflow we needed to support

At WorkTango, a PR moves through a predictable lifecycle:

  1. Developer opens a PR against the main branch (master, in our case)
  2. CI runs (tests, linting, type checking)
  3. At least one approving review required (more for certain files via CODEOWNERS)
  4. All status checks must pass
  5. When ready, the developer adds a GitHub label (breakdance) to request auto-merge
  6. On merge: commits are squashed into a single commit
  7. Commit title = PR title (must contain a Jira ticket, e.g., WT-12345)
  8. Commit description = extracted from the PR body’s “Commit Message” section

This keeps our git history clean: one commit per PR, meaningful messages, traceable to Jira.

The challenge was automating steps 5 through 8 reliably, with visibility into what’s happening.

Why we left Mergify

Mergify is a solid product. It auto-merged our PRs for years. Configuration was declarative and mostly intuitive. Support was responsive when we had questions.

So why move on?

We weren’t using the advanced features. Mergify has merge queues, automatic rebasing, smart scheduling, conditional rules. We used… automatic rebasing, labels, and… that’s about it. We were paying for a Ferrari and driving it to the grocery store.

We wanted to see inside the box. When something didn’t merge, the debugging experience was: check the Mergify dashboard, squint at logs, wonder if it was a race condition or a config issue. It usually worked when we retried, but when it didn’t, we felt blind.

We wanted to customize. Our PR title validation and commit message extraction relied on their template system. It worked, but we had ideas that didn’t fit their config model. We were bristling against the shape of the SDLC it wanted us to use.

CI time is cheap. GitHub Actions minutes are essentially free for our usage. The Mergify subscription wasn’t expensive, but “free” is hard to beat when you’re not using premium features.

To get right down to it, we only really needed a check that runs after a PR’s status checks pass, updates the PR if it’s behind, merges if it’s ready, and posts a comment if there’s a problem. That’s it. Not a process worth paying to automate.

How it works

When triggered, Breakdance processes all labeled PRs. The flow:

flowchart TD
    A[Fetch all open PRs] --> B[Filter to labeled PRs]
    B --> C{For each PR}
    C --> D{Is Draft?}
    D -->|Yes| E[Remove label, skip]
    D -->|No| F{Valid title?}
    F -->|No| G[Post error comment]
    F -->|Yes| H{Valid body?}
    H -->|No| I[Post error comment]
    H -->|Yes| J{Mergeable state?}
    J -->|behind| K[Update branch, skip]
    J -->|dirty| L[Post conflict error]
    J -->|blocked| M[Skip - wait for CI/reviews]
    J -->|clean/unstable| N[Extract commit message]
    N --> O[Squash merge to master]

Title must match /[A-Z]+-\d+/ (Jira ticket format). Body must have a ## Commit Message section. When validation fails, we post a comment explaining exactly what’s wrong—with a hidden HTML signature (<!-- breakdance-bot -->) so we can find and update our own comments later.

(Without that signature check, failing PRs got spammed with identical error comments every 10 minutes. I spent a good chunk of a morning cleaning those up before I realized we needed deduplication. 🙃)

The ## Commit Message section gets parsed and becomes the squash commit message:

// PR body contains:
// ## Commit Message
// {{ title }}
//
// This PR adds the thing that does the stuff.

// Becomes:
// WT-12345 - Add the thing
//
// This PR adds the thing that does the stuff.

Every destructive action checks a dryRun flag. We tested against real PRs in production with --dry-run=true before going live. Caught several bugs without breaking anything—highly recommend.

Triggering

Most of our CI runs in CircleCI, but Breakdance lives in GitHub Actions. Why the split? Our CircleCI jobs are read-only by design—no write access to the repo. That’s a deliberate security boundary we didn’t want to compromise.

So we set up a handoff: after all checks pass, CircleCI makes an API call to trigger the Breakdance workflow in GitHub Actions. That’s where the actual merge happens. Most PRs merge within seconds of CI going green.

We also have a scheduled fallback—every 10 minutes during business hours, hourly overnight. If something goes wrong with the API trigger, the worst case is a short delay. In practice, the trigger works reliably. But I sleep better knowing there’s a fallback.

schedule:
  # Backup: every 10 minutes during business hours
  - cron: "*/10 7-23 * * 1-5"
  - cron: "*/10 0-1 * * 2-6"
  # Overnight: hourly
  - cron: "0 * * * *"

We use concurrency controls so only one instance runs at a time. Prevents race conditions where two runs try to merge the same PR.

Edge cases

Situation Response
PR is a draft Remove the merge label, skip
Merge conflicts Post a comment explaining the conflict
Branch is behind master Update via merge, wait for next run
CI still running Skip, check again next run
Flaky test (“unstable” state) Merge anyway—fix flaky tests, don’t block merges

Things we intentionally don’t handle: approval requirements, required reviewers, merge conflict resolution. GitHub branch protection already handles the first two, and developers fix their own conflicts.

Was it worth it?

So far so good. We got: full visibility into why a PR did or didn’t merge. Customization we couldn’t get from a config file. Zero cost beyond CI minutes. About 600 lines of TypeScript we fully understand.

What we gave up: merge queues (we don’t use them), automatic rebasing (we update via merge instead), and Mergify’s nice dashboard. We prefer logs, but your mileage may vary.

Should you build your own? If you’re not using advanced features, you want full control, and CI time is cheap, then probably yes. The whole thing took a few hours to build and has been essentially maintenance-free.

If you need merge queues or sophisticated scheduling, or your team doesn’t have bandwidth to own another tool, stick with a provider like Mergify. It’s good software. We just wanted something simpler, transparent, and ours.

There’s also a middle ground: GitHub’s native merge queue exists now. If you just need “merge when ready,” that might be enough without any third-party tool.


Anyway, one PR at a time steps into the circle, does its thing, and merges. Then the next one. That’s the breakdance. ✌️