release: v1.0.0#134
Draft
seanmcgary wants to merge 61 commits into
Draft
Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrote the top-up command to support both USDC and credit card payment methods. Users can now choose between on-chain USDC payment or credit card checkout. Changes: - Added method flag to select payment method (usdc or card) - Extracted USDC flow into handleUsdc() method - Added handleCard() method for credit card checkout flow - Added pollForCredits() helper to share polling logic - Updated description and examples - Integrated with new SDK methods: getPaymentMethods and purchaseCredits Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
c09e81b to
1f55c5b
Compare
### New CLI Commands **User-facing:** - `ecloud billing redeem-coupon [--code CODE]` — redeem a coupon code for credits (prompts interactively if no `--code` flag) **Admin (requires admin privileges):** - `ecloud admin coupons create --amount <dollars>` — create a new coupon - `ecloud admin coupons list [--active] [--redeemed] [--limit N] [--offset N]` — list coupons with optional filters - `ecloud admin coupons get <id>` — get coupon details - `ecloud admin coupons deactivate <id>` — deactivate a coupon - `ecloud admin coupons redeem <id> --address <wallet>` — redeem a coupon on behalf of a user - `ecloud admin admins add <address>` — grant admin privileges - `ecloud admin admins remove <address>` — revoke admin privileges - `ecloud admin admins list` — list all admins ### SDK Changes - `BillingApiClient` — added methods for all admin and coupon REST endpoints - `AdminModule` — new module wrapping admin API surface (`createAdminModule`) - `BillingModule` — added `redeemCoupon(code)` method - New types: `AdminCoupon`, `AdminUser`, `CreateCouponResponse`, `ListCouponsResponse`, `GetCouponResponse`, `AddAdminResponse`, `ListAdminsResponse`, `RedeemCouponResponse`
The `billing cancel` command was cherry-picking only `private-key` and `verbose` from commonFlags, leaving `--environment`, `--rpc-url`, `--max-fee-per-gas`, `--max-priority-fee`, and `--nonce` undeclared. This made the command unusable in CI/scripted flows because there was no non-interactive way to pick the environment. Spread `...commonFlags` like the other billing subcommands (status, subscribe, top-up, list-cards) so all common flags are accepted; keep `product` and `force` flags on top.
The 'ecloud compute app upgrade' command would hang indefinitely after submitting the on-chain transaction whenever the orchestrator was silent (15+ minutes observed in the wild) — there was no deadline, no progress between status transitions, and no recovery hint. - Bound watchUntilUpgradeComplete with a deadline (default 10 minutes, overridable via ECLOUD_WATCH_TIMEOUT_SECONDS env var or an explicit timeoutSeconds option). - Log every status transition on its own line with elapsed seconds, mirroring watchUntilRunning. - On timeout, throw a typed WatchUpgradeTimeoutError carrying appId, lastStatus, elapsedSeconds, and timeoutSeconds. - CLI catches the timeout, prints a recovery hint pointing the user to 'ecloud compute app info <id>' along with txHash and appId, and exits non-zero. Success path is unchanged.
watchUntilRunning previously only logged on status transitions, so when the orchestrator silently kept the app in Unknown the user saw a single "Status: Unknown (1s)" line forever (visible especially over non-TTY stdout where carriage-return overwrites are invisible). The loop also had no timeout, so the CLI would hang indefinitely. Add a 30s heartbeat that re-emits the current status with elapsed time, plus a configurable timeout (default 10 minutes, override via ECLOUD_WATCH_TIMEOUT_SECONDS) that throws a typed WatchTimeoutError. The CLI deploy command catches it and prints a hint pointing at 'ecloud compute app info <id>' before exiting non-zero. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sepolia-dev AppController was upgraded to v1.5.x (eigenx-contracts KMS-006, PR #15), which added a 4th field `containerPolicy` to the on-chain `Release` struct. This changed the `createApp` selector from 0xa60daa8f to 0x5e92a19f (and likewise createAppWithIsolatedBilling / upgradeApp). The SDK still shipped the 3-field ABI, so every deploy/upgrade encoded the old selector and the upgraded contract reverted with empty revert data — opaque "execution reverted" with no decodable reason. Changes: - Replace the vendored AppController.json with the v1.5.x ABI generated from eigenx-contracts master (createApp/createAppWithIsolatedBilling/upgradeApp now take the 4-field Release; adds createEmptyApp, confirmUpgrade, etc.). All SDK-used functions remain present. - Add ContainerPolicy / EnvVar types + EMPTY_CONTAINER_POLICY default, and an optional `containerPolicy` field on Release (backwards-compatible: callers that omit it get an empty policy that preserves the image's own entrypoint/env). - Encode containerPolicy at both release-encoding sites in caller.ts (prepareDeployBatch / prepareUpgradeBatch) via a shared helper. - Add release-encoding regression test pinning the 0x5e92a19f selector and the 4-field encoding so this drift cannot silently return. Verified E2E on sepolia-dev: deploy now passes the on-chain createApp step (app created on-chain, status STARTED) where it previously bare-reverted. The follow-on "Failed state" during TEE provisioning is unrelated to this ABI fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1.5) The first commit swapped the vendored ABI wholesale to v1.5.x, which would have broken sepolia / mainnet-alpha — both still run AppController v1.4.0 (3-field Release, createApp selector 0xa60daa8f). Only sepolia-dev is on v1.5.x (4-field Release + containerPolicy, selector 0x5e92a19f). Verified on-chain via each controller's version() and createApp selector. Make the SDK support BOTH formats, selected per environment: - Keep the v1.5 ABI as AppController.json and re-add the v1.4 ABI as AppController.v1_4.json (the two also differ on getApps AppConfig shape, so the whole ABI is selected, not just the create/upgrade entries). - Add `releaseAbiVersion?: "v1.4" | "v1.5"` to EnvironmentConfig; set sepolia-dev = v1.5, sepolia + mainnet-alpha = v1.4. Omitted defaults to v1.5. - caller.ts: appControllerAbiFor(env) picks the ABI; releaseForViem(release, env) includes containerPolicy only on v1.5. All read/lifecycle calls also route through the version-aware ABI. - Drop the now-unused `Address` import in environment.ts (pre-existing lint error in a file this change touches). Tests: per-version encoding (0xa60daa8f vs 0x5e92a19f, containerPolicy round-trip, arity guards) + env→ABI selection through getEnvironmentConfig. 31 SDK tests pass; tsc + eslint clean on changed files. E2E verified on both: sepolia-dev (v1.5) and sepolia-prod (v1.4) each created an app on-chain (status STARTED) where the wholesale-swap build would have reverted on one of them. (Provisioning "Failed state" for the bare nginx test image is unrelated to the ABI.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Folding the RND-568 (upgrade watch) and RND-569 (deploy watch) branches both introduced the same watcher primitives — WATCH_DEFAULT_TIMEOUT_SECONDS, WatchTimeoutError, resolveWatchTimeoutSeconds — and a duplicate WatchTimeoutError re-export in client/index.ts. The 3-way merge kept both copies (different line positions), which would not compile. Keep the single shared definition near the top of watcher.ts (the superset: includes the heartbeat interval and optional message) and the single export on line 53; remove the RND-568 duplicates. Both watchUntilRunning and watchUntilUpgradeComplete now use the one shared WatchTimeoutError/resolveWatchTimeoutSeconds. SDK tsc clean; sdk+cli build; 23 sdk + 61 cli tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(RND-564)
Non-interactive `compute app deploy` / `upgrade` (CI, scripted use, agents)
previously failed one flag at a time: each run errored "Cannot prompt in
non-interactive mode" for the next unset optional flag, so callers had to add
a flag, retry, hit the next prompt, and repeat. `--force` only short-circuited
the verifiable-build confirmation, contradicting its "skip all prompts" help.
Each optional prompt that has a safe default now falls back to it (with a
single warning line) when there is no TTY, instead of throwing:
- --dockerfile/--image-ref: build from the discovered ./Dockerfile
- --env-file: no env file (".env" auto-detect unchanged)
- --log-visibility: private (never silently public)
- --resource-usage-monitoring: disable
Required inputs with no safe default (--instance-type, image source, app
name/id) still error via ensureInteractive — but now as a single error after
the defaultable prompts resolve, not buried mid-cascade.
Also fixes the ticket's symptom #3: when --image-ref is provided without
--dockerfile, deploy/upgrade now skip the Dockerfile prompt entirely so a
stray Dockerfile in the working directory no longer hijacks an existing-image
deploy (interactively a spurious "build or deploy existing?" prompt;
non-interactively a silent flip to a local build).
Scope: narrow per RND-564/RND-571. The broader RND-589 consolidation
(--instance-type/--environment defaults, --non-interactive+CI detection,
all-at-once required-flag errors, env bindings, version-check hook) is
intentionally out of scope here.
Tested: 12 new prompts.test.ts cases (73 pass); manual non-interactive deploy
on sepolia-dev runs through every prompt to the on-chain step.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er (RND-589) isNonInteractive now keys off --non-interactive flag, CI=true, then !isTTY. Add collectMissingRequiredInputs to report every missing required deploy/upgrade input in one error instead of cascading one prompt at a time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ha prod default (RND-589) - Add --non-interactive common flag (env ECLOUD_NON_INTERACTIVE). - getInstanceTypeInteractive: in non-interactive mode default to g1-standard-2s (deploy) or reuse the pinned defaultSKU (upgrade) instead of erroring. - Default --environment to mainnet-alpha on prod builds (was sepolia). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lt + ECLOUD_FORCE (RND-589) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ ECLOUD_APP_ID/ECLOUD_FORCE (RND-589) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The update prompt ran before any command, with no TTY guard, so in CI/agents it threw and read as the command failing. Skip it (and snooze the check) when CI=true or no TTY. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-op (RND-589) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…RND-589) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…into RND-589 branch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One-shot status via getStatuses; --json emits { appId, status }. --wait blocks
via the bounded watchDeployment machinery (honors --watch-timeout /
ECLOUD_WATCH_TIMEOUT_SECONDS), catches WatchTimeoutError with a recovery hint,
then prints a final status read. Gives agents a supported wait mechanism
instead of tight-looping 'app info'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pp info' (RND-592) Rewrite Gate 3 to use the bounded 'app status --wait' instead of a bare 'app info' poll loop (the tight loop that trips rate limits). Add --json to the supported-flags list and update the upgrade gate. Also brings SKILL.md into prettier compliance (was pre-existing non-conforming; whitespace-only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onfirm behavior The PR #162 merge changed confirmWithDefault to return the default in non-TTY mode instead of throwing, so promptUseVerifiableBuild(false) now resolves to false (regular build) rather than erroring. Update the two tests that asserted the old throw behavior. Mainnet deploy confirm stays safe: it's gated on !flags.force and defaults to false, so non-TTY without --force cancels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…f throwing (RND-589) Optional yes/no confirms must not block a non-interactive run. In non-TTY mode confirmWithDefault now returns its default rather than throwing 'Use --force'. Safe for the mainnet deploy confirm: that path is gated on !flags.force and defaults to false, so a non-interactive mainnet deploy without --force cancels rather than auto-proceeding. Makes the committed tests (which assert this) match the code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-591) Callers keying off exit status can now tell which stage failed: 2 = invalid/missing input (pre-build) 3 = build/push failed (no on-chain tx attempted) 4 = build OK but on-chain tx failed (image already pushed; re-run reuses it) Wrap prepare* (build) and execute* (on-chain) in stage-labeled try/catch in both deploy.ts and upgrade.ts; the non-interactive precheck now exits 2. On-chain failure message states the image was already built+pushed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ND-591) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-flight check (RND-597) Three gaps that let an arm64 image deploy and crash on first request in the TEE: 1. digest.ts: the 'architecture undetectable' branch assumed linux/amd64 and returned without verifying. Now throws createPlatformErrorMessage (fail closed). 2. prepare.ts: verify the remote --image-ref is linux/amd64 (docker manifest inspect, no pull) BEFORE layerRemoteImageIfNeeded, so an arm64 ref fails in ~seconds with the buildx/--verifiable remediation instead of after a multi-minute pull+layer+push. 3. dockerhub.ts: resolveDockerHubImageDigest (prebuilt verifiable images) now asserts a linux/amd64 manifest — checks the index for an amd64 entry, or the config blob's architecture for single-platform — rejecting otherwise. --verifiable --repo --commit is unchanged (server-side build, no local Docker; never hits these paths). Tests: digest.test.ts (undetectable→throw, single-platform arm64, multi-platform no-amd64) + dockerhub.test.ts (multi/single platform accept+reject). 40 SDK + 99 CLI tests pass. E2E sepolia-dev: arm64 --image-ref rejected pre-push in ~16s (exit 3); amd64 passes pre-flight and proceeds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…RND-596) Compute credits do not pay on-chain gas (paid by the EOA via EIP-7702), so an agent funded only with credits had its deploy tx revert with no machine-readable pre-check. Add a typed pre-flight gate in the SDK so BOTH CLI and SDK/agent paths are protected: - New InsufficientGasError + assertSufficientGas(publicClient, address, gasEstimate) in common/gas/insufficientGas.ts; threshold is gasEstimate.maxCostWei (not zero — dust below cost still fails). Exported from the SDK index. - Call it at all 4 prepare* gas-estimate sites (deploy + upgrade, normal + verifiable), right after estimateBatchGas, before returning. - billing status now shows the wallet's on-chain ETH (best-effort) so the credit-vs-gas gap is visible pre-deploy. Tests: assertSufficientGas (above/equal/dust-below/error-shape). 44 SDK + 99 CLI tests pass. E2E sepolia-dev: a 0.0044-ETH wallet is blocked with 'needs ~0.0198 ETH ... credits do not pay on-chain gas'; billing status shows the ETH line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Strip RND-* ticket references from code comments and test names across the CLI and SDK packages. Ticket tracking belongs in the PR/commit/branch, not in committed source. Comment-only and describe()-label changes — no behavior change; all affected tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ions
Replace bare `let prepared, gasEstimate;` / `let res;` (implicit any) with
explicit annotations in the deploy and upgrade command flows. None take
undefined — the catch block calls this.error(..., {exit}) (returns never),
so the vars are definitely assigned. Types sourced from the SDK's exported
Prepare{Deploy,Upgrade}Result / GasEstimate and Awaited<ReturnType<...>>.
No behavior change; deploy/upgrade tests + prettier pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The optional-input helpers (env-file, log-visibility, resource-usage- monitoring, dockerfile) called isNonInteractive() internally, re-deriving the decision from process.stdin/CI on every call. That dropped the --non-interactive flag: the bare call only sees CI + !isTTY, so --non-interactive on a real TTY (CI unset) still prompted. Resolve isNonInteractive(flags) once at the command boundary in deploy/ upgrade and thread the boolean into each helper as a parameter — matching the existing getInstanceTypeInteractive(..., nonInteractive) shape. No global state: helpers are now pure and unit-testable by passing the bool (no process.stdin mocking), and --non-interactive is honored everywhere. Tests: added a block asserting each helper honors the injected decision on a TTY (the dropped-flag case), plus a sanity check that nonInteractive=false still reaches the prompt. 104 CLI tests pass; eslint + prettier clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wallet-ETH balance read in `billing status` was wrapped in an empty catch that discarded the error. It lumped three distinct failures — malformed --private-key, bad environment config, and transient RPC errors — into a silent no-op: the user saw no ETH line and no reason why. Keep it best-effort (must not abort `billing status`) but warn with the reason via this.warn, matching the existing pattern in app list/info. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getDockerfileInteractive / getEnvFileInteractive / getLogSettingsInteractive / getResourceUsageMonitoringInteractive / getInstanceTypeInteractive each take a nonInteractive argument now, so the "Interactive" suffix read contradictorily (getDockerfileInteractive(..., nonInteractive)). These helpers resolve a value from flag/default/prompt in either mode, so rename to getDockerfile / getEnvFile / getLogSettings / getResourceUsageMonitoring / getInstanceType and update their doc comments. The genuinely interactive-only helpers (getImageReferenceInteractive, getEnvironmentInteractive, etc.) keep their suffix. Also clarify exitCodes.ts: exit 1 is oclif's default for unclassified errors, not one we define — the doc comment listed it alongside our 2/3/4. No behavior change; 106 CLI tests pass; eslint + prettier clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…exit code
The old exitCodes test only asserted the EXIT_CODES constants and a trivial
errorMessage helper — it would pass even if the codes were never wired into
the commands. And the six this.error(..., { exit }) blocks (3 stages x deploy
/upgrade) duplicated the message + code mapping inline.
Extract a pure stageFailure(operation, stage, err) -> { message, exit } that
owns the mapping, and call it from both commands. Now the real logic is unit-
tested directly (invalid-input -> 2, build -> 3 "no X was attempted",
onchain -> 4 "image already pushed, re-run reuses it"), with operation-specific
wording, and the duplication is gone.
110 CLI tests pass; eslint + prettier clean; no new tsc errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…; dedup instance-type fetch
Two correctness fixes surfaced by review:
1. Insufficient-gas failures were misclassified as exit 3 ("build failed,
no deployment attempted"). assertSufficientGas runs inside prepare*() AFTER
the image is built+pushed, so an InsufficientGasError surfaces through the
build try/catch — but the image already exists. stageFailure now reclassifies
InsufficientGasError as on-chain (exit 4, "re-run reuses the pushed image"),
matching the documented invariant and giving agents an accurate signal.
2. Non-interactive deploy/upgrade with --dockerfile but no --image-ref slipped
past the all-at-once required-input check, then threw at the interactive
--image-ref prompt as an unclassified exit 1. collectMissingRequiredInputs
now requires --image-ref (the push destination) when building from a local
Dockerfile, so it's reported as invalid-input (exit 2) up front.
Also dedup: fetchAvailableInstanceTypes was byte-identical in deploy.ts and
upgrade.ts — extracted to utils/instanceTypes.ts. (This also removes one
duplicate copy of a pre-existing SkuInfo fallback-shape tsc error: 43 -> 41.)
113 CLI tests pass; eslint + prettier clean; no new tsc errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The platform-rejection message from digest.ts (hit by the prepare.ts pre-flight for a plain --image-ref) still pointed at `docker build` + "use the SDK", while the CLI's prebuilt-verifiable path (dockerhub.ts) already recommended `docker buildx ... --push` OR a server-side verifiable build (--verifiable --repo <repo> --commit <sha>). An arm64 --image-ref is exactly the case where the verifiable-build escape hatch is most useful, so both arm64 entry points now give the same remediation. Updated createPlatformErrorMessage to match dockerhub.ts and asserted both `buildx` and `--verifiable --repo --commit` appear. SDK + CLI unit suites pass; prettier clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…keep --json pure Addresses review feedback on #164: - --wait now does a one-shot read first and only blocks while the app is in a transitional status (created/deploying/upgrading/resuming/stopping/ terminating), matched case-insensitively. Settled statuses (Running, Stopped, Terminated, Suspended, Failed) return immediately instead of polling until the watch timeout. - --wait --json no longer corrupts stdout: the SDK compute module now accepts a logger override, and the command routes SDK progress output ("Waiting for app to start...", "Status: ...") to stderr in JSON mode so stdout stays a single JSON object. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pp status --wait, staged exit codes, gas pre-flight + amd64 hardening (RND-589/591/592/596/597 + RND-567/568/569) (#164) Big consolidated CLI PR for the agent-driven deploy path. Targets `release/v1.0.0`. ## Tickets included | Ticket | What | |---|---| | **[RND-589](https://linear.app/eigenlabs/issue/RND-589)** (supersedes [RND-564](https://linear.app/eigenlabs/issue/RND-564), [RND-571](https://linear.app/eigenlabs/issue/RND-571)) | Non-interactive deploy/upgrade: default safe flags, error all-at-once on required ones | | **[RND-592](https://linear.app/eigenlabs/issue/RND-592)** | `app status [--wait] [--json]` so agents stop tight-looping `app info` | | **[RND-591](https://linear.app/eigenlabs/issue/RND-591)** | Distinct exit codes for deploy/upgrade failure stages | | **[RND-597](https://linear.app/eigenlabs/issue/RND-597)** | Harden amd64 enforcement: close assume-amd64 hole, pre-flight platform check, prebuilt-image arch check | | **[RND-596](https://linear.app/eigenlabs/issue/RND-596)** | Block deploy/upgrade when wallet ETH < estimated gas (credits don't pay on-chain gas) | | **[RND-567](https://linear.app/eigenlabs/issue/RND-567) / [568](https://linear.app/eigenlabs/issue/RND-568) / [569](https://linear.app/eigenlabs/issue/RND-569)** (folded from PR #162) | billing-cancel commonFlags; bounded upgrade & deploy watch with timeout + `WatchTimeoutError` | ## Highlights **Non-interactive ([RND-589](https://linear.app/eigenlabs/issue/RND-589))** - `isNonInteractive()` = `--non-interactive` flag › `CI=true` › `!isTTY`; new `--non-interactive` common flag. - Defaults in non-TTY (logged): `--instance-type` → `g1-standard-2s` (deploy) / pinned type (upgrade), `--log-visibility` → private, `--resource-usage-monitoring` → disable, `--env-file` → `.env`-or-none. - All-at-once error listing every missing required input (image source + name/app-id). - `--environment` → `mainnet-alpha` on prod builds; version-check hook no-ops non-interactively; env bindings `ECLOUD_FORCE` / `ECLOUD_APP_ID`. - `confirmWithDefault` returns its default in non-TTY instead of throwing (mainnet confirm stays safe: gated on `!force`, defaults to cancel). **`app status` ([RND-592](https://linear.app/eigenlabs/issue/RND-592))** - One-shot via `getStatuses`; `--json` → `{appId,status}`. `--wait` blocks via the bounded watch machinery (honors `--watch-timeout`/`ECLOUD_WATCH_TIMEOUT_SECONDS`), catches `WatchTimeoutError` with a recovery hint, then prints a final read. - `requestWithRetry` now retries `502/503/504` in addition to `429`. - Agent SKILL.md: use `status --wait`, do not loop `app info`. **Exit codes ([RND-591](https://linear.app/eigenlabs/issue/RND-591))** - `2` = invalid/missing input (pre-build), `3` = build/push failed (no on-chain tx), `4` = build OK but on-chain failed (image already pushed; re-run reuses it). Documented in SKILL.md. **amd64 enforcement ([RND-597](https://linear.app/eigenlabs/issue/RND-597))** - `digest.ts`: undetectable-architecture branch now throws instead of assuming amd64 (fail closed). - `prepare.ts`: verify a remote `--image-ref` is linux/amd64 **before** the slow pull+layer+push (fails in ~seconds, not minutes). - `dockerhub.ts`: `resolveDockerHubImageDigest` (prebuilt verifiable images) rejects a tag with no linux/amd64 manifest. - `--verifiable --repo --commit` unchanged (server-side build). **Gas pre-flight ([RND-596](https://linear.app/eigenlabs/issue/RND-596))** - New `InsufficientGasError` + `assertSufficientGas` in the SDK, called at all 4 `prepare*` gas-estimate sites — so deploy/upgrade throw on **both CLI and SDK/agent** paths when wallet ETH < `gasEstimate.maxCostWei` (threshold is the estimate, not zero). - `billing status` shows the wallet's on-chain ETH so the credit-vs-gas gap is visible pre-deploy. ## Testing - [x] 113 CLI + 45 SDK unit tests pass; `eslint` + `prettier` clean - [x] E2E sepolia-dev: headless deploy with only `--image-ref` + `--name` → all flags defaulted, on-chain createApp, app `STARTED` - [x] E2E: missing inputs → one all-at-once error, **exit 2** - [x] E2E: bad image → build fails, **exit 3** - [x] E2E: valid image + bad nonce → built+pushed then on-chain fails, **exit 4** with reuse hint - [x] E2E: `app status --json` and `--wait` (timeout + final read) - [x] E2E: arm64 `--image-ref` rejected pre-push in ~16s with remediation ([RND-597](https://linear.app/eigenlabs/issue/RND-597)); amd64 passes pre-flight - [x] E2E: 0.0044-ETH wallet blocked with `needs ~0.0198 ETH ... credits do not pay on-chain gas` ([RND-596](https://linear.app/eigenlabs/issue/RND-596)); `billing status` shows the ETH line - [x] mainnet-alpha prod default locked in by unit test (no live mainnet deploy) - [x] Rebased onto `release/v1.0.0` (carries the v1.4/v1.5 ContainerPolicy ABI fix #165) ## Supersedes / closes - Closes **[RND-564](https://linear.app/eigenlabs/issue/RND-564)**, **[RND-571](https://linear.app/eigenlabs/issue/RND-571)** (duplicates of [RND-589](https://linear.app/eigenlabs/issue/RND-589)). - Folds in **PR #162** ([RND-567](https://linear.app/eigenlabs/issue/RND-567)/[568](https://linear.app/eigenlabs/issue/RND-568)/[569](https://linear.app/eigenlabs/issue/RND-569)) — that PR can be closed in favor of this one. ### Out of scope - [RND-572](https://linear.app/eigenlabs/issue/RND-572) (`--verifiable`+`--dockerfile`). - "App entered Failed state" in E2E is TEE provisioning of the bare-nginx test image — unrelated. ## Review follow-ups (post-initial-review) Hardening and cleanup from a follow-up review pass: - **Non-interactive decision is now injected, not re-derived.** The optional-input helpers (`getDockerfile`, `getEnvFile`, `getLogSettings`, `getResourceUsageMonitoring`, `getInstanceType`) take the resolved `isNonInteractive(flags)` boolean as a parameter instead of recomputing it from `process` internally. Fixes a latent bug where `--non-interactive` on a TTY (with `CI` unset) was ignored, and keeps the helpers pure/unit-testable. Dropped the now-misleading `Interactive` suffix from these dual-mode helpers. - **Exit-code classification fixes.** Extracted a pure `stageFailure(operation, stage, err)` mapping (removing the duplicated `this.error(..., { exit })` blocks across deploy/upgrade). Two corrections: an insufficient-gas failure is now **exit 4** (the image is already built+pushed) instead of being mislabeled **exit 3** "no deployment attempted"; and non-interactive `--dockerfile` without `--image-ref` is now reported up front as **exit 2** instead of throwing an unclassified **exit 1** at an interactive prompt. - **`billing status`** now warns (with the reason) when the wallet-ETH read fails instead of silently swallowing it; still best-effort and never aborts the command. - **Unified amd64 remediation text.** The platform-rejection message for a plain `--image-ref` (`digest.ts`) now matches the prebuilt path (`dockerhub.ts`): both recommend `docker buildx ... --push` **and** the server-side `--verifiable --repo <repo> --commit <sha>` escape hatch (most useful for arm64 hosts — no local Docker). Previously the plain path still said `docker build` / "use the SDK". - **`app status --wait` returns immediately on settled statuses.** The command now does a one-shot read first and only blocks while the app is in a transitional status (`Created`/`Deploying`/`Upgrading`/`Resuming`/`Stopping`/`Terminating`), matched case-insensitively. Settled statuses (`Running`/`Stopped`/`Terminated`/`Suspended`/`Failed`) return right away instead of polling until the watch timeout. Additionally, `--wait --json` no longer corrupts its output: the SDK compute module accepts a logger override, and the command routes SDK progress (`Waiting for app to start...`, `Status: ...`) to stderr in JSON mode so stdout stays a single JSON object. - **Cleanup:** explicit type annotations on declare-then-assign locals in deploy/upgrade; extracted the byte-identical `fetchAvailableInstanceTypes` into a shared `utils/instanceTypes.ts`. All CLI/SDK unit tests pass (117 CLI + 45 SDK); `eslint` + `prettier` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.