Skip to content

feat(tools): standalone tvc-deploy helper + manual deploy workflow#395

Open
prasanna-anchorage wants to merge 9 commits into
mainfrom
feat/tvc-deploy-helper
Open

feat(tools): standalone tvc-deploy helper + manual deploy workflow#395
prasanna-anchorage wants to merge 9 commits into
mainfrom
feat/tvc-deploy-helper

Conversation

@prasanna-anchorage

@prasanna-anchorage prasanna-anchorage commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

A standalone Rust helper + manual workflow to perform dev/staging parser_app TVC deploys, plus a post-deploy smoke check.

tools/tvc-deploy/ — standalone helper (xshell + anyhow + lexopt; own workspace, no visualsign deps)

  • gen-operator-key --out <path>: mint a qos_p256 operator key (seed → 0600 file, only the public key printed).
  • deploy …: image-derived digest gate — extract /parser_app from the image, sha256 it, and assert it equals --expected-digest before anything is submitted (ties the deployed digest to the actual binary) — then assemble tvc-deploy.json (gRPC health), tvc deploy create → approve → poll-to-healthy → set-live.

.github/workflows/tvc-deploy.yml — manual + label-gated deploy

  • workflow_dispatch (explicit inputs) and a tvc-deploy-test-labeled pull_request path (test app via vars.TVC_TEST_*). Pins tvc --version 0.7.0 --locked; minimal permissions.
  • Smoke step (post set-live): runs the published turnkey-client container parse --dev-path against /visualsign-dev and asserts the V0+ALT fixture renders (regression guard for "Cannot render V0"), reusing TVC_API_KEY_*; outage abort-guard. Requires the turnkey-client image on GHCR.

Validated: helper proven end-to-end on a throwaway dev app and via a green labeled CI run against the test app 80b6f48f; smoke logic validated live (35,177 chars rendered).

🤖 Generated with Claude Code

prasanna-anchorage and others added 2 commits June 23, 2026 07:17
Lean, fast-compiling Rust binary (own workspace; deps: qos_p256 + serde_json
only, no visualsign crates) that owns the parser_app TVC deploy flow:

- gen-operator-key: mint a qos_p256 operator key; seed -> 0600 file, only the
  public key is printed (never the seed).
- deploy: assemble tvc-deploy.json (gRPC health), create the deployment, gate
  on the manifest pivot hash matching --expected-digest before approving,
  approve, poll until replicas are healthy, then set live. Retries the
  transient TVC states (status-not-ready, zero-healthy-replicas) and tolerates
  the fresh-app auto-target. Shells out to the `tvc` CLI for API actions.

Proven end-to-end against a throwaway dev app (create -> digest gate -> approve
-> 3/3 -> live), which was then deleted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A workflow_dispatch workflow to exercise dev/staging parser_app deploys on
demand: builds the standalone tvc-deploy helper and runs create -> digest
safety-gate -> approve -> poll-to-healthy -> set-live with operator/API-key
secrets. Inputs (app_id, image_url, expected_digest, operator_id, qos/host/port)
let an operator trigger a deploy with args. Dev/staging only; prod stays manual.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 23, 2026 07:19
Comment thread .github/workflows/tvc-deploy.yml Fixed
…code

Drop the hand-rolled hex_encode; use P256Pair::to_master_seed_hex() and
P256Public::to_hex_bytes() (qos_p256 owns these formats). No new deps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Adds a standalone Rust helper and a manually-triggered GitHub Actions workflow to perform dev/staging TVC deployments of parser_app, including manifest digest gating and health polling, without pulling in the main src/ Rust workspace.

Changes:

  • Introduces tools/tvc-deploy/ as an independent Rust crate to generate operator keys and orchestrate TVC deploy flows via the tvc CLI.
  • Adds a workflow_dispatch GitHub Actions workflow to build/run the helper with input parameters and scrub the operator seed afterward.
  • Vendors a lockfile and minimal .gitignore to keep the helper self-contained and fast to build.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tools/tvc-deploy/src/main.rs Implements the deploy helper (flag parsing, config generation, digest gate, approve, poll health, set-live).
tools/tvc-deploy/Cargo.toml Declares the standalone crate and its minimal dependencies, with its own workspace root.
tools/tvc-deploy/Cargo.lock Locks the helper’s dependency graph for reproducible builds.
tools/tvc-deploy/.gitignore Ignores the helper’s local target/ directory.
.github/workflows/tvc-deploy.yml Adds a manual CI workflow to install tvc, build the helper, run deploy, and scrub the seed.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tools/tvc-deploy/src/main.rs Outdated
Comment on lines +135 to +137
validate_digest(&digest)?;
let (seed_path, cleanup_seed) = resolve_seed_file(flags)?;

Comment thread tools/tvc-deploy/src/main.rs Outdated
Comment on lines +352 to +354
fn path(p: &Path) -> &str {
p.to_str().unwrap_or_default()
}
Comment on lines +54 to +56
- name: Install tvc CLI
run: cargo install tvc

…gate

Reorganize the deploy helper on the idiomatic shell-orchestration stack
(xshell cmd! + anyhow + lexopt), dropping the hand-rolled process wrappers,
String errors, HashMap flag parser, and the to_str()-lossy path() helper.

Replace the fragile `approve --dry-run` manifest-hash parse with an
image-derived digest gate: extract /parser_app from the image and sha256 it,
asserting it equals --expected-digest before deploying (ties the digest to the
actual binary).

Addresses review on #395:
- workflow: add minimal `permissions: contents: read`; pin `cargo install tvc
  --version 0.6.2 --locked` (reproducible, no drift).
- deploy(): move config-write inside the cleanup closure so an early error
  never leaves the operator seed temp file on disk.
- path() (lossy non-UTF-8 -> "") removed; xshell takes Path directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@prasanna-anchorage

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback in 6c4aef8d:

  • CodeQL (missing permissions): added a minimal permissions: { contents: read } to the workflow.
  • cargo install reproducibility (+ pin request): now cargo install tvc --version 0.6.2 --locked.
  • Operator-seed temp file could leak on early error: moved config assembly/write inside the cleanup closure, so the seed + config temp files are always removed regardless of where the flow fails.
  • path() lossy non-UTF-8 → empty arg: removed entirely — the helper was reorganized onto xshell (cmd!), which takes Path/PathBuf directly, so there's no to_str() fallback path anymore.

Also reorganized the helper onto the idiomatic shell-orchestration stack (xshell + anyhow + lexopt, all small/fast-compiling) and replaced the fragile approve --dry-run manifest parse with an image-derived digest gate (extract /parser_app, sha256, assert == --expected-digest) — stronger, ties the digest to the actual binary.

…label

Add a label-gated pull_request trigger (mirrors the stagex pattern) so the
deploy workflow can be exercised from a PR before merge: applying the
`tvc-deploy-test` label runs a deploy against the dev TEST app using repo
vars.TVC_TEST_* (never the live app). workflow_dispatch keeps explicit inputs;
the job `if` gates to dispatch or the specific label, and a concurrency group
prevents overlapping deploys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 5 changed files in this pull request and generated 4 comments.

Comment on lines +102 to +112
fn write_secret_file(path: &Path, contents: &str) -> Result<()> {
let mut f = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(path)
.with_context(|| format!("open {}", path.display()))?;
f.write_all(contents.as_bytes())
.with_context(|| format!("write {}", path.display()))
}
Comment on lines +193 to +215
fn verify_image_digest(sh: &Shell, image: &str, expected: &str) -> Result<()> {
let cid = cmd!(sh, "docker create {image} /bin/true")
.read()
.context("docker create (digest gate)")?;
let cid = cid.trim().to_owned();
let bin = temp_path("parser_app", "bin");
let target = format!("{cid}:/parser_app");
let cp = cmd!(sh, "docker cp {target} {bin}")
.run()
.context("docker cp /parser_app");
let _ = cmd!(sh, "docker rm {cid}").ignore_status().quiet().run();
cp?;
let sha = cmd!(sh, "sha256sum {bin}").read().context("sha256sum")?;
let _ = std::fs::remove_file(&bin);
let actual = sha.split_whitespace().next().unwrap_or_default();
if !actual.eq_ignore_ascii_case(expected) {
bail!(
"DIGEST GATE FAILED: image /parser_app sha256 {actual} != expected {expected}; refusing to deploy"
);
}
println!("digest gate passed: image /parser_app sha256 == {expected}");
Ok(())
}
Comment on lines +165 to +171
let deploy_id = parse_after(&created, "Deployment ID:")
.with_context(|| format!("no deployment id in create output:\n{created}"))?;
println!("created deployment {deploy_id}");

cmd!(sh, "tvc deploy approve --deploy-id {deploy_id} --operator-id {operator_id} --operator-seed {seed_path} --dangerous-skip-interactive")
.run()
.context("tvc deploy approve")?;
Comment on lines +335 to +341
fn temp_path(prefix: &str, ext: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
std::env::temp_dir().join(format!("{prefix}-{}-{nanos}.{ext}", std::process::id()))
}
@prasanna-anchorage prasanna-anchorage added the tvc-deploy-test Trigger a TVC test-app deploy from this PR label Jun 23, 2026
crates.io tvc 0.6.2 takes `deploy create <CONFIG_FILE>` positionally, but the
helper (and the locally-built tvc) use `--config-file`. 0.7.0 is the published
release that carries the `--config-file` flag interface; pin to it so CI matches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@prasanna-anchorage prasanna-anchorage added tvc-deploy-test Trigger a TVC test-app deploy from this PR and removed tvc-deploy-test Trigger a TVC test-app deploy from this PR labels Jun 23, 2026
prasanna-anchorage and others added 3 commits June 24, 2026 20:08
Add a smoke step to the TVC deploy workflow that parses a known Solana V0+ALT
transaction through the deployed dev parser (/visualsign-dev) and asserts it
renders — a regression guard for the "Cannot render V0" failure. Drives the
published turnkey-client container (no Go), reconstructing the parse API key
from the existing TVC_API_KEY_* secrets; abort guard skips on a transport
outage. Requires the turnkey-client image on GHCR (visualsign-turnkeyclient).

- scripts/smoke.sh: container-driven parse + jq assertions.
- testdata/solana_v0_alt.b64: the V0+ALT fixture.
- tvc-deploy.yml: Smoke step (+ packages: read) and key scrub.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- write_secret_file: explicitly chmod 0600 after open (mode() only applies on
  create; a pre-existing broader-perm file could leave the seed world-readable).
- verify_image_digest: always remove the extracted temp binary + rm the
  container, even when docker cp / sha256sum fails early.
- temp_path: add a per-process atomic counter so same-tick calls can't collide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bound the job (cargo install tvc + poll-to-healthy + smoke) so a hung step
can't run indefinitely.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tvc-deploy-test Trigger a TVC test-app deploy from this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants