Skip to content

Managed Hugo Upgrades

jmrGrav edited this page Aug 10, 2026 · 4 revisions

Managed Hugo Upgrades

Since #980/#990 (v1.8.3), the server can optionally stage, activate, and roll back official Hugo releases through four tools: get_hugo_update, stage_hugo_upgrade, activate_hugo, rollback_hugo. This page is the runbook for the actual arleo.eu deployment — for the full config reference see docs/operator-guide.md § "Managed Hugo Upgrade Configuration" (that table is the source of truth for field meanings; this page is not a copy of it, see Documentation Source of Truth).

What This Does and Does Not Do

  • get_hugo_update is read-only and safe to call anytime. check_latest:true makes one bounded outbound request to the official Hugo GitHub API.
  • stage_hugo_upgrade downloads, checksum-verifies, and extracts a release into a private managed directory. It never touches the live hugo binary.
  • activate_hugo atomically swaps a managed symlink — never /usr/local/bin/hugo or a package-manager path — and never restarts the service itself. An explicit operator restart is required afterward for the swap to take effect.
  • rollback_hugo atomically restores the previous managed target, same no-auto-restart rule.

All four require a write bearer. Staging/activation/rollback additionally require hugo_upgrade.enabled: true in config — status reporting works even when disabled.

Why the systemd PATH Change Is Required

build_site invokes the bare command hugo and lets the OS resolve it via $PATH (internal/tools/admin/build.go). activate_hugo only ever changes where hugo_upgrade.binary_link points — if that symlink's directory isn't first on the service's PATH, an activation "succeeds" but the next build_site silently keeps using whatever hugo resolved to before. This is exactly the kind of drift class that caused the #981/#983/#984 incidents earlier in the v1.8.x cycle — verify this before trusting an activation.

Live Configuration (hugo-vm)

/etc/systemd/system/mcp-hugo-server-go.service.d/hugo-managed-path.conf
/etc/mcp-hugo-server-go/config.yaml   (hugo_upgrade: block, appended)

The drop-in sets:

[Service]
Environment=PATH=/var/lib/mcp-hugo-server-go/hugo/current:/usr/local/bin:/usr/bin:/bin

/var/lib/mcp-hugo-server-go was already in the base unit's ReadWritePaths (used for the OAuth token store and other state), so managed_dir needed no new systemd grant — only the PATH drop-in.

config.yaml has:

hugo_upgrade:
  enabled: true
  managed_dir: /var/lib/mcp-hugo-server-go/hugo
  binary_link: /var/lib/mcp-hugo-server-go/hugo/current/hugo
  require_extended: true
  allow_downgrade: false

After editing either file:

ssh hugo-vm 'sudo systemctl daemon-reload && sudo systemctl restart mcp-hugo-server-go'

Verification Performed (2026-08-10, capability enabled — not yet activated)

Enabling this was config-only: prepending an empty managed directory to PATH is safe, since a missing/empty PATH entry is simply skipped by lookup and /usr/local/bin/hugo (the existing install) resolves exactly as before. Confirmed in this order, live:

build_site                                    -> status: ok (proves PATH change didn't break Hugo resolution)
get_hugo_update(check_latest=true)            -> managed_upgrades_enabled: true, installed 0.147.0, latest 0.164.0 available
stage_hugo_upgrade(v0.164.0, dry_run=false)   -> checksum_verified: true, version_verified: true, staged: true

activate_hugo was deliberately not called — enabling the capability is not the same as performing an upgrade. The staged v0.164.0 sits at /var/lib/mcp-hugo-server-go/hugo/versions/v0.164.0/, mode 0700, owned by the mcp-hugo-server-go service account (confirmed via sudo stat; the jm operator account correctly cannot read it directly — that's the design, not a bug).

To Actually Upgrade

get_hugo_update(check_latest=true)
stage_hugo_upgrade(target_version="vX.Y.Z", dry_run=false)
activate_hugo(target_version="vX.Y.Z", dry_run=false)
ssh hugo-vm 'sudo systemctl restart mcp-hugo-server-go'
get_hugo_update    # confirm installed.version now matches
build_site         # confirm the new binary actually builds

If anything looks wrong after restart, rollback_hugo(dry_run=false) then restart again — the previous binary's checksum is re-verified before the symlink is restored, so a tampered or missing previous binary fails closed rather than silently reactivating something unverified.

Common Mistake

Do not enable hugo_upgrade.enabled: true without also adding the PATH drop-in first — the tools will happily stage and "activate" a new Hugo version whose binary build_site never actually picks up, and you'll spend time debugging a phantom version mismatch instead of a one-line systemd config gap.

Clone this wiki locally