← writing featured

Glazier Release: v0.2.4

Last night, I minted and tagged Glazier v0.2.4. This release focussed on addressing 80 bugs and stability issues I’ve collated over the last few months. I initially thought I’d take care of a dozen or so HIGH and MEDIUM ones, but ended up spending the better part of the last week working through all of them.


I might’ve gone a little overboard.

Yes, you read that right. Dozens of bugs covering the full spectrum of compatibility, stability, security and just plain broken functionality. Here’s the list in its entirety:

BugProblem
B01tmux matched session names by prefix, so glaze down on a session named project could kill project-long.
B02A window with four or more panes failed with “no space for new pane”.
B03Panes and windows ignored the starting_directory of their window and session.
B04A non-final command ending in &, ; or a comment hung up.
B05The layout overwrote size and adjust, so neither had any effect.
B06Session commands hung or failed with some session names.
B07Logs and diagnostics went to stdout, so save --stdout > file wrote an invalid profile.
B08A failed glaze up left a partial session behind a subsequent glaze up would accept.
B09The first window index was base-index + 1.
B10A semicolon or a tab in a name broke the parsing of tmux output.
B11save overwrote an existing .glaze without asking.
B12save outside tmux picked a session without a warning.
B13Validation accepted some values that up could not apply.
B14Diagnostics contained ANSI colour codes when the output was not a terminal.
B15Diagnostics for a syntax error showed “(source code not available)”.
B16The README and --help claimed that --var-file reads JSON; it reads HCL only.
B17The README example for the session-created hook could never fire.
B18A ~ in --profile-path or ${GLAZE_PATH} was not expanded.
B19ls exited 1 when no tmux server was running.
B20attach passed the socket flag twice.
B21tmux expanded format sequences such as #{session_name} inside names.
B22save wrote directories that no longer existed.
B23The README did not say that reverse only accepts a string.
B24tmux quietly changed . and : in a session name.
B25With tmux 3.4, a $ in a name gained an extra backslash on every read.
B26up --clear from inside the target session killed its own shell and left nothing behind.
B27~/ in starting_directory passed validation but reached tmux unexpanded.
B28${TMUX} was trusted without a socket check, so up could switch a client on another server.
B29format and save truncated the file before writing, so a failed write destroyed the only copy.
B30--debug printed every envs value, secrets included.
B31down panicked when the session name evaluated to a number, a bool or null.
B32A session without a name worked with up, but down rejected the same profile.
B33down rolled random() in the session name again and killed the wrong session.
B34down, ls and save reported “not running” when tmux was merely unreachable.
B35tmux split an argument ending in ;, which broke up with an odd error.
B36A ! in a command triggered history expansion in bash and zsh. Oops.
B37Readline completion ate a tab in a command.
B38The wait-for signal relied on tmux from ${PATH} and ${TMUX}, so a changed environment hung up.
B39A pane whose command exits at once broke the next split.
B40A session option declared on a window leaked to every later window.
B41A window or pane option declared on the session landed on a throwaway window and was lost.
B42A base-index set in the background by tmux.conf made glaze kill the first declared window.
B43-v after a subcommand was ignored, so glaze up -v built a session.
B44Under LANG=C, non-ASCII names read back as _ and broke a second up and save.
B45A small chain of locals could exhaust memory, even in down and format --validate.
B46Deeply nested brackets (about 100,000 levels) crashed glaze with a stack overflow.
B47A size with only x or only y passed validation but was never applied.
B48A percentage adjust.amount passed validation, then failed halfway through up.
B49make install on Apple Silicon built an amd64 binary outside ${PATH}.
B50A null element in a list or map panicked up and format --validate.
B51A command starting with - was read as send-keys flags.
B52A final command equal to a tmux key name, such as Enter, was sent as that key.
B53The shell’s line editor cut long command lines, which hung up.
B54SIGINT or SIGTERM during a wait left an orphaned tmux client and a partial session.
B55A bare ~ was rejected, and ~user gave a misleading error.
B56glaze failed from a deleted working directory, even with an absolute profile path.
B57A ${GLAZE_PATH} naming a file, or a .glaze directory, gave “profile not found” with no hint.
B58format hung on a FIFO and dropped its output on /dev/stdin.
B59format rewrote an unchanged file and bumped its modification time.
B60down without a required --var reported “Unsupported attribute”.
B61A cycle between locals showed up as three misleading errors.
B62random() on a map put a Go stack dump in its diagnostic.
B63save of a session that glaze did not create named every pane after the host and every window after the shell.
B64Two up runs at the same time left the loser with an opaque error.
B65glaze read base-index over and over, so ls on 80 windows spawned 83 processes.
B66A tmux.conf hook could rename every declared window in silence.
B67A raw layout with the wrong number of panes was applied in part without a word.
B68Unknown hook names passed validation and failed halfway through up.
B69A bool variable accepted 1 and 0, which the README never mentioned.
B70A binary from go install reported Version: dev.
B71--help showed the author as a Go struct.
B72save warned “EXPERIMENTAL” on every run.
B73${NO_COLOR} was ignored in a pipe.
B74tmux 3.9 prints layouts as JSON, which save wrote and glaze then rejected.
B75tmux read a window name starting with - as a flag, which failed up.
B76up without a terminal exited 1 but left the session running.
B77A failed write in format exited 3, the code for an invalid profile.
B78format --validate printed its errors in a different order on each run.
B79A signal during a wait was blamed on the pane’s shell.
B80Errors in a --var-file showed “(source code not available)”.

Some of these were just plain embarrassing, but I’m glad I was able to work through them all. Surely, there are many more lurking in the code somewhere, but each one is a learning opportunity. On the bright side, Glazier is now more stable than ever!

We now have proper package management!

I also took the time to reduce the friction involved in actually installing the tool. I looked at something like GoReleaser, which would definitely have simplified distribution, but it would’ve come at the cost of the current release pipeline in GitHub. Definitely doable, but I’d have to bend the pipeline to fit the tool rather than the other way around. Besides, a handful of files was enough to get the following platforms sorted.

Homebrew

This is probably the one that makes the most sense for a lot of fellow developers. The most tedious part was standing up an additional repository to get a custom tap working. But now, installing Glazier on your Mac is as easy as:

Bash
brew install wilhelm-murdoch/tap/glazier

APK, DEB and RPM packages

These were actually easier to implement, as each one is rendered by a simple Bash script.

.deb

Bash
curl -LO https://github.com/wilhelm-murdoch/glazier/releases/latest/download/glazier-linux-amd64.deb
sudo apt install ./glazier-linux-amd64.deb

.rpm

Bash
curl -LO https://github.com/wilhelm-murdoch/glazier/releases/latest/download/glazier-linux-amd64.rpm
sudo dnf install ./glazier-linux-amd64.rpm

.apk

Bash
curl -LO https://github.com/wilhelm-murdoch/glazier/releases/latest/download/glazier-linux-amd64.apk
apk add --allow-untrusted ./glazier-linux-amd64.apk
The .apk package isn’t signed, so you’ll have to use the --allow-untrusted flag for this to work. You can always verify it yourself against the release’s attestation. Alternatively, if enough people ask, I’ll go through the trouble of getting it properly signed.

What about AUR support?

This was one of the last platforms I wanted to support. However, the AUR currently has a registration freeze in place due to a massive influx of requests. So, I’m putting this one on pause and I’ll check back later.

E2E testing gets a brand-new harness!

Most of these bugs were found by an end-to-end harness that drives the real binary against real tmux servers. It began life as a bunch’a Bash scripts. For this release, I rebuilt it as an extensible Go test module that lives in the repository under e2e/.

Every case starts its own tmux server in its own temporary directory, runs the glaze binary exactly as you would and reads the result back from tmux itself. The harness never imports glaze, so a bug cannot hide behind glaze’s own code.

A few details:

  • It currently has over 200 cases and more than 1,000 checks, with profiles kept as real .glaze fixture files.
  • It covers six tmux versions from 3.2a to 3.7c, Alpine (musl) included, on amd64 and arm64, plus macOS.
  • Every pull request runs the full matrix and compares the result of each check against the main branch.
  • A weekly canary runs against the development branch of tmux. It caught the JSON layouts of tmux 3.9 before any tmux release shipped them.
  • Releases are now gated behind the harness, so a release tag publishes only when every case passes against it.
  • Known failures live in a baseline that fails the run the moment a fix makes an entry stale.

The last big release, v0.1.6, failed 88 checks on every target. At the time of this writing, all checks pass on all targets! This was arguably the biggest win for future releases because we now have near-full integration coverage on top of the 700+ unit tests… probably a bit excessive for this kind of tool. I’m nothing if not thorough.

The downside to this new harness is that it’s one more thing I need to maintain alongside the rest of the code. We’ll see how much of a headache that becomes.

What’s next?

Now that all known bugs have been squashed, I can focus on some interesting new features. Specifically, something like glaze diff ... that compares a given session with the specified .glaze profile. Think of terraform plan, but for your tmux session. I’m also considering a registry, or pool, of named profiles that can be called from anywhere regardless of directory context. I’ve also been thinking about lifecycle hooks outside of tmux, and about a way to import your existing profiles from other tools like tmuxinator, tmuxp and smug.

Then there’s the repository’s ever-growing README.md. It isn’t exactly the most intuitive thing to peruse as a newcomer, and the SPEC.md file is a bit daunting. So, a dedicated documentation site is something I’m considering as a next step.

If there’s one thing I’ve learned while building this tool it’s that I never want to build functionality around an other well-known tool ever again. But, I probably will anyway.

To the two other people using this, enjoy!

Comments

1 revision since publication

  • e7b4aba +2 −0 promote glazier release article to featured and pinned
  • 4a2397e +175 −0 published