Greentic · Internal Build Status
Rebuilding how Greentic deploys software: an Environment you push bundles to, immutable Revisions, and traffic splitting that's separate from deploying — the pattern every modern cloud platform uses. Here's where we are, in plain words.
// what this section is: the 10-second summary — how far along each of the five phases is
The work is split into five phases. Phase 0 (security) is done, and the foundation of Phase A is mostly done. The runtime that actually routes traffic, the credential flows, and the real cloud deployers all come later — and depend on this foundation.
op env init). A10 is partway through its per-repo PR train; C3 and C4 remain.// what this section is: the problem we're solving and the three ideas the whole rebuild rests on
The problem. Today Greentic can only run one bundle per customer, and deploying a new version overwrites the old one in place. There's no safe way to roll out v2 to 1% of traffic while v1 keeps serving the other 99%. That's the gap we're closing.
The fix. Borrow the pattern from Cloud Run / Kubernetes and split three ideas that used to be tangled together:
① Environment — the place you deploy to (local today; later prod-eu, zain-prod).
It owns plug-in "env-packs" for secrets, telemetry, sessions, state, and the deployer itself.
② Revision — one immutable, frozen version of a deployed bundle. You can have several at once.
③ Traffic split — a separate dial that says "send 1% here, 99% there". Deploying ≠ shifting traffic.
Why it's built this way. New clouds (AWS, Kubernetes, GCP, Azure, Snap) drop in as plug-ins
without rewriting the core — each is described by a string like greentic.deployer.k8s@1.0.0,
not a hard-coded list. Add a cloud = publish a pack, not change core code.
// what this section is: every milestone shipped so far — what code actually changed, and why it mattered
Each milestone below shows Changed (what was actually built) and Why (the reason we did it). Phase 0 closed security holes first; Phase A then laid the foundation everything else stands on.
ChangedBundle builders (both ZIP and SquashFS) now skip dev secret files and plaintext setup answers instead of packing them in.
WhyBundles get uploaded and shared. A secret baked into an artifact is a credential leak — close it before any large refactor begins.
ChangedAdded a doctor secret-leak scanner plus a CI gate that greps the raw archive bytes and fails the build if a secret shape appears.
WhyOne fix isn't enough — a later change could quietly reintroduce the leak. The gate makes that regression impossible to merge.
ChangedNon-secret config is still written where the current runtime reads it, but that path is now marked deprecated with a one-time warning.
WhyWe're relocating config. Deprecate gradually so existing components keep working until the new config channel exists (Phase C).
ChangedBefore extracting a bundle, reject absolute paths, .., symlinks/hardlinks that escape the folder, and duplicate paths.
WhyExtracting an untrusted bundle could otherwise overwrite files anywhere on disk — the classic "zip-slip" attack.
ChangedNew crate greentic-deploy-spec holds every new type — Environment, Revision, TrafficSplit, BundleDeployment, Credentials — with validators (e.g. traffic weights must sum to 100%).
WhyThree repos were each inventing their own idea of "environment". One shared definition stops them drifting apart and becomes the single source of truth.
ChangedAn EnvironmentStore with a local-filesystem implementation: atomic writes (temp file + rename), a per-environment lock, and a backup before every change.
WhyThose types are useless if a crash mid-save corrupts them or two operators overwrite each other. This makes the state durable and concurrency-safe.
ChangedBuilt the full gtc-dev op surface — 8 nouns (env, env-packs, bundles, revisions, traffic, config, credentials, secrets) × their verbs — and dropped the old hard-coded --provider aws admin subcommands.
WhyOperators need one consistent way to drive the new model, not cloud-specific one-offs scattered across the CLI.
ChangedFirst gtc-dev setup <bundle> auto-creates the local environment with 5 sensible default env-packs (local-process, dev-store, stdout, in-memory sessions + state) before the bundle wizard runs.
WhyNobody should hand-write JSON to get started. Run one command against a real bundle, get a working local environment.
op env init verb#216ChangedNew gtc-dev op env init verb exposes the A4 helper directly — creates the local env if missing, fills in any missing default slots if it already exists, and is idempotent (returns "created" / "healed" / "untouched"). Audit-recorded per A7.
WhyA4's helper was only reachable as a side-effect of setup_or_update (which needs a valid bundle) or run_start (which needs to actually start). From an empty directory there was no clean path. init closes that gap — one verb, no bundle, no hack.
ChangedA migrate-dev command scans for legacy dev usage and migrates it; if migration isn't safe, it keeps a temporary dev alias that warns and can be hard-disabled with an env var.
WhyOld installs say dev everywhere. We want one canonical name (local) but can't yank it out from under live setups.
ChangedA lifecycle state machine (staged → warming → ready → draining → inactive → archived) with a validated transition matrix; you cannot archive a revision that still serves traffic.
WhyRollout state must stay coherent. Routing traffic to a half-warmed revision, or archiving a live one, would silently break deployments.
ChangedA one-shot migrate-state that moves the legacy state/deploy/<provider> tree into the new layout and fails loudly if anything is left behind.
WhyReading half-old, half-new state silently would cause subtle, hard-to-trace bugs. Migrate once; refuse to proceed on leftovers.
ChangedAn append-only audit log for every mutating command, plus a local authorization gate; changes to non-local environments fail closed.
WhyYou must be able to answer "who changed this, and when". And until real access control exists, refusing risky operations is safer than allowing them unguarded.
ChangedWire types + docs for a future remote store: compare-and-swap with ETags, idempotent retries, an integrity (corruption-detection) hash, RBAC decision and backup/restore shapes. No remote server yet — just the contract.
WhyProduction won't use local files. Pinning the contract now means the local path and the eventual cloud path behave identically — no surprises later.
ChangedAn env-pack registry that maps a descriptor like greentic.deployer.local-process@0.1.0 to a real handler, with built-ins for the 5 local defaults and a plug-in registration hook (EnvPackHandler trait, EnvPackRegistry::register). op env doctor already consults the registry.
WhyThis is the seam every cloud plugs into later. Without it, the descriptor strings are just labels with no code behind them.
ChangedPer-repo train threading env_id through the four wizards: qa-lib#46 added the env_id field + a SecretsSink seam and merged; greentic-bundle#117 threaded it through 6 wizard helpers + 3 WizardRunConfig sites and merged. Setup, operator, and dw PRs still pending.
WhyRight now a wizard answer doesn't know which environment it belongs to; scoping it keeps each environment's secrets in the right place. Per-repo PRs keep each change reviewable.
ChangedA preflight that checks tool versions, credentials, and cluster/region access — and prints "install X with Y" instead of crashing mid-deploy.
WhyA clear up-front error saves hours versus a confusing failure halfway through provisioning a cloud.
ChangedDistroless + static MUSL builds running as non-root (uid 65532); replace the unsquashfs shell-out with a Rust library.
WhySmaller images, no shell to exploit, no root — standard production hardening for what ships to customers.
greentic-deploy-spec test suite green (98 tests);
A8's remote.rs / integrity.rs / audit.rs shipped (#214);
A9 env-pack registry merged (#215, squash 3e52f88);
A4 follow-up op env init verb merged (#216, squash 2e7d230) with +3 lib tests covering created / healed / idempotent-untouched. The installed gtc-dev /
greentic-deployer-dev binaries lag #215+#216 until the next nightly dev publish.// what this section is: commands you can actually run today to see the foundation working
The whole gtc-dev op surface runs against the local environment today — real
on-disk state under ~/.greentic/environments/local/, real file locking, a real audit log.
Cloud deploys aren't wired yet (Phase D), so credentials and secrets put/get
return an honest "not yet implemented".
gtc-dev — the binary name on the development lane. The dev build
of the CLI publishes as gtc-dev (so cargo binstall gtc-dev works with no flags); the
stable release is plain gtc. Same commands, just the dev binary name.list / show / doctor) take a positional <env_id>. Every mutating verb
(create, add, warm, traffic set, …) takes its entire payload as a JSON
file via --answers <file>. There is no inline <rev>=<weight> syntax. Use
--schema on any verb to print its JSON shape, then write the file. This is intentional — answer
files are reproducible, scriptable, and auditable.local env — fixed.
The A4 follow-up (PR #216) added a first-class gtc-dev op env init verb that
creates or heals the local env directly — no bundle path, no --bundle /dev/null
trick. Note: if your installed gtc-dev predates the next nightly dev publish, run
cargo binstall gtc-dev (or build from source) to pick it up.local environment# the canonical answer: one verb, no bundle, idempotent $ gtc-dev op env init { "outcome": "created", "added_slots": ["deployer","secrets","telemetry","sessions","state"] } # run it again — idempotent, no audit churn $ gtc-dev op env init { "outcome": "untouched" } # or run it on an env that's missing some slots — heals only the missing ones $ gtc-dev op env init { "outcome": "healed", "added_slots": ["telemetry","sessions"] } # still want the bundle path? `gtc-dev setup` runs the same helper after bundle validation $ gtc-dev setup ./my-bundle.gtbundle
$ gtc-dev op env list local ready (5 packs) $ gtc-dev op env show local $ gtc-dev op env doctor local ✓ local environment healthy (validate: ok, no version skew) $ gtc-dev op env-packs list local deployer greentic.deployer.local-process@0.1.0 secrets greentic.secrets.dev-store@0.1.0 telemetry greentic.telemetry.stdout@0.1.0 sessions greentic.sessions.in-memory@0.1.0 state greentic.state.in-memory@0.1.0 $ gtc-dev op bundles list local $ gtc-dev op revisions list local
# step 1: ask the verb for its JSON shape $ gtc-dev op bundles add --schema { "environment_id": "...", "bundle_id": "...", "route_binding": {...}, "customer_id": "local-dev" } # step 2: write the payload $ cat > bundle-add.json <<'EOF' { "environment_id": "local", "bundle_id": "fast2flow", "route_binding": { "host": "localhost", "path_prefix": "/" } } EOF # step 3: apply $ gtc-dev op bundles add --answers bundle-add.json $ gtc-dev op revisions list local 01J8... staged
# weights are basis-points (0–10000) or percent (0–100); entries must sum to 100% $ gtc-dev op traffic set --schema $ cat > traffic-set.json <<'EOF' { "environment_id": "local", "deployment_id": "01J...", "idempotency_key": "first-rollout", "entries": [ { "revision_id": "01J...", "weight_percent": 100 } ] } EOF $ gtc-dev op traffic set --answers traffic-set.json
# every mutating command above appended a line here (A7) $ cat ~/.greentic/environments/local/audit/events.jsonl {"verb":"create","authorization":{"decision":"allow",...},"result":{"outcome":"ok"}} # dev → local migration scanner (A4b) — through gtc-dev op $ gtc-dev op env migrate-dev local --check # legacy on-disk state migration (A6) — currently ONLY via the deployer binary, # not yet routed through `gtc-dev op env` $ greentic-deployer-dev op env migrate-state local --check
$ cd greentic-deployer && cargo test -p greentic-deploy-spec test result: ok. 98 passed; 0 failed
One verb, no bundle, idempotent. Outcomes: created / healed / untouched.
Take a positional <env_id>. The fast way to inspect any local state.
Payload via --answers file.json. Use --schema on any verb to print the shape.
Return a clear "not yet implemented" — they depend on Phase B/C/D.
// what this section is: what's left, in the order we'll build it
Finish Phase A (4 gates left), then Phase B is the big one — it's where traffic splitting actually routes live requests.
qa-lib#46 + greentic-bundle#117 merged; setup, operator, and dw PRs still pending.unsquashfs shell-out.Two-mode credential flow (validate vs. bootstrap-then-export-rules-pack); a non-secret runtime config channel; env-packs contribute their own wizards.
AWS ECS as the first proving ground; Kubernetes is Zain's production target ("Zain ready" = K8s shipped). Then GCP, Azure, Snap/Juju — all as drop-in env-packs.