Skip to content

Serialize deploys with a staging lock#13

Merged
miharp merged 1 commit into
mainfrom
feat/deploy-staging-lock
Jul 24, 2026
Merged

Serialize deploys with a staging lock#13
miharp merged 1 commit into
mainfrom
feat/deploy-staging-lock

Conversation

@miharp

@miharp miharp commented Jul 24, 2026

Copy link
Copy Markdown
Owner

First half of the deploy-API split. Before adding a third deploy front door (the API), make concurrent deploys safe.

Why

A deploy can now come from the command or the webhook, and soon the API. r10k rewrites the staging directory in place, so two overlapping deploys corrupt each other's trees. Unlike a mid-deploy read on the serve side — where the agent's verify-by-reseal rejects a bad artifact — there is no backstop for a tree r10k is halfway through writing. It must be prevented.

What

deploy.Run takes an exclusive flock on <state>/deploy.lock across r10k, sealing, and --wait. Every deploy path on a host — CLI, webhook, future API — serializes through one lock, not just within a single daemon's worker.

  • Polls with LOCK_NB + a timeout (DefaultLockTimeout 10m, r10k is slow) rather than blocking in the syscall, so it gives up on a wedged deploy instead of hanging.
  • flock is advisory and released on process exit, so a crashed deploy doesn't leave the lock stuck.

Test

TestConcurrentDeploysSerialize runs two deploys against one staging directory concurrently, with a fake r10k that fails if it ever sees a second copy running. It passes only because they serialize (runtime ≈ 2× the fake's work, not 1×).

Verified: go test -race ./..., golangci-lint 0 issues, gofmt/vet clean, markdownlint clean.

Next PR: the deploy API (POST /v1/deploys + status), unifying the webhook and API into one deploy-server daemon that shares this now-safe deploy path.

🤖 Generated with Claude Code

Now that a deploy can be triggered from the command, the webhook, and soon a
deploy API, more than one can run at once. r10k rewrites the staging directory
in place, so two overlapping deploys corrupt each other's trees — and unlike a
mid-deploy read on the serve side, there is no verify-by-reseal backstop for a
tree r10k itself is halfway through writing.

deploy.Run now takes an exclusive flock on <state>/deploy.lock across r10k,
sealing, and the --wait, so every deploy path on a host serializes through one
lock rather than only within a single daemon. The lock polls with LOCK_NB and a
timeout instead of blocking in the syscall, so it gives up on a wedged deploy
rather than hanging; flock is released on process exit, so a crash does not
leave it stuck.

The test runs two deploys against one staging directory concurrently, with a
fake r10k that fails if it ever sees a second copy running — proving they
serialize.

This is the first half of the deploy-API split: the API adds a third deploy
front door, and this makes concurrent front doors safe before that lands.

Signed-off-by: Michael Harp <mike@mikeharp.com>
@miharp
miharp merged commit f01712f into main Jul 24, 2026
7 checks passed
@miharp
miharp deleted the feat/deploy-staging-lock branch July 24, 2026 13:50
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.

1 participant