Skip to content

Keep the on-$PATH miren CLI in sync with the self-managed server#969

Merged
phinze merged 1 commit into
mainfrom
phinze/mir-1275-keep-the-on-path-miren-cli-in-sync-with-the-self-managed
Jul 24, 2026
Merged

Keep the on-$PATH miren CLI in sync with the self-managed server#969
phinze merged 1 commit into
mainfrom
phinze/mir-1275-keep-the-on-path-miren-cli-in-sync-with-the-self-managed

Conversation

@phinze

@phinze phinze commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

The install docs put the miren binary on $PATH at /usr/local/bin/miren and tell you to use it directly. That's fine until the first upgrade: miren server upgrade installs the new payload to /var/lib/miren/release/miren, repoints systemd, restarts, and health-checks, but it never advances the /usr/local/bin/miren the docs told you to install. From then on the CLI on $PATH silently drifts from the running server. We hit exactly this on one of our own clusters, where the server was current but /usr/local/bin/miren was a stale build missing commands the server had.

Since miren is all about self-managing a deploy, it should own getting a current CLI onto $PATH too. So miren server install and miren server upgrade now maintain /usr/local/bin/miren as a symlink to the managed release binary. The same path covers runner and coordinator upgrades, which is exactly where you SSH in and want a matching CLI.

A symlink beats a refresh-copy because it's self-correcting: it targets the path, not an inode, so it tracks whatever install or rollback last dropped at /var/lib/miren/release/miren, including after an auto-rollback. And because upgrades swap the binary via atomic rename, a miren invoked through the symlink keeps its already-mapped inode, so replacing the target under a running process is safe on Linux. It's maintained best-effort after a healthy upgrade, so a symlink failure warns rather than failing an otherwise-good server.

On existing boxes /usr/local/bin/miren is a real stale file today; the helper atomically replaces it, so the first upgrade after this ships heals the drift on its own. We stay polite about it: we only take over the path when it's a plain file, missing, or already our own symlink. A symlink another tool owns (a Homebrew Cask linking into its Caskroom, say) is left alone. macOS never reaches this code anyway, since the server commands are Linux-only.

Closes MIR-1275

The install docs put the miren binary on $PATH at /usr/local/bin/miren
and tell you to use it directly. But `miren server upgrade` only
advances the managed payload at /var/lib/miren/release/miren; it never
touched the CLI the docs told you to install, so after the first upgrade
the CLI on $PATH silently drifts from the running server. We hit exactly
this on one of our own clusters: the server was current while
/usr/local/bin/miren was a stale build missing newer commands.

Since miren self-manages the deploy, it should own the on-$PATH CLI too.
`server install` and `server upgrade` (which also covers runner and
coordinator upgrades) now maintain /usr/local/bin/miren as a symlink to
the managed release binary. A symlink is self-correcting: it targets the
path, so it tracks whatever install or rollback last put there, and
because upgrades swap the binary via atomic rename a miren invoked
through the symlink keeps its mapped inode. It's best-effort after a
healthy upgrade, so a hiccup never fails or rolls back a good server,
and we leave a foreign symlink (e.g. a Homebrew Cask) alone rather than
stepping on another tool.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1f237ad2-c883-4e85-86e4-484a8b01869a

📥 Commits

Reviewing files that changed from the base of the PR and between 6d8e830 and ea09787.

📒 Files selected for processing (4)
  • cli/commands/server_install.go
  • pkg/release/manager.go
  • pkg/release/symlink.go
  • pkg/release/symlink_test.go

📝 Walkthrough

Walkthrough

Adds a release utility that creates, refreshes, and atomically replaces the managed CLI symlink while preserving foreign symlinks. Server upgrades and installations now invoke this utility as a best-effort post-operation step. Upgrade configuration includes a PathSymlink field defaulting to /usr/local/bin/miren. Tests cover creation, replacement, foreign symlink handling, and idempotency.


Comment @coderabbitai help to get the list of available commands.

@miren-code-agent miren-code-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍪 biscuit: ⚠️ ready with caveats — auto-review, non-blocking

This is a draft, so I'm judging readiness to hand off to human review. The core logic is solid and I'd be comfortable with most of it, but there's one concrete gap worth flagging before this graduates.

What's here and what I checked

The PR adds pkg/release/symlink.go — a self-contained EnsurePathSymlink function — and wires it into two places: ServerInstall (linux CLI) and Manager.UpgradeServer (the upgrade path). The atomic swap via os.CreateTemp + os.Remove + os.Symlink + os.Rename is the right pattern for keeping an in-use binary's inode alive during replacement on Linux. The ErrPathManagedElsewhere sentinel and the symlinkPointsInto guard are well-reasoned: they prevent silently hijacking a Homebrew/Cask-managed link. The test coverage in symlink_test.go is thorough — it covers creation, stale-file replacement, stale-managed-symlink replacement, foreign-symlink preservation, and idempotency.

The one real gap: ServerUninstall doesn't remove the symlink

When miren server uninstall runs, it tears down the systemd service and optionally removes /var/lib/miren, but it never touches /usr/local/bin/miren. After uninstall, the symlink points at a binary that no longer exists (or will be removed). A user who then types miren gets a broken command with no obvious hint about what happened. Given that the PR specifically creates the symlink during install, cleaning it up on uninstall is the expected counterpart. It's not a security issue, but it is a correctness gap that a human reviewer will almost certainly ask about.

One minor implementation note (non-blocking)

In EnsurePathSymlink, os.CreateTemp opens a file and returns an *os.File. The code calls tmp.Close() but there's a window between Close and os.Remove where the temp file sits on disk open-file-descriptorless. This is fine on Linux (the file still exists until Remove) and the code handles it correctly — I just want to note the sequence is correct, not a bug.

Summary

The symlink mechanism itself is ready. The missing ServerUninstall cleanup is the one thing I'd want addressed (or at least consciously deferred with a TODO) before this goes to a human reviewer.


🍪 full review note · comment /biscuit review to run biscuit again.

Inline comments

cli/commands/server_install.go:558

ServerInstall creates /usr/local/bin/miren as a symlink, but ServerUninstall never removes it. After uninstalling, miren on $PATH points at a deleted binary. Consider calling os.Remove(release.SystemCLIPath) (or a dedicated release.RemovePathSymlink) during uninstall — but only when it is the symlink we own (i.e. points into releaseBinPath's directory), to avoid removing a link belonging to another tool.

🤖 Prompt for AI Agents
In cli/commands/server_install.go, the
ServerUninstall function (starting around line
558) does not clean up the /usr/local/bin/miren
symlink that ServerInstall creates. Add a step at
the end of ServerUninstall (before the final
return nil) that removes release.SystemCLIPath if
and only if it is a symlink pointing into
/var/lib/miren/release (i.e. use
release.EnsurePathSymlink's existing
symlinkPointsInto helper or an equivalent check
via os.Readlink). If the symlink belongs to
another tool, leave it alone and emit a ctx.Info
note instead. If the remove fails, emit a ctx.Warn
rather than returning an error, consistent with
the best-effort pattern used in ServerInstall.

@phinze
phinze marked this pull request as ready for review July 24, 2026 17:57
@phinze
phinze requested a review from a team as a code owner July 24, 2026 17:57
@phinze
phinze merged commit bec1aba into main Jul 24, 2026
24 checks passed
@phinze
phinze deleted the phinze/mir-1275-keep-the-on-path-miren-cli-in-sync-with-the-self-managed branch July 24, 2026 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants