Greentic · Internal Build Status

Next-Generation Deployment Platform

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.

Updated 2026-05-20 Plan next-gen-deployment.md Phase 0 complete Phase A 10 of 13 gates landed CLI gtc-dev (develop lane)

01At a glance

// 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.

Phase 0 — Security hotfix stop leaking dev secrets
4 / 4 · done
Dev secrets no longer leak into built bundles; archive extraction is hardened against path-traversal & symlink escapes.
Phase A — Foundations the new object model + CLI
10 / 13 · in progress
A1–A9 all landed (plus an A4 follow-up that adds op env init). A10 is partway through its per-repo PR train; C3 and C4 remain.
Phase B — Runtime, multi-bundle, traffic splitter make it actually route
0 / 16 · not started
Where two versions of a deployment run side-by-side and 1% / 99% traffic splits become real.
Phase C — Credentials & runtime config two-mode credential flow
0 / 7 · not started
Validate minimum-privilege creds, or bootstrap them from admin and export a rules-pack.
Phase D — Real cloud deployers AWS proving ground, K8s for Zain
0 / 12 · not started
Each cloud ships as a plug-in "env-pack". "Zain ready" = the Kubernetes deployer shipped.
landed in progress planned

02What we're building, in plain words

// 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.

03What's been done — milestone by milestone

// 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.

Phase 0 — Security hotfix complete

P0.1Stop secrets leaking into bundlesbundle#112 · setup#109

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.

P0.2An automated guard against future leaks

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.

P0.3Don't break what's already running

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).

P0.4Refuse malicious archives

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.

Phase A — Foundations 10 of 13

A1Define the vocabulary oncedeployer#196

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.

A2Save it safely#197 · #198 · #199

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.

A3One command line for everything#200 · op#71 · gtc#220

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.

A4Works out of the box#204 · #205 · #206

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.

A4+A first-class op env init verb#216

ChangedNew 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.

A4bRename "dev" → "local" without breaking anyone#207 · setup#113 · config#43

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.

A5A revision can't skip steps#208 · #210 · dc#146/147

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.

A6Move old state once, loudly#211

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.

A7Every change is recorded#213

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.

A8Agree the contract before building the remote store#214 · op#74

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.

A9Turn a string into running code#215

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.

A10Wizards know which environment they're forin progressqa-lib#46 · bundle#117 merged

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.

C3Fail early, with helpplanned

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.

C4Tiny, locked-down imagesplanned

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.

Verified 2026-05-20 against live repos: 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.

04What you can test right now

// 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".

Examples use 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.
⚠ How mutating verbs work. Read-only verbs (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.
✓ Bootstrapping the 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.

① Bootstrap the local environment

A4 + A4 follow-up (#216)
# 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

② Read-only commands — these just work

inspection
$ 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

③ Mutating commands — the schema-then-answers pattern

bundles & revisions (A3 + A5)
# 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

④ Set a 100% traffic split (real example, real weights)

traffic set (A3 + A5)
# 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

⑤ Audit log & legacy migration

audit (A7) & migrations (A4b + A6)
# 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

⑥ Run the foundation's own test suite

greentic-deploy-spec (A1 + A5 + A8)
$ cd greentic-deployer && cargo test -p greentic-deploy-spec
test result: ok. 98 passed; 0 failed
bootstrap · #216

op env init

One verb, no bundle, idempotent. Outcomes: created / healed / untouched.

read-only · works

list · show · doctor

Take a positional <env_id>. The fast way to inspect any local state.

mutating · works

create / add / warm / set / migrate-dev …

Payload via --answers file.json. Use --schema on any verb to print the shape.

stubbed on purpose

credentials · secrets put/get/rotate · env destroy

Return a clear "not yet implemented" — they depend on Phase B/C/D.

05What's next — the near future

// 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.

Finishing Phase A

Then Phase B — the payoff

Phase C

Credentials & runtime config

Two-mode credential flow (validate vs. bootstrap-then-export-rules-pack); a non-secret runtime config channel; env-packs contribute their own wizards.

Phase D

Real cloud deployers

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.