Skip to content

feat: send X-Tower-Idempotency-Key on deploy#303

Open
bradhe wants to merge 3 commits into
developfrom
feature/deploy-idempotency-key
Open

feat: send X-Tower-Idempotency-Key on deploy#303
bradhe wants to merge 3 commits into
developfrom
feature/deploy-idempotency-key

Conversation

@bradhe

@bradhe bradhe commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Wires the tower deploy command to send an X-Tower-Idempotency-Key header so consecutive deploys of identical source (e.g. to staging then production) reuse a single AppVersion server-side instead of creating a new version each time. The server already honors this header; the CLI just didn't send it.

Behavior

  1. Auto-populate from git when safe. If the deploy directory is inside a git worktree:
    • Clean tree → send X-Tower-Idempotency-Key: <git HEAD SHA>.
    • Dirty tree → omit the header, so every deploy creates a new version (provenance is never misrepresented).
  2. Manual override. --idempotency-key <value> wins over auto-detection — useful for CI that builds artifacts outside a git checkout.
  3. Opt-out. --no-idempotency-key suppresses the header even on a clean tree (conflicts with --idempotency-key).
  4. Stdout hint. When the server returns a version whose key matches what we sent and whose created_at predates this deploy, the CLI prints a line explaining that an existing version was reused (suppressed in JSON mode).

The MCP tower_deploy tool gets the same git auto-detection.

Implementation notes

  • New util::git::clean_head_sha() shells out to git (no new dependencies); returns None when not in a repo, the tree is dirty, or git is unavailable.
  • Key flows deploy_from_dir → do_deploy_package → deploy_app_package → upload_file_with_progress, which attaches the header when present.

Wire up the deploy command to send an idempotency key so consecutive
deploys of unchanged source (e.g. to staging then production) collapse to
a single AppVersion server-side instead of creating a new version each time.

- Auto-populate the key from the git HEAD SHA when the working tree is
  clean; omit it on a dirty tree so provenance is never misrepresented.
- Add --idempotency-key to override detection (useful for CI building
  outside a checkout) and --no-idempotency-key to opt out on a clean tree.
- Print a hint when the server reuses an existing version so the user
  understands why no new version was created.
- Apply the same git auto-detection to the MCP deploy tool.
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bc94438a-7f39-40d8-8cb8-b0cbbc82bdc9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/deploy-idempotency-key

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

bradhe added 2 commits June 18, 2026 17:19
BDD regression tests for the X-Tower-Idempotency-Key deploy behavior:
explicit --idempotency-key, --no-idempotency-key opt-out, git auto-detect
on a clean tree, header omission on a dirty tree, and the reuse hint on a
repeat deploy with the same key.

The mock API server now records the idempotency key seen on each deploy
and reuses a stored (backdated) version when the same key recurs, exposing
both via test-only inspection endpoints.

@socksy socksy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please have a look at the output function comment, but otherwise looks good.

}

Some(format!(
"Reusing version `{}` (deployed {}) — no source changes since key {}.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

since this is not actually true, but is rather that the git repository doesn't have any committed changes, we should probably mention "git"


// Only emit the informational hint in normal mode; in JSON mode a
// bare line would corrupt the structured output.
if output::get_output_mode().is_normal() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FWIW I think if you're using this function to work out what to write out, then it's almost certainly a code smell (it's public so that we can check for ctrl-c behaviour differently, but wanting to only print things out if it's not json or mcp mode seems to me like an output.rs specific concern).

Additionally, in this case would it make sense to either re-use output::success, or to print it to stderr with output::write_to_stderr? If not, perhaps a simple function addition to output that writes only informational text to output_mode().is_normal() texts would be better?

// Auto-detect the idempotency key from git (clean tree HEAD) just like
// the CLI deploy command does, so repeated deploys of unchanged source
// collapse to a single AppVersion server-side.
let idempotency_key = crate::util::git::clean_head_sha(&working_dir);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

would be good to do a SHA of the SHAs for the idempotency_key when git is dirty. Alternatively, swap the warning around to tell the user that because their git repo is not clean, the whole bundle is being re-uploaded (current way around you'd only know this behaviour exists if you pushed a clean repo, and given we state that even an untracked file is enough to make a repo dirty, some people may never hit upon this behaviour and wonder why tower doesn't support it)

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