feat(tools): standalone tvc-deploy helper + manual deploy workflow#395
feat(tools): standalone tvc-deploy helper + manual deploy workflow#395prasanna-anchorage wants to merge 9 commits into
Conversation
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>
…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>
There was a problem hiding this comment.
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 thetvcCLI. - Adds a
workflow_dispatchGitHub Actions workflow to build/run the helper with input parameters and scrub the operator seed afterward. - Vendors a lockfile and minimal
.gitignoreto 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.
| validate_digest(&digest)?; | ||
| let (seed_path, cleanup_seed) = resolve_seed_file(flags)?; | ||
|
|
| fn path(p: &Path) -> &str { | ||
| p.to_str().unwrap_or_default() | ||
| } |
| - 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>
|
Addressed the review feedback in
Also reorganized the helper onto the idiomatic shell-orchestration stack ( |
…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>
| 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())) | ||
| } |
| 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(()) | ||
| } |
| 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")?; |
| 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())) | ||
| } |
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>
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>
Summary
A standalone Rust helper + manual workflow to perform dev/staging
parser_appTVC deploys, plus a post-deploy smoke check.tools/tvc-deploy/— standalone helper (xshell + anyhow + lexopt; own workspace, novisualsigndeps)gen-operator-key --out <path>: mint aqos_p256operator key (seed → 0600 file, only the public key printed).deploy …: image-derived digest gate — extract/parser_appfrom the image,sha256it, and assert it equals--expected-digestbefore anything is submitted (ties the deployed digest to the actual binary) — then assembletvc-deploy.json(gRPC health),tvc deploy create→ approve → poll-to-healthy →set-live..github/workflows/tvc-deploy.yml— manual + label-gated deployworkflow_dispatch(explicit inputs) and atvc-deploy-test-labeledpull_requestpath (test app viavars.TVC_TEST_*). Pinstvc --version 0.7.0 --locked; minimal permissions.turnkey-clientcontainerparse --dev-pathagainst/visualsign-devand asserts the V0+ALT fixture renders (regression guard for "Cannot render V0"), reusingTVC_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