The ticket tracker showed the change still marked “in review.” Production was already serving it.

We’d built a review-request module for a major B2B software-review platform, meant to live on the post-checkout Thank You page. We’d also rebuilt three existing domain modules into collapsible blocks to match a teammate’s page restyle, expanded by default. It was small, self-contained front-end work, the kind of change that should sit in a review environment for a day before anyone touches prod. Instead it went straight through.

A branch name was the whole routing rule

The platform’s repo has a pipeline with two branch families that CI watches by name pattern. Anything matching staging deploys automatically to a shared review environment. Anything matching hotfix-* skips that environment entirely and deploys straight to production, because hotfixes are supposed to be small, urgent, and already reviewed elsewhere before they’re cut.

The branch for this work was named hotfix-checkout-thankyou. “Hotfix” was the closest existing label for “small, isolated, ticket-scoped change,” which is exactly what it was. It wasn’t urgent, and it hadn’t been reviewed. The CI trigger can’t tell “hotfix” as urgency from “hotfix” as branch-naming habit. It matches on the string, and the string alone was enough to route the push to the production deploy job, with no staging stop and no review gate.

We pushed it expecting the staging pipeline to pick it up so we could look at it live before any real users touched it. The production deploy fired instead. The Thank You page on the live site had the new review-request module and the restyled domain blocks before we had looked at them outside a local browser.

Finding it in the deploy log

The first sign was the ticket state mismatch. The tracker said “in review,” and the CI dashboard showed a completed production deploy job against hotfix-checkout-thankyou, timestamped minutes after the push. Pulling the deploy log confirmed it: the job had run against the production target. Nothing in the pipeline had asked for confirmation, because nothing about a hotfix push is supposed to need one.

Cherry-picking it back out

Recovery was a cherry-pick rather than a revert, because by the time we caught it there were already unrelated commits landing on top from other work. We identified the single commit sha for the Thank You page change and cherry-picked it onto a properly named branch cut from the last production tag. That one went through staging the normal way, so we could look at it before it shipped again. The original hotfix-checkout-thankyou branch got deleted so it couldn’t fire the same trigger twice.

We reviewed the cherry-picked version live in staging, and it wasn’t done anyway. The domain blocks needed another pass on which sections should default open. The branch is still sitting local and uncommitted, with the current state written into the ticket’s dev notes instead of pushed anywhere, on purpose this time.

Checking lineage instead of the name

The bug was that “hotfix” meant two different things and the pipeline only understood one of them. We rewrote the branch-shape rule so the trigger no longer matches on name alone. A branch only qualifies for the direct-to-production path if its merge base is the current production tag. A branch cut from staging HEAD, no matter what it’s called, routes through the staging deploy and the review step, full stop. Calling something hotfix-anything no longer buys a shortcut around review. Only branching from a shipped tag does.

That’s a much narrower door than “contains the word hotfix.” The failure we hit, a small unreviewed change riding a name it happened to share with the urgent-fix path, can’t come from naming habits anymore. The rule now checks lineage.

Check your own triggers

If you run a pipeline like this, look at whether any automated deploy path to production is triggered by a branch name alone. In our case a name chosen out of habit sent an unreviewed change straight to the live site, with no confirmation step. A rule based on where the change came from, such as requiring it to descend from the current production tag, keeps the shortcut for urgent fixes and keeps review for everything else.