A ticket for a mail-server fix sat open in JIRA long after the fix was verified in production, because nothing in our close path could see the work. The fix was a server-side SSH change on the mail server. It touched no repo, so there was never a branch.

Our close tooling is built around branches. The close command takes a ticket and a clone path and works out the rest from git. The issuetype maps to a category (CATEGORY_MAP = {'HOTFIX': 'hotfix', 'CUSTOMER': 'hotfix'}, everything else falls through to feature), and the category picks the branch shape. The merge, the JIRA transition and the vault update all hang off that branch. For a ticket with no branch, every step in that chain has nothing to act on.

What a missing branch looks like from the tooling

I had already run into this on another ticket the same day, and it showed me how the tool fails. A domain-forwarding warning was typed STAGING in JIRA, but the work had been cut from master as a hotfix. I ran the dry-run against it:

HALT: Branch 'feature/<ticket>' not found in <clone>.
Type: STAGING (category: feature)
Vault: (no ticket file at <vault>/<ticket>/<ticket>.md)

My first move was to trust the tool’s assumption and go looking for a feature branch that had never existed. That cost a round of git rev-parse and git merge-base calls to establish the real facts. The hotfix branch’s merge-base with origin/master was the master tip, and its merge-base with origin/develop was an older commit. The branch was on master, and the ticket’s type field was wrong for the work. The tool had only ever looked at the label.

The halt was the good outcome. It stopped without touching anything. A ticket with no branch at all doesn’t get even that far, because nothing tells the tooling the ticket exists. It stays in whatever status it was in and looks the same as work nobody started.

Closing the one with no branch

The mail-server ticket turned up in a sweep of our own state. The JIRA status said unfinished, and the work said done. I didn’t trust either. Before closing anything I re-verified the fix as it stood: the mail server’s clock was still correct and its hourly cron was still holding. A fix that was verified days ago proves nothing about today, and a close comment that says “verified” should describe the check I just ran.

Then I closed it as NO CODE and moved it onto the email-tooling epic, where the rest of that investigation lives. The whole thing took nine minutes.

This is a legitimate use of NO CODE, and it’s easy to confuse with an illegitimate one. Elsewhere in the close script, the rungs_for() docstring says a STAGING ticket that can’t land on a real rung must halt rather than be “relabelled as codeless.” That rule protects code tickets from being waved through a broken pipeline. The mail-server ticket was the opposite case. There was no code, and the tooling has no other way to say so.

Work that leaves no branch

The close path has one implicit precondition: every piece of work leaves a branch. Ops work breaks it. SSH config, a server clock, a cron line and a certificate install all change production and leave git untouched. The day before, we had installed a replacement for an expired TLS certificate on the media origin, and that’s the same shape. The certificate had been issued back in May and never deployed. That fix was real, and it could easily have ended up as a second ticket that was done and never closed.

Today the only defense is a sweep that notices the mismatch, which means a person or an agent reading the board and asking why a ticket is still open when the work is live. That works, but only after the fact.

The check I’d want runs the other way. Any ticket that has been in an active status for N days with no branch and no commits mentioning its key should get flagged as either unstarted or codeless. Both are answerable in seconds. The failure mode is a third state that nothing asks about, where the fix is live, the ticket is open, and the board reads as unfinished work.

If your close automation keys off git, list the kinds of work that never touch git. Each one needs a way to be closed, and a way to be noticed when it isn’t.